核心问题

谁是谁?谁在什么关系里?谁能做什么?谁对什么负责?

IdentityRolePermissionOwnership 经常被混在一个字段里:

type User = {
  id: string
  role: "student" | "instructor" | "admin"
}

这在早期看起来很简单,但很快会出问题。

因为这四个词回答的是四类不同问题:

  • Identity:你是谁?
  • Role:你在某个上下文里扮演什么角色?
  • Permission:你能做什么动作?
  • Ownership:这个东西归谁负责、控制或拥有?

把它们混在一起,系统会变得脆弱。

Identity:你是谁

Identity 解决的是识别问题。

在课程平台里,至少要区分:

type Person = {
  id: string
  name: string
}

type Account = {
  id: string
  personId?: string
  status: "active" | "suspended" | "deleted"
}

type Credential = {
  accountId: string
  kind: "email" | "phone" | "oauth"
  identifier: string
}

这里的边界是:

  • Person 是现实中的人。
  • Account 是系统里的账号。
  • Credential 是登录凭证。

有些系统不需要 Person,只有 Account 就够。关键不是一定要拆,而是要知道自己是否在混用概念。

判断问题:

这个字段描述的是现实中的人,还是系统里的账号,还是登录方式?

如果答案经常变化,说明 User 这个名字太宽了。

Role:上下文里的身份

Role 不是人的本质,而是人在某个上下文里的身份。

例如:

type OrganizationMembership = {
  accountId: string
  organizationId: string
  role: "owner" | "manager" | "member"
}

这里的 role 只在 organizationId 这个上下文里成立。

同一个账号可以在 A 公司是 owner,在 B 公司是 member。

所以不要写:

account.role = "organization_owner"

这会错误地把上下文身份变成全局属性。

类似地,课程里的角色也应该带上下文:

type CourseStaffAssignment = {
  accountId: string
  courseId: string
  role: "primary_instructor" | "assistant" | "reviewer"
}

原则:

角色必须回答“在哪个上下文里”。

如果一个 role 没有上下文,它很可能会膨胀成错误的全局身份。

Permission:能不能做某件事

Permission 解决的是动作授权问题。

例如:

canEditCourse(accountId, courseId)
canPublishCourse(accountId, courseId)
canInviteOrganizationMember(accountId, organizationId)
canViewRevenueReport(accountId, courseId)

权限不应该简单等同于角色。

坏代码:

if (user.role === "admin") {
  editCourse()
}

更好的代码:

if (await canEditCourse(accountId, courseId)) {
  editCourse()
}

因为权限可能来自多个事实:

  • 平台管理员可以编辑所有课程。
  • 课程 owner 可以编辑自己的课程。
  • 助教可以编辑部分内容。
  • 企业管理员可以编辑企业内部课程配置,但不能编辑课程正文。

如果调用者只判断 role,它必须知道太多领域细节。

权限函数的价值是:

把复杂授权规则藏在一个可信边界后面。

Ownership:谁拥有或负责

Ownership 回答的是归属和责任问题。

例如:

type Course = {
  id: string
  ownerAccountId: string
}

这表示课程由某个账号拥有。

但在更复杂的平台里,课程可能属于组织:

type Course = {
  id: string
  ownerType: "account" | "organization"
  ownerId: string
}

这里要小心。通用 ownership 很容易变成过早抽象。

如果当前只有个人讲师拥有课程,先写:

ownerAccountId

等组织课程真的出现,再迁移到 owner model。

Ownership 和 Permission 也不能混为一谈。

拥有者通常有权限,但有权限的人不一定是拥有者。

例如:

  • 课程 owner 可以编辑课程。
  • 助教也可以编辑课程部分内容,但不是 owner。
  • 平台管理员可以下架课程,但不是 owner。
  • 企业管理员可以分配课程,但不拥有课程。

原则:

Ownership 是归属关系,Permission 是动作判断。

Access:能不能使用

课程平台里还有一个容易混淆的概念:Access。

Access 回答的是:

这个账号现在能不能访问某个资源?

例如课程访问权:

type CourseAccessGrant = {
  accountId: string
  courseId: string
  validFrom: Date
  validUntil?: Date
  source: "purchase" | "subscription" | "organization_assignment" | "coupon"
}

访问权可以来自多个来源:

  • 购买
  • 订阅
  • 企业分配
  • 兑换码
  • 活动赠送

调用方最好只问:

canAccessCourse(accountId, courseId)

而不是自己判断:

hasPaidOrder(accountId, courseId) ||
hasActiveSubscription(accountId) ||
hasOrganizationAssignment(accountId, courseId)

Access 和 Permission 的区别是:

  • Access 偏向能不能使用资源。
  • Permission 偏向能不能执行管理动作。

例如:

  • canAccessCourse:能不能看课。
  • canEditCourse:能不能编辑课。
  • canPublishCourse:能不能发布课。
  • canAssignCourse:能不能把课分配给别人。

一个错误模型

type User = {
  id: string
  role: "student" | "instructor" | "admin" | "enterprise_admin"
  organizationId?: string
  canEditCourse?: boolean
  canViewCourse?: boolean
  isOwner?: boolean
}

这个模型把多个维度混在一起:

  • role 是上下文身份,却被做成全局字段。
  • organizationId 暗示一个人只能属于一个组织。
  • canEditCourse 没有说明是哪门课。
  • isOwner 没有说明拥有什么。
  • canViewCourse 把访问权存成了派生状态。

这类模型早期写起来快,后期会制造大量特殊判断。

一个更清楚的模型

type Account = {
  id: string
  status: "active" | "suspended"
}

type OrganizationMembership = {
  accountId: string
  organizationId: string
  role: "owner" | "manager" | "member"
}

type Course = {
  id: string
  ownerAccountId: string
}

type CourseStaffAssignment = {
  accountId: string
  courseId: string
  role: "primary_instructor" | "assistant" | "reviewer"
}

type CourseAccessGrant = {
  accountId: string
  courseId: string
  validFrom: Date
  validUntil?: Date
  source: "purchase" | "subscription" | "organization_assignment" | "coupon"
}

再用规则函数表达动作:

canAccessCourse(accountId, courseId)
canEditCourse(accountId, courseId)
canPublishCourse(accountId, courseId)
canAssignCourse(accountId, organizationId, courseId)

这个模型看起来类型更多,但每个概念边界更窄。

判断口诀

遇到 rolepermissionowneraccess 时,先问:

  1. 这是全局事实,还是上下文事实?
  2. 它是在描述身份,还是授权动作?
  3. 它是归属关系,还是临时访问权?
  4. 它应该被存储,还是从事实推导?
  5. 它失效时,谁负责更新?

如果一个字段回答不了这些问题,就先不要放进核心模型。

小结

  1. Identity、Role、Permission、Ownership 是四类问题,不要混成一个 role 字段。
  2. Identity 解决“你是谁”。
  3. Role 解决“你在某个上下文里是什么身份”。
  4. Permission 解决“你能不能做某个动作”。
  5. Ownership 解决“这个东西归谁拥有或负责”。
  6. Access 解决“你能不能使用某个资源”。
  7. 权限应该通过规则函数表达,而不是让调用者到处判断角色。