ABAC 权限模型
要点
- ABAC(Attribute-Based Access Control)基于属性做权限判断
- 属性包括:主体属性(用户角色、部门)、资源属性(类型、所有者)、环境属性(时间、IP)
- ABAC 比 RBAC 更灵活,但实现更复杂
- 策略可以用代码、DSL 或 JSON 表达
- AI 项目里,ABAC 适合多租户、复杂配额、资源级权限
- CASL、Open Policy Agent(OPA)是实现 ABAC 的工具
1. ABAC vs RBAC
1.1 RBAC 的局限
RBAC 基于角色,适合简单的权限模型:
// RBAC
if (user.role === 'admin') {
// 允许
}
if (user.role === 'editor' && resource.type === 'article') {
// 允许
}RBAC 的问题:
- 角色爆炸:每个场景都需要新角色
- 不够灵活:无法表达「用户只能访问自己的资源」
- 无法考虑环境:无法表达「只在工作时间访问」
1.2 ABAC 的优势
ABAC 基于属性,可以表达更复杂的规则:
// ABAC
if (
user.department === resource.department &&
user.clearance >= resource.classification &&
isBusinessHours()
) {
// 允许
}属性类型:
- 主体属性:用户 ID、角色、部门、级别
- 资源属性:资源类型、所有者、标签、状态
- 环境属性:时间、IP、设备
- 操作属性:读、写、删除
2. 策略定义
2.1 代码表达
最简单的 ABAC 是用代码表达策略:
type Action = 'read' | 'write' | 'delete'
type Subject = {
id: string
role: string
department: string
clearance: number
}
type Resource = {
type: string
ownerId: string
department: string
classification: number
status: string
}
function canAccess(
subject: Subject,
action: Action,
resource: Resource,
environment: { time: Date; ip: string }
): boolean {
// 管理员可以访问所有资源
if (subject.role === 'admin') return true
// 用户只能读自己的资源
if (action === 'read' && resource.ownerId === subject.id) return true
// 同部门可以读写
if (
(action === 'read' || action === 'write') &&
subject.department === resource.department
) {
return true
}
// 高机密资源需要高权限
if (resource.classification > subject.clearance) return false
// 只能在工作时间访问
const hour = environment.time.getHours()
if (hour < 9 || hour > 18) return false
return false
}2.2 策略引擎
更复杂的场景可以用策略引擎:
// 策略定义
const policies = [
{
name: 'admin-full-access',
description: 'Admins can access everything',
effect: 'allow',
condition: (subject: Subject) => subject.role === 'admin',
},
{
name: 'owner-read',
description: 'Owners can read their own resources',
effect: 'allow',
condition: (subject: Subject, action: Action, resource: Resource) =>
action === 'read' && resource.ownerId === subject.id,
},
{
name: 'department-access',
description: 'Department members can read/write department resources',
effect: 'allow',
condition: (subject: Subject, action: Action, resource: Resource) =>
(action === 'read' || action === 'write') &&
subject.department === resource.department,
},
{
name: 'business-hours',
description: 'Access only during business hours',
effect: 'deny',
condition: (subject: Subject, action: Action, resource: Resource, env: any) => {
const hour = env.time.getHours()
return hour < 9 || hour > 18
},
priority: 100, // 高优先级
},
]
// 评估策略
function evaluate(
subject: Subject,
action: Action,
resource: Resource,
environment: any
): boolean {
// 按优先级排序
const sortedPolicies = [...policies].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
for (const policy of sortedPolicies) {
if (policy.condition(subject, action, resource, environment)) {
return policy.effect === 'allow'
}
}
return false // 默认拒绝
}3. CASL
CASL 是 JavaScript 的 ABAC 库:
3.1 定义能力
import { AbilityBuilder, createMongoAbility } from '@casl/ability'
function defineAbilitiesFor(user: User) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility)
// 管理员可以管理所有
if (user.role === 'admin') {
can('manage', 'all') // manage 是所有操作的别名
}
// 用户可以读所有文章
can('read', 'Article')
// 用户可以写自己的文章
can('create', 'Article')
can('update', 'Article', { authorId: user.id })
can('delete', 'Article', { authorId: user.id })
// 用户不能删除已发布的文章
cannot('delete', 'Article', { status: 'published' })
// 用户可以读自己的评论
can('read', 'Comment', { userId: user.id })
return build()
}
// 使用
const ability = defineAbilitiesFor(user)
ability.can('read', 'Article') // true
ability.can('update', article) // 检查 article.authorId === user.id
ability.can('delete', publishedArticle) // false3.2 在 Hono 中使用
import { createMiddleware } from 'hono/factory'
const caslMiddleware = createMiddleware(async (c, next) => {
const user = c.get('user')
const ability = defineAbilitiesFor(user)
c.set('ability', ability)
await next()
})
// 使用
app.use('/api/*', caslMiddleware)
app.get('/api/articles/:id', async (c) => {
const ability = c.get('ability')
const articleId = c.req.param('id')
const article = await db.query.articles.findFirst({
where: eq(articles.id, articleId),
})
if (!ability.can('read', article)) {
return c.json({ error: 'Forbidden' }, 403)
}
return c.json(article)
})
app.delete('/api/articles/:id', async (c) => {
const ability = c.get('ability')
const articleId = c.req.param('id')
const article = await db.query.articles.findFirst({
where: eq(articles.id, articleId),
})
if (!ability.can('delete', article)) {
return c.json({ error: 'Forbidden' }, 403)
}
await db.delete(articles).where(eq(articles.id, articleId))
return c.json({ message: 'Deleted' })
})3.3 数据库查询集成
CASL 可以和数据库查询集成:
import { toPrisma } from '@casl/ability/extra'
// 把 CASL 规则转换为 Prisma where 条件
const rules = ability.rulesFor('read', 'Article')
const where = toPrisma(rules)
// 查询用户能读的文章
const articles = await prisma.article.findMany({
where,
})4. Open Policy Agent
OPA 是通用的策略引擎,用 Rego 语言表达策略。
4.1 Rego 策略
package authz
default allow = false
# 管理员可以访问所有
allow {
input.subject.role == "admin"
}
# 用户可以读自己的资源
allow {
input.action == "read"
input.resource.ownerId == input.subject.id
}
# 同部门可以读写
allow {
input.action == "read"
input.subject.department == input.resource.department
}
# 工作时间检查
allow {
time := time.clock(time.now())
time[0] >= 9
time[0] <= 18
}4.2 在 Hono 中调用 OPA
const opaMiddleware = createMiddleware(async (c, next) => {
const user = c.get('user')
const action = c.req.method.toLowerCase()
const resource = await getResource(c)
// 调用 OPA
const response = await fetch('http://opa:8181/v1/data/authz/allow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
input: {
subject: user,
action,
resource,
},
}),
})
const { result } = await response.json()
if (!result) {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
})5. AI 项目的 ABAC
5.1 多租户隔离
function defineAbilitiesFor(user: User, tenant: Tenant) {
const { can, build } = new AbilityBuilder(createMongoAbility)
// 只能访问自己租户的数据
can('read', 'Conversation', { tenantId: tenant.id })
can('create', 'Conversation', { tenantId: tenant.id })
// 租户管理员可以管理租户内所有数据
if (user.tenantRole === 'admin') {
can('manage', 'Conversation', { tenantId: tenant.id })
}
return build()
}5.2 配额控制
function defineAbilitiesFor(user: User) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility)
// 检查配额
const usage = await getUsage(user.id)
const quota = await getQuota(user.id)
can('create', 'Message')
// 超过配额不能创建
if (usage.messageCount >= quota.maxMessages) {
cannot('create', 'Message')
}
// 只能用授权的模型
if (user.plan === 'free') {
can('create', 'Message', { model: 'gpt-3.5-turbo' })
cannot('create', 'Message', { model: 'gpt-4' })
}
return build()
}5.3 资源级权限
function defineAbilitiesFor(user: User) {
const { can, build } = new AbilityBuilder(createMongoAbility)
// 用户可以读自己的对话
can('read', 'Conversation', { userId: user.id })
// 用户可以读共享给自己的对话
can('read', 'Conversation', {
id: {
$in: await getSharedConversationIds(user.id),
},
})
// 团队成员可以读团队的对话
if (user.teamId) {
can('read', 'Conversation', { teamId: user.teamId })
}
return build()
}6. ABAC 最佳实践
6.1 策略集中管理
// abilities/article.ts
export function defineArticleAbilities(user: User) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility)
// 所有登录用户可以读文章
can('read', 'Article', { status: 'published' })
// 作者可以管理自己的文章
can('update', 'Article', { authorId: user.id })
can('delete', 'Article', { authorId: user.id })
// 编辑可以管理所有文章
if (user.role === 'editor') {
can('manage', 'Article')
}
// 所有人都不能删除已发布的文章
cannot('delete', 'Article', { status: 'published' })
return build()
}6.2 性能优化
ABAC 需要在运行时评估策略,可能影响性能:
// ❌ 每次请求都查询数据库
function defineAbilitiesFor(user: User) {
const sharedConversations = await db.query.sharedConversations.findMany({
where: eq(sharedConversations.userId, user.id),
})
// ...
}
// ✅ 缓存用户属性
async function getUserAttributes(userId: string) {
const cached = await redis.get(`user:${userId}:attributes`)
if (cached) return JSON.parse(cached)
const attributes = await loadUserAttributes(userId)
await redis.setex(
`user:${userId}:attributes`,
300, // 5 分钟
JSON.stringify(attributes)
)
return attributes
}6.3 可解释性
ABAC 策略应该可以解释为什么拒绝:
function evaluateWithReason(
subject: Subject,
action: Action,
resource: Resource
): { allowed: boolean; reason?: string } {
const policies = [
{
name: 'admin-full-access',
condition: (s) => s.role === 'admin',
effect: 'allow',
},
{
name: 'owner-read',
condition: (s, a, r) => a === 'read' && r.ownerId === s.id,
effect: 'allow',
},
// ...
]
for (const policy of policies) {
if (policy.condition(subject, action, resource)) {
return {
allowed: policy.effect === 'allow',
reason: policy.effect === 'allow' ? undefined : `Denied by policy: ${policy.name}`,
}
}
}
return {
allowed: false,
reason: 'No matching policy found',
}
}总结
ABAC 基于属性做权限判断,比 RBAC 更灵活,适合复杂场景。
这一节涉及到的几个实践:
- ABAC vs RBAC:ABAC 基于属性,RBAC 基于角色
- 策略定义:代码、DSL、JSON
- CASL:JavaScript 的 ABAC 库
- OPA:通用策略引擎,Rego 语言
- AI 项目:多租户、配额控制、资源级权限
- 最佳实践:策略集中管理、性能优化、可解释性
ABAC 适合需要细粒度权限的场景。多租户、复杂配额、资源级权限都需要 ABAC。CASL 是 JavaScript 生态的好选择,OPA 适合需要跨语言统一策略的场景。
下一篇看多租户权限设计——租户隔离、数据隔离、权限继承。