核心问题

一个主体应该拥有多少能力?

最小权限原则:

只授予完成任务所需的最小权限。

在软件工程里,这不仅是安全原则,也是伦理原则。

因为权限不是普通配置。权限是在分配权力:

  • 谁能看见什么数据?
  • 谁能修改什么资源?
  • 谁能删除什么内容?
  • 谁能代表别人行动?
  • 谁能绕过规则?
  • 谁能影响其他用户?

一个系统如果随便授予权限,本质上是在随便分配权力。

默认拒绝,而不是默认允许

最小权限的第一条是:

默认拒绝,明确允许。

坏模型:

function canEditCourse(accountId: string, courseId: string) {
  if (isBlocked(accountId)) return false
  return true
}

这意味着除了被明确阻止的人,其他人都能编辑课程。

好方向:

function canEditCourse(accountId: string, courseId: string) {
  return (
    isCourseOwner(accountId, courseId) ||
    isAssignedCourseEditor(accountId, courseId) ||
    isPlatformAdmin(accountId)
  )
}

这里系统要求明确理由。

核心句:

权限判断应该寻找允许的理由,而不是寻找拒绝的理由。

Admin 不是万能神

很多系统有一个危险捷径:

if (user.role === "admin") return true

这很方便,但风险极高。

因为 admin 权限常常会扩张成:

  • 看所有用户数据
  • 改所有课程
  • 删除所有订单
  • 退款
  • 修改权限
  • 导出数据
  • 冒充用户
  • 关闭风控

这些能力风险完全不同,不应该被一个 admin 吞掉。

更好的做法是拆分能力:

canViewUserProfile(adminId, accountId)
canRefundPayment(adminId, paymentId)
canEditCourse(adminId, courseId)
canExportUserData(adminId)
canImpersonateUser(adminId, accountId)

并且为高风险能力加额外约束:

  • 二次确认
  • 审计日志
  • 双人审批
  • 时间限制
  • 只读模式
  • 数据脱敏

原则:

管理员不是一种身份,而是一组高风险能力的集合。

权限必须有上下文

不要写:

user.role = "manager"

因为 manager 必须回答:

管理什么?

更清楚:

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

或者:

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

权限没有上下文,就会变成全局权力。

核心句:

权限必须绑定资源范围。

RBAC 和 ABAC

常见权限模型有两类。

RBAC:Role-Based Access Control

基于角色授权。

例如:

organization owner 可以邀请成员
course editor 可以编辑课程
platform support 可以查看工单

优点:

  • 简单
  • 好理解
  • 适合组织权限

缺点:

  • 角色容易膨胀
  • 特殊情况多时会变复杂

ABAC:Attribute-Based Access Control

基于属性授权。

例如:

account.organizationId == course.organizationId
account.department == "training"
course.status == "draft"
request.ip in trusted_range

优点:

  • 灵活
  • 适合复杂条件

缺点:

  • 难理解
  • 难审计
  • 容易出现隐式规则

多数业务系统会混合使用:

function canAssignCourse(accountId, organizationId, courseId) {
  return (
    hasOrganizationRole(accountId, organizationId, "manager") &&
    organizationHasCourseAccess(organizationId, courseId)
  )
}

这里既有角色,也有属性条件。

重点不是选择名词,而是:

权限规则要可读、可测试、可审计。

权限不是前端按钮

隐藏按钮不是权限控制。

坏做法:

if (user.isAdmin) {
  showDeleteButton()
}

然后后端接口没有检查。

真正权限必须在后端执行:

async function deleteCourse(accountId: AccountId, courseId: CourseId) {
  if (!(await canDeleteCourse(accountId, courseId))) {
    throw new ForbiddenError("COURSE_DELETE_FORBIDDEN")
  }

  await courses.delete(courseId)
}

前端权限只改善体验,不能提供安全。

核心句:

UI 可以提示权限,后端必须执行权限。

权限和审计绑定

高风险权限必须留下审计。

例如:

  • 删除课程
  • 手动退款
  • 导出用户数据
  • 修改用户权限
  • 冒充用户
  • 解封账号

审计日志应该记录:

type AuditLog = {
  actorId: string
  action: string
  resourceType: string
  resourceId: string
  reason?: string
  occurredAt: Date
  requestId: string
}

尤其是后台操作,要知道:

  • 谁做的?
  • 对什么资源做的?
  • 为什么做?
  • 什么时候做?
  • 从哪里发起?

原则:

权力越大,证据越多。

Service Account 也要最小权限

最小权限不只针对人,也针对服务。

坏做法:

所有服务共用一个 database admin credential

风险是一个服务被攻破,所有数据都暴露。

更好的做法:

  • 读服务只给读权限
  • worker 只给需要的表权限
  • analytics 只能读脱敏数据
  • billing service 只能操作支付相关表
  • CI token 只给部署需要的权限

Service account 应该回答:

这个服务为了完成职责,最少需要哪些能力?

临时权限必须自动过期

临时权限如果不会过期,就不是临时权限。

例如:

给某工程师临时 production database access 排查事故。

应该有:

  • 申请理由
  • 审批人
  • 过期时间
  • 操作审计
  • 自动回收

否则临时权限会变成永久后门。

核心句:

临时权限必须带过期时间。

最小权限的坏味道

  1. 到处都是 if admin return true
  2. 一个角色拥有大量无关能力。
  3. 权限只在前端判断。
  4. 后台操作没有审计日志。
  5. service account 使用超级权限。
  6. 临时权限不会过期。
  7. 权限错误只能靠客服或用户发现。
  8. 不知道谁能导出用户数据。
  9. 权限规则散落在各个 handler 里。
  10. 没有测试覆盖拒绝路径。

最小权限检查清单

设计权限时问:

  1. 这个主体是谁?人、服务、组织还是外部应用?
  2. 它要完成什么任务?
  3. 完成任务所需的最小能力是什么?
  4. 权限绑定在哪个资源范围?
  5. 默认是否拒绝?
  6. 拒绝时是否有稳定 reason?
  7. 高风险操作是否有审计?
  8. 是否需要二次确认或审批?
  9. 临时权限是否自动过期?
  10. 是否有测试覆盖允许和拒绝路径?

小结

  1. 最小权限是安全原则,也是伦理原则。
  2. 权限是在分配权力。
  3. 默认拒绝,明确允许。
  4. 管理员不是万能神,而是一组高风险能力。
  5. 权限必须绑定上下文和资源范围。
  6. UI 可以提示权限,后端必须执行权限。
  7. 权力越大,证据越多。
  8. 人和服务都应该遵守最小权限。
  9. 临时权限必须自动过期。