ACL 模型详细设计:资源授权、权限继承与共享控制

RBAC 可以表达“项目管理员拥有文档分享权限”,却不擅长回答“Alice 能否下载 D1001”。当权限需要精确到某个文件、项目、看板或报表时,ACL 更自然。

ACL 的基本思路是:在资源上保存一组访问控制条目 ACE,每条条目说明哪个主体对该资源执行什么操作时允许或拒绝。

ACL 基本模型

1. ACL 与 ACE

一条 ACE 至少包含:

subject_type + subject_id
resource_id
action
effect
valid_from / valid_until
source_type / source_id
granted_by

示例:

Subject: group:PROJECT_A_MEMBERS
Resource: document:D1001
Action: read
Effect: ALLOW
Source: PROJECT_MEMBERSHIP

ACL 是资源上的 ACE 集合。Owner 表示资源所有者,但所有者拥有哪些能力仍由资源类型策略定义,不能把 Owner 当作无限管理员。

2. 主体和操作粒度

ACL 主体可以是用户、用户组、组织、角色、外部用户或服务账号。项目文档通常以用户组作为主要授权对象,减少逐人配置;对少数例外再使用用户直接 ACE。

操作应按风险拆分:

read、update、delete、download、share、manage-permission

“能读”不等于“能下载”。在线阅读可以执行动态脱敏和水印,下载后的文件脱离在线控制,因此应独立授权和审计。

3. Allow、Deny 和冲突

专题默认在同一 ACL 继承边界内采用 Deny 优先:

Project-A 项目组 ALLOW read
Alice DENY read
→ Alice 最终 DENY

Deny 适合明确例外,例如外部成员禁止下载机密文件。它不应被滥用为另一套复杂角色系统,否则管理员很难解释多个组、多个父目录和多个直接条目的冲突。

系统级强制 Deny 不属于普通 ACL:租户不匹配、主体已禁用、合规规则禁止外部访问时,即使资源 ACE 为 Allow 也不能放行。

4. 权限继承

资源通常形成树:项目目录、子目录、文档。默认继承方向是父资源到子资源,不能由子文档反推对父目录的全部权限。

ACL 继承与停止继承

INHERIT

子资源持续继承父级 ACL。父目录新增或撤销成员后,子资源自动变化,适合绝大多数项目文档。

BREAK_COPY

停止继承时,把当前有效继承条目复制为本地条目。以后父级变化不再影响子资源,适合从某一时刻开始独立管理的文档。

BREAK_EMPTY

停止继承并清空继承条目,只保留重新配置的本地授权。适合高度保密的子目录,但风险很高,应要求单独权限、二次确认和审计。

停止继承只影响普通 ACL,不能移除租户隔离、资源分类、外部用户限制和强制 MFA 等系统策略。

5. 恢复继承和资源移动

恢复继承时需要明确如何处理本地 ACE:

  • 保留本地 ACE,再叠加父级;
  • 清空本地 ACE,完全跟随父级;
  • 先显示影响预览,由管理员选择。

资源移动到新目录时,默认保留本地 ACE,并把继承来源切换到新父目录。移动前必须计算权限差异,尤其检查外部成员是否因为新目录的宽松 ACL 获得访问能力。跨租户移动不能被普通资源移动实现,应走数据迁移流程。

6. 资源所有权

文档所有者通常可以编辑、分享和管理协作者,但不一定可以:

  • 修改机密级别;
  • 删除审计记录;
  • 绕过保留策略;
  • 把资源分享给任意外部租户;
  • 跳过管理员设置的下载禁令。

资源类型应该定义所有权默认能力。例如付款单创建人可以修改草稿、提交和撤回,却不能审批本人创建的付款单。

7. 分享模型

文档分享流程

指定用户或用户组分享通常生成 ACE;匿名或外部链接分享则需要独立 Share Link,至少包含:

resource_id
token_hash
allowed_actions
expires_at
password_hash
max_access_count
watermark_policy
status
created_by

访问链接时仍要校验资源状态、租户规则、有效期、访问次数和动态策略。不能把随机 URL 当作唯一安全机制。撤销分享后,相关缓存和 CDN 私有访问凭证也要同步失效。

8. 数据库设计

CREATE TABLE auth_resource (
    id                 BIGINT PRIMARY KEY,
    tenant_id          BIGINT NOT NULL,
    resource_type      VARCHAR(64) NOT NULL,
    resource_key       VARCHAR(128) NOT NULL,
    parent_resource_id BIGINT NULL,
    owner_principal_id BIGINT NULL,
    inheritance_mode   VARCHAR(32) NOT NULL DEFAULT 'INHERIT',
    status             VARCHAR(32) NOT NULL,
    version            BIGINT NOT NULL DEFAULT 1,
    UNIQUE KEY uk_resource (tenant_id, resource_type, resource_key),
    KEY idx_parent (tenant_id, parent_resource_id)
);

