核心问题
谁是谁?谁在什么关系里?谁能做什么?谁对什么负责?
Identity、Role、Permission、Ownership 经常被混在一个字段里:
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)
这个模型看起来类型更多,但每个概念边界更窄。
判断口诀
遇到 role、permission、owner、access 时,先问:
- 这是全局事实,还是上下文事实?
- 它是在描述身份,还是授权动作?
- 它是归属关系,还是临时访问权?
- 它应该被存储,还是从事实推导?
- 它失效时,谁负责更新?
如果一个字段回答不了这些问题,就先不要放进核心模型。
小结
- Identity、Role、Permission、Ownership 是四类问题,不要混成一个
role字段。 - Identity 解决“你是谁”。
- Role 解决“你在某个上下文里是什么身份”。
- Permission 解决“你能不能做某个动作”。
- Ownership 解决“这个东西归谁拥有或负责”。
- Access 解决“你能不能使用某个资源”。
- 权限应该通过规则函数表达,而不是让调用者到处判断角色。