Repository navigation
Agent 需要的权限是动态的,传统权限模型在这里崩掉 #23
01men
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Agent 需要的权限是动态的,传统权限模型在这里崩掉
先说一个让人不舒服的结论:给 Agent 做权限,难点不是"给它什么权限",而是"这个权限什么时候失效"。
我们做过 RBAC,做过 ABAC,也在企业里见过各种权限系统。这套东西给人用是够的:一个人入职,给个角色,他就能访问对应的东西;他离职,把角色摘掉。人不会在下午三点突然决定去读写另一个部门的数据库。
Agent 会。
一、传统模型有三个隐含假设,Agent 全部违反
RBAC 在 1992 年定型,它成立的前提是三条隐含假设:
假设一:主体是稳定的。 一个账号对应一个人,人的职责半年不变一次。角色可以粗到"部门"这个级别,因为部门成员的工作内容相似。
假设二:会话是长时段的。 人早上登录,晚上退出,中间八小时权限不变。权限变更在"下一次登录"生效是可以接受的延迟。
假设三:一次授权对应一片资源。 给"财务角色"读财务报表的权限,这个权限覆盖的是财务报表这一类东西,不是某一笔凭证。
Agent 把三条全踩了。
它不稳定。 同一个 Agent,在早上八点做的是日报汇总,需要读三个业务库;十点被一个临时任务调用,需要写一张订单表;下午三点做的是售后工单分类,只需要读一张 MySQL 视图。它不是"一个角色",它是"一天里换了五个身份"。
它没有长时段的会话概念。 一次 Agent 调用可能持续 40 毫秒。如果权限在调用之间变化,最长 TTL 不能超过这次调用的寿命。我们最终的入场票据 TTL 默认是 180 秒,硬上限 600 秒,自助类票据上限 120 秒。这不是保守,是因为票据活得比调用久就是在制造攻击窗口。
它的授权最细的粒度是"这一次工具调用"。 不是"这一类资源"。Agent 调一个 MCP 工具,参数里带着具体要看哪一行数据、要写哪个字段。授权"允许访问订单表"和授权"允许把 order_id=8812 的收货地址改成苏州"是两个量级的事情。
结论很直接:如果把 Agent 硬塞进"用户-角色-权限点"这套模型,能跑通,但每个转弯处都会漏。
二、我们踩过的坑:把"未知"当成"可读"
这是最值得讲的一个 bug,因为它不是设计失误,是一个看起来非常合理的默认值。
早期我们的 MCP 网关从远端服务同步
tools/list,拿到工具清单。清单里每个工具可以申报自己的风险等级:read / write / admin。问题在于,很多第三方 MCP 服务根本不申报。当时的处理是:没申报的,一律当 read。
这个默认值在开发环境完全没问题——大部分工具确实是读。但它同时意味着:任何有写能力的工具,只要不申报,就能绕过审批直接进入调用面。 攻击者不需要攻破任何东西,只需要让同步进来的工具"忘记"申报。
现在我们的口径是反过来的:
/** 网关闸门 facts.risk 一律取本登记的生效值(effectiveRisk);无生效登记 → 'unknown'。 */未登记的工具风险是
unknown,不是read。unknown在不同语境下行为不同:只读语境放行,写语境返回RISK_UNREGISTERED拒绝。这个改动上线后,我们的登记队列里立刻出现两百多个
unknown工具,全部在写语境下被拒。业务侧第一反应是"你们把系统搞坏了"。但正确的那句话是:这些工具从来就没有被授权过,只是以前没人检查。写能力要恢复,必须走一条状态机:
有几条细节比状态机本身更重要:
审批人身份必须来自服务端解析的 caller,不接受请求参数指定。 否则就是"我可以替你审批"。
同一 (serviceId, toolName) 至多一条非 retired 记录。 effective 必须先退场才能重新登记,避免同一个工具挂着两条互相矛盾的生效结论。
rejected 不阻断新登记。 拒绝的结论锁的是"这一次申报",不是"这个工具永远不行"。
退场会立刻让闸门回到 unknown。 也就是说写语境下立刻恢复拒绝。登记既能生效,也能收回——这一条很少被设计,但它是"可撤销"的前提。
三、权限的正确形状:三个维度同时收紧
做完上面这些,我们把 Agent 的权限拆成了三个互相独立的维度。任何一个维度单独调都是不够的。
维度一:作用域绑定场景,不绑定人
这是我们和大多数实现分歧最大的地方。
常规做法是给角色加细粒度权限点。但我们发现,角色这个维度根本无法表达"同一批人在不同业务场景下该有不同的权限"——因为角色是人的属性,场景是任务的属性。
所以我们加了场景级 ABAC 层。策略条目长这样:
判定顺序是死的:
default,回落到角色 RBAC 和部门范围权限(存量行为不变)第 4 条是关键。一个场景一旦开始配置策略,它就进入了"默认拒绝"的世界。这很反直觉,配置策略本该是"多给一点权限",结果却是"整体收紧"。但这正是我们要的语义:你敢为一个场景立规矩,就得把该放行的都写清楚。
default这个返回值也是刻意的。它不是"允许"也不是"拒绝",它是"这一层没有意见"。调用方必须继续走 RBAC 链路,而不是看到default就当通过。另外,场景策略同时满足角色 RBAC 才算通过。场景判定是收敛,不是替代。
维度二:有效期短于调用,且一次性
我们对票据做了三层限制:
Math.min(600, Math.max(30, ...)))有一处设计取舍值得说:票据熵是 256 bit,我们没有做 IP 限流。原因是错误尝试没有爆破收益(256 bit 熵),而限流只会伤到共享出口 IP 的正常用户。这是一个"安全措施不做"的决定,但它有明确理由——不是忘了做。
对比一下人用的 token:access token 30 分钟,refresh token 7 天。差了 3 个数量级。这不是双重标准,是因为人的会话本来就长,而 Agent 的会话本来就短。TTL 应该匹配被授权行为的生命周期,而不是匹配"方便"。
维度三:组织归属 fail-closed,且支持兼任只读
这一条在真实企业里比前两条更容易出事。
我们的用户模型支持一人多部门:
orgRemoteId是主归属(优先取钉钉主部门),extraOrgRemoteIds是兼任挂靠。权限校验时,兼任部门只有只读权限。组织归属的校验也是 fail-closed 的:非平台管理员(无
*权限)只能管理本组织子树内的对象。越界直接拒,不是"过滤掉"。为什么强调这个?因为 SaaS 时代的多租户是靠"数据隔离"保证的——查询时带上 tenant_id。但企业内网的 Agent 平台不是多租户,它是单实例多组织,所有数据在同一个库里,隔离全靠每次调用的组织校验。一旦某个接口漏了这一步,就是跨部门数据泄露。
四、动态权限真正的难点:撤销
到这里,如果只理解成"权限要短、要细、要按场景",那是把一个困难问题简化成了配置问题。
真正的难点是撤销。
考虑这个场景:一个 Agent 正在执行一个长任务,中途它的权限被管理员收回了。任务怎么办?
第三种做法的代价是:你必须有能力在任何时刻回答"这个正在跑的任务,当前持有的权限集合是什么"。这要求权限不是启动时快照,而是每次调用实时解析。
我们的实现是每次校验都去查当前的登记值、当前的策略、当前的用户状态。代价是每个调用多几次本地查询(我们用的是进程内集合,不是网络往返)。收益是撤销是真的即时生效,不是"下次登录生效"。
顺带一个我们发现的问题:乐观锁不是可选的。 场景策略带
version字段,并发编辑用expectedVersion对账,冲突直接抛错。为什么?因为两个管理员同时改一个场景的策略,后写的覆盖前写的,而"被覆盖掉的那条 deny"恰恰是最危险的那类丢失——你不会收到任何告警,只是某个本该被拒的调用通过了。五、诚实地说,这套东西做不到什么
写这类文章最容易滑向"我们的方案完美解决了问题"。所以这一段必须写清楚。
我们没法阻止 Agent 在权限范围内做蠢事。 授权"允许写订单表",Agent 就有权把订单表写错。权限系统管的是"能不能",不是"该不该"。这个边界必须让业务方知道,否则会误以为上了管控台就安全了。
我们的场景策略表达力有限。 只支持角色/用户 × 动作 × allow/deny,没有条件表达式,没有时间窗口,没有数值约束。想做"金额 > 5 万才需要二次审批",现在得在应用层做。
两级审批允许同一人连审。 我们早期是强制"四眼原则",后来按业务要求取消了——因为小团队根本凑不出两个审批人,结果是大家共用一个管理员账号,四眼变成零眼。这是个妥协,不是成就。真实的风控应该由审计日志事后覆盖,而不是靠流程假装。
unknown会让存量系统大面积报错。 这是"未知即拒绝"的必然代价,我们接受它,但它确实意味着接入成本。这一点在文档里写得很清楚,也不打算美化。我们没有做权限的可视化追溯。 现在能查"某个权限点被哪些角色持有",但查不了"某个资源为什么对这个人可见"——后者需要反向推理链路,我们没做。
—— 以上这些边界,前三条已经写进仓库 README 的「运行边界」章节,后两条还只在这篇文章里。它们迟早都要进 README。
六、如果只记一句话
给 Agent 做权限,不要从"它需要什么"开始想,要从"它什么时候不该有"开始想。
传统权限模型的思路是累加:这个人需要 A、需要 B、需要 C,加上去。Agent 的思路必须是收敛:这个调用需要什么,只有这一件事,用完就没了。
一个 40 毫秒的调用,配一个 7 天的 token,中间的所有设计都是空的。
实现全在这,MIT,可以自己起一个看:
https://github.com/01men/ybkkasi
当前口径:30 个插件包 / 94 个权限点 / 98 个 MCP 工具 / 6 个内置角色。风险登记、场景策略、票据签发都在
packages/plugin-iam、packages/plugin-mcp、packages/plugin-authn里,代码不算多,读得完。All reactions