feat(authz): add built-in RBAC provider and provider factory (#4260)

* feat(authz): add built-in RBAC provider and provider factory (Phase 1A-2, #4063)

Phase 1A-2: RBAC provider + provider factory. No runtime behavior change.

New authz/rbac.py — RbacAuthorizationProvider:
- allow: '*' / True / list / [] / missing → deny-wins semantics
- deny always overrides all allow forms
- resource name explicit mapping (tool→tools, model→models, etc.)
- unknown/missing role raises ValueError (never silent allow)
- config compiled to immutable frozensets at construction
- filter_resources preserves order, no mutation, consistent with authorize
- sync == async decisions

New authz/runtime.py — resolve_authorization_provider:
- disabled → None (no import attempted)
- enabled + no provider → ValueError
- invalid class path / construction failure → ValueError with path
- isinstance Protocol check post-construction
- no caching, no fail_closed/default_role injection

48 tests (37 RBAC + 11 factory). No config schema change, no config_version bump.
Per RFC #4063 Phase 1A-2. Layer 1/Layer 2 wiring deferred to Phase 1B.

* fix(authz): reject unknown RBAC provider config

Fail fast on misspelled top-level RBAC settings, cover factory error propagation, and record Phase 1B policy and audit caveats.

* fix(authz): reject unreachable resource aliases

Fail fast when RBAC config uses reserved request-side aliases, preserve same-name and custom resources, and add regression coverage for every mapped alias.

* fix(authz): validate RBAC request identifiers
This commit is contained in:
hataa
2026-07-21 09:23:14 +08:00
committed by GitHub
parent 5eb59cb130
commit 92c8f2f03b
6 changed files with 983 additions and 0 deletions
@@ -200,6 +200,54 @@ Phase 1 最低验证要求:
- **延期:** RBAC provider、provider factory、Layer 1 过滤、Layer 2 自动接线移至
Phase 1A-2 / Phase 1B。
### 2026-07-17 — Phase 1A-2 / 内置 RBAC provider 与 provider factory
- **背景:** Phase 1A-1 建立了可信 Principal 链路,但没有策略引擎。
Phase 1A-2 实现内置 RBAC provider 和统一 provider factory。
- **决策:** `RbacAuthorizationProvider` 在构造时完成全部配置校验并编译为
不可变结构(`frozenset` / sentinel `_ALL`)。请求路径只做 O(1) membership 检查。
- **决策:** deny 永远优先于 allow,无论 allow 是 `"*"``True`、列表还是缺失。
- **决策:** 未知角色和缺失角色抛 `ValueError`(不返回 allow),由执行层
根据 `fail_closed` 决定。
- **决策:** 资源名使用显式映射(`tool → tools``model → models` 等),
不通过加 `s` 猜测。配置中的保留请求别名(如 `tool`)在构造期拒绝,并提示使用
对应配置键(如 `tools`),防止策略被存储在永远无法命中的键下。未知 resource
使用原名查找;未配置时视为"不受限"。
- **决策:** `resolve_authorization_provider()` 是唯一 provider 解析入口。
disabled 时返回 `None`(不 import provider 模块);enabled 但缺少 provider
时抛 `ValueError`。不缓存实例。不注入 `fail_closed``default_role`
- **决策:** 内置和自定义 provider 使用完全相同的 `resolve_variable` class-path
解析路径,无特殊分支。
- **证据:** 66 tests passed51 RBAC + 15 factory,其中 5 条为 malformed-policy
回归测试)。
- **兼容性:** 无运行时行为变化(`authorization.enabled: false`)。不修改
`config.example.yaml`,不 bump `config_version`
- **延期:** Layer 1 工具过滤、Layer 2 自动接线、DeerFlowClient、RBAC 配置示例
移至 Phase 1B。
- **Phase 1B 注意:** 已知角色缺少某个 resource policy 时语义是“不受限”,不是
fail-closed。配置示例必须明确提醒,并枚举部署方希望限制的每种 resource。
- **Phase 1B 注意:** 内置 RBAC 当前按 role + resource + target 决策,不区分
`AuthzRequest.action``policy_id` 也是稳定但粗粒度的
`rbac:allow` / `rbac:deny` / `rbac:unrestricted`。接入审计日志前应决定是否通过
更具体的 policy id 或 decision metadata 记录 role / resource / target。
### 2026-07-20 — Phase 1A-2 / PR #4260 请求边界收口
- **背景:** review 发现 `request.target` 未经运行时校验;通配符策略会允许
`None` 或空字符串,而列表策略会拒绝,形成依赖策略形态的不一致结果。进一步审查
发现无效 resource 和批量过滤候选项也存在相同的“不受限/通配符路径放行”风险。
- **决策:** 内置 RBAC 在请求边界要求 resource、resource type、target 和每个
candidate 都是非空字符串;`filter_resources()` 还要求 candidates 是 list。
非法输入统一抛 `ValueError`,不能进入 `rbac:unrestricted` 或通配符 allow 路径。
- **决策:** `filter_resources()` 对缺失/未知角色继续与 `authorize()` 一致地抛
`ValueError`,不在 provider 内静默返回空列表。Phase 1B 集成层负责按
`fail_closed` 处理 provider 异常,避免隐藏身份或部署配置错误。
- **证据:** 90 tests passed75 RBAC + 15 factory),覆盖 unrestricted、
wildcard、allow-list、同步/异步、非法 resource/target/candidates,以及
`filter_resources()` 的缺失/未知角色错误语义。
- **兼容性:** 只拒绝不符合 `AuthzRequest` / `filter_resources` 类型契约的运行时
输入;Phase 1A-2 仍未接入运行时,`authorization.enabled: false` 行为不变。
### 新记录模板
```markdown