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)  // false

3.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 更灵活,适合复杂场景。

这一节涉及到的几个实践:

  1. ABAC vs RBAC:ABAC 基于属性,RBAC 基于角色
  2. 策略定义:代码、DSL、JSON
  3. CASL:JavaScript 的 ABAC 库
  4. OPA:通用策略引擎,Rego 语言
  5. AI 项目:多租户、配额控制、资源级权限
  6. 最佳实践:策略集中管理、性能优化、可解释性

ABAC 适合需要细粒度权限的场景。多租户、复杂配额、资源级权限都需要 ABAC。CASL 是 JavaScript 生态的好选择,OPA 适合需要跨语言统一策略的场景。

下一篇看多租户权限设计——租户隔离、数据隔离、权限继承。