CREATE TABLE auth_acl_entry (
    id              BIGINT PRIMARY KEY,
    tenant_id       BIGINT NOT NULL,
    resource_id     BIGINT NOT NULL,
    subject_type    VARCHAR(32) NOT NULL,
    subject_id      BIGINT NOT NULL,
    action_code     VARCHAR(64) NOT NULL,
    effect          VARCHAR(16) NOT NULL,
    source_type     VARCHAR(32) NOT NULL,
    source_id       BIGINT NULL,
    granted_by      BIGINT NULL,
    valid_from      DATETIME NULL,
    valid_until     DATETIME NULL,
    status          VARCHAR(32) NOT NULL,
    version         BIGINT NOT NULL DEFAULT 1,
    KEY idx_resource_action (tenant_id, resource_id, action_code, status),
    KEY idx_subject (tenant_id, subject_type, subject_id, status)
);

不要把角色分配、组织成员、ACL、资源关系全部塞进一张万能 relation 表。它们生命周期、索引方向和约束规则不同。

9. 权限计算

单资源检查可以按以下步骤:

  1. 校验租户和资源状态。
  2. 展开 Subject 的直接身份、用户组、组织和有效角色。
  3. 从当前资源向父级查找可继承 ACL。
  4. 合并当前资源本地 ACE。
  5. 过滤过期、撤销和不适用 Action。
  6. 应用 Deny/Allow 规则。
  7. 执行资源分类与 ABAC 强制策略。
  8. 返回决策和来源链。

伪代码:

AclDecision checkAcl(AclRequest request) {
    Resource resource = resourceRepo.requireInTenant(
            request.tenantId(), request.resourceId());

    Set<SubjectRef> subjects = subjectResolver.expand(request.subjectId());
    List<AclEntry> entries = aclRepo.findEffectiveEntries(
            resource, subjects, request.action(), request.at());

    if (entries.stream().anyMatch(AclEntry::isDeny)) {
        return AclDecision.deny("DENY_ACL", entries);
    }
    if (entries.stream().anyMatch(AclEntry::isAllow)) {
        return AclDecision.allow(entries);
    }
    return AclDecision.deny("DENY_NO_ACL");
}

真实系统还需要把强制策略放在普通 ACL 之前或之后的固定层级中,避免业务 ACE 覆盖合规限制。

10. 两种查询方向

ACL 查询方向

“这个资源授权给了谁”容易:按 resource_id 查询 ACL 即可。

“这个用户能访问哪些资源”困难得多,需要展开用户组、组织和角色,再反查直接 ACE、计算父级继承、应用 Deny 和有效期,并且还要支持分页和排序。常见优化方式包括:

  • 对直接 ACL 建主体反向索引;
  • 对资源树建立闭包表或物化路径;
  • 对高频查询维护可访问资源倒排索引;
  • 使用搜索引擎过滤授权后的候选资源;
  • 对用户组变更做选择性缓存失效;
  • 对 List Objects 使用专门算法,而不是循环调用单资源 Check。

大规模 ReBAC 系统通常把关系元组和权限模型分开管理,提供 CheckListObjectsListUsers 等不同接口。

11. 缓存与失效

可缓存:

  • 用户有效用户组;
  • 资源父链;
  • 资源局部 ACL;
  • 经版本标记的最终决策。

必须触发失效的事件:ACL 修改、资源移动、停止继承、用户退出组、分享撤销、资源敏感级别变化。最终决策缓存键至少包含 tenant、subject、resource、action、ACL version 和 policy version。

12. 常见错误

  • 只保护文档详情,不保护下载、历史版本和附件接口。
  • 链接足够随机就不检查权限。
  • 用户退出项目组后,预计算索引不更新。
  • 子资源的 Allow 静默覆盖父级强制 Deny。
  • 移动文档后没有重新计算继承 ACL。
  • 删除资源但外链或对象存储签名 URL 仍可访问。
  • 把 Owner 当成绕过所有策略的超级管理员。

13. 适用边界

ACL 适合资源实例分享、项目协作和文件树权限,不适合独立承担大规模功能权限,也不擅长复杂金额、设备和风险判断。通常应与 RBAC、ABAC 和 ReBAC 组合使用。

参考资料