Mastra Agent Builder 权限与 RBAC 冒烟测试指南:角色门控、权限矩阵与归属校验的端到端验证

Mastra Agent Builder 权限与 RBAC 冒烟测试指南:角色门控、权限矩阵与归属校验的端到端验证

【免费下载链接】mastra Mastra is the modern TypeScript framework for AI-powered applications and agents. 【免费下载链接】mastra 项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

本指南围绕 Mastra 仓库中 Agent Builder 功能分支的冒烟测试(builder-smoke-test)展开,聚焦 Studio 与 Agent Builder 场景下基于角色的访问控制(RBAC)验证。文章以 .claude/skills/builder-smoke-test/references/permissions.md 为骨架,结合脚手架工程中的角色映射实现与 packages/server 中的归属校验源码,完整讲解 owner / admin / member / viewer 四类角色的路由级权限门控(#16271)、组件级门控、UI 端角色模拟(#15864)以及关闭认证后的旁路行为(#16107)。读完本文,你将掌握如何在 --auth on--auth off 两种模式下,用 curl 对存储型 Agent/Skill 的读写、执行、发布、删除与可见性切换进行逐角色的预期状态码断言,并能从源码层面理解"404 隐藏存在性""admin 等价 owner""member 仅能改自己的记录"等设计决策。

背景:为什么需要一套独立的 RBAC 冒烟测试

Mastra 的 Agent Builder 涉及 stored-agentsstored-skillsstored-workspaces 等由用户创建并拥有(owner)的存储型实体。当接入 WorkOS 认证后,这些实体带上了 authorIdvisibility 元数据,所有读写、执行、发布、删除操作都必须经过角色与归属双重校验。为了在不依赖完整示例工程的前提下覆盖这条 EE 面,仓库在 .claude/skills/builder-smoke-test/SKILL.md 中定义了一个"构建器冒烟测试"技能:它通过 scripts/scaffold.sh 生成一个封闭(hermetic)的测试工程,并用 pnpm link: 将当前工作树的 packages/stores/auth/ 等目录链接进去,从而在每次 mastra dev 重启后立即生效。权限/RBAC 是其中的第 11 个必测小节,可通过 --test permissions 单独运行,也可通过 --scope rbac(包含 permissions 与 auth 两节)执行。

该小节要验证的核心内容(来自 permissions.md 开头)包括:

  • 路由级 RBAC(requiresPermission 门控);
  • 组件级门控(#16271,UI 侧按角色裁剪侧边栏与按钮);
  • 仅存在于 UI 层的角色模拟(#15864,无服务端角色覆盖头);
  • 关闭认证后的全量旁路(#16107)。

默认角色与权限授予(Default roles)

脚手架工程并不依赖 core 包中的 DEFAULT_ROLES,而是在自身项目的 src/mastra/auth.ts 中配置了 WorkOS 的 roleMapping(对应仓库模板文件 auth.ts)。这份映射把四个角色的权限授予定义如下:

角色权限授予
owner*(一切权限,包括删除)
admin*(通过 WorkOS 映射与 owner 等价,见下方说明)
member*:read*:executestored-agents:writestored-skills:writestored-workspaces:write
viewer*:read

模板中 MastraRBACWorkos 的构造参数值得注意两点:

  1. cache: { ttlMs: 1 } —— 注释明确说明这是刻意为之:1ms 的 TTL 等效于禁用缓存,每次请求都会从 WorkOS 重新拉取角色与权限,保证测试过程中对 roleMapping 的改动和对上游角色的调整能立即生效,这正是冒烟测试断言"实时 RBAC 行为"的前提。
  2. _default: [] —— 任何未在映射中列出的 WorkOS 角色默认不获得任何权限。

此外还有三条重要的语义说明:

  • 公共存储实体的短路读取:公开的 stored skills/agents 会短路读检查(见 authorship.tsassertReadAccessvisibility === 'public' 的提前返回)。
  • 关闭认证直接旁路AUTH_PROVIDER 未配置时,角色检查被整体跳过。
  • member 的写权限是窄的:member 可以创建/PATCH 自己名下的 stored agents/skills/workspaces(因此 Library Copy、Stars、编辑等流程可以在非管理员角色下演练),但不能 :publish:delete:share。归属规则仍然生效:member PATCH 别人的记录会得到 403。这份映射只存在于脚手架工程的 auth.ts,core 中的 DEFAULT_ROLES 保持不变。
  • WorkOS admin → owner 等价:脚手架把 admin 映射为 ['*'],所以 WorkOS 预置的 admin 在本工程中携带 permissions: ["*"],能够通过 DELETE 检查。在矩阵判断中,admin 应被当作 owner 对待。

如何选定要测试的角色

--auth on 模式下,冒烟测试以当前登录的 WorkOS 用户实际拥有的角色运行。--role 标志(默认 admin)只是 Agent 的预期值;setup 阶段会断言它与 /api/auth/me 返回的 roles 字段一致,不一致则终止运行。

本构建中没有服务端"按角色预览"请求头。 UI 中的 "View as role"(角色模拟)功能纯粹是前端状态(详见 ui.md 的 Impersonation UI 小节),它不会改变 API 的返回结果。要在 API 层验证角色门控,登录用户必须真实持有该角色。

如果当前以 admin 登录但想测试 viewer 行为,只有两个途径:

  1. 把 WorkOS 角色改为 viewer,重启 mastra dev,再用 --role viewer 重跑;
  2. 保持 --role admin 运行,走 UI 专属的角色模拟流程(详见 ui.md 第 8 步)。

注意:角色模拟是纯 UI 行为。在模拟 viewer 的同时用 curl 请求同一端点,得到的仍是 admin 的响应——这是预期结果,应在报告中如实记录,而不是当作 bug 上报。

角色预期矩阵(Role expectation matrix)

以下矩阵按角色给出代表性端点的通过标准,Agent 在 --role 为非 admin 时会据此为每个小节设置预期状态码:

端点 / 动作owneradminmemberviewer
GET /stored/agents200200200200
GET /stored/skills200200200200
POST /stored/agents(创建)200200200403
POST /stored/skills(创建)200200200403
PATCH /stored/agents/:id(自己的)200200200403
PATCH /stored/agents/:id(他人的)200200404404
DELETE /stored/agents/:id(自己的)200200403403
PATCH /stored/skills/:id visibility200200200403
POST /stored/skills/:id/publish200200403403
POST /agents/:id/chat(执行)200200200403
GET /editor/builder/infrastructure200200200200
PUT /stored/agents/:id/favorite200200200200

矩阵背后的两个设计要点值得展开:

  • member 之所以能对自己的记录创建/PATCH,是因为脚手架授予了 stored-{agents,skills,workspaces}:write;而 publish/delete/share 仍然仅限 admin(即 *)。
  • member PATCH 他人记录返回 404 Not Found 而非 403:可见性/归属过滤器在 handler 执行前就隐藏了该行,非属主无法区分"记录不存在"与"被禁止访问"。这是 REST 中标准的"不暴露存在性"(don't reveal existence)模式,其实现依据可在 authorship.tsresolveAuthorFilter 中看到:非属主查询他人 authorId 时解析为 ownedOrPublicOthers 过滤器,matchesAuthorFilter 只放行"既是该属主又是 public"的行。

逐步骤验证流程(Steps)

1. 确认当前登录角色

curl -s -H "$SESSION" "$BASE/auth/me" | jq '{roles, permissions}'
  •  roles 包含通过 --role 传入的值
  •  permissions 与"默认角色"表中该角色的授予一致

若不匹配,立即停止并参考 auth.md 第 1b 步处理。注意 $SESSION 取自 WorkOS 会话 Cookie;该 Cookie 是 httpOnly 的,无法用 document.cookie 在浏览器中读取,需要通过脚手架提供的调试路由 GET /smoke-test/cookie(由 .env 中的 SMOKE_TEST_COOKIE_LEAK=1 开启)获取,详见 auth.md 第 0 步。此外该路由在 mastra dev 启动时一次性从环境变量构建,若启动后才写入该标志需要重启服务才能生效。

2. 读取对每个角色都放行

curl -s -o /dev/null -w '%{http_code}\n' -H "$SESSION" "$BASE/stored/agents"
curl -s -o /dev/null -w '%{http_code}\n' -H "$SESSION" "$BASE/stored/skills"
  •  无论角色如何,两者都应返回 200
  •  响应体是 JSON,而不是 HTML 或堆栈跟踪
  •  属于其他用户的私有 agents/skills 不应出现在列表中(除非调用者是 admin/owner)

这一条对应 resolveAuthorFilter 的默认分支 ownedOrPublic:只返回"自己的行 + 无属主的遗留行 + 任意公开行"。

3. 写操作按角色门控

按当前 --role 对照矩阵中的 POST 行发起请求,预期状态码见上表:

curl -s -o /dev/null -w '%{http_code}\n' -H "$SESSION" \
  -X POST "$BASE/stored/agents" \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Role Gating Test", "instructions": "x", "model": { "provider": "openai", "name": "gpt-4o-mini" } }'
  •  状态码与 --role 在矩阵中的预期一致
  •  403 的响应体是带错误信息的 JSON(无堆栈跟踪、无 HTML)

4. 执行与写入随资源不同(member 场景)

--role member

  •  POST /stored/agents → 200(持有 stored-agents:write
  •  对现有公开 agent 执行 POST /agents/:id/chat → 200(持有 :execute
  •  对自己草稿执行 POST /stored/skills/:id/publish → 403(无 :publish
  •  PATCH 另一位作者的 stored/skills/:id → 403(归属检查,而非权限检查)

--role viewer

  •  POST /agents/:id/chat → 403(无 :execute
  •  POST /stored/agents → 403(无 :write

5. 删除仅限属主

脚手架将 admin 映射为 ['*'],因此本工程中的 admin 能通过 DELETE 检查(与 owner 等价)。只有 viewermember 在 DELETE 时看到 403。

  •  --role owner--role admin:删除自己的 DELETE /stored/agents/:id → 200
  •  --role member:同样的 DELETE → 403
  •  --role viewer:同样的 DELETE → 403

如果矩阵与线上响应不一致,那就是发现(finding),请记录下来;在核对脚手架 auth.ts 中的 roleMapping 之前,不要擅自"修正"矩阵。

6. 可见性切换与发布语义

stored-skills / stored-agents 上的 :share:publish 动作没有接入路由的 requiresPermission(该元数据字段定义在 packages/core/src/server/types.ts)。相反,handler 在 PATCH/POST 内部调用 assertShareAccess(ctx, record)(发布有对应的等价辅助函数)。该辅助函数在以下任一条件成立时放行(源码见 authorship.ts):

  1. 记录没有属主(遗留/无主记录);
  2. 调用者是该记录的 authorId
  3. 调用者持有该资源的 admin 旁路权限(如不带记录过滤的 stored-skills:write);
  4. 调用者的角色授予中显式持有 <resource>:share<resource>:publish

注意第 3 条依赖的 hasAdminBypass 会识别 *<resource>:*<resource>:admin 三种通配形态,且只有带资源 ID 段的细粒度授权(如 agents:read:agent-123)才会被当作逐记录覆盖,宽泛的角色级授权不会二次生效,以免破坏属主/可见性模型(见 authorship.ts)。

通过 API 验证:

  •  属主能切换自己 skill 的可见性:PATCH /stored/skills/:id 携带 {"visibility":"public"} 返回 200,且响应中的 visibility"public"
  •  admin 能切换非自己 skill 的可见性:对他人 skill 执行相同 PATCH 返回 200
  •  viewer / member 不能切换非自己 skill 的可见性:相同 PATCH 返回 403 且错误体为 JSON
  •  认证关闭模式会旁路这些检查(getCallerAuthorId(ctx) 返回 null 时 handler 短路);记录为 "auth-off bypass",而不是当作矩阵测试

7. 关闭认证的旁路(Auth-off bypass)

注释掉 .env 中的 AUTH_PROVIDER,重启服务,一切接口都应无需角色检查即可访问(#16107)。mastra dev 只在启动时读取一次 .env,因此任何改动都必须重启;建议先用 .claude/skills/builder-smoke-test/scripts/preflight.sh --expect off 确认模式(详见 auth.md)。

curl -s -o /dev/null -w '%{http_code}\n' "$BASE/stored/agents"
curl -s "$BASE/auth/me"
  •  /stored/agents 返回 200
  •  /auth/me 返回 200 且响应体为 null(不是 401,也不是用户对象)——路由把缺失的调用者解析为 null 而不是拒绝
  •  UI 无需登录即可加载
  •  所有操作入口(affordances)均可见
  •  新建记录的 authorIdnull

getCallerAuthorId 的实现印证了这一点:它优先读取 MASTRA_RESOURCE_ID_KEY(由 authConfig.mapUserToResourceId 写入),回退到认证用户对象上的 user.id,两者都取不到时返回 null(见 authorship.ts)。assertReadAccess 等辅助函数在 callerAuthorId 为空且请求上下文无用户时直接放行——因为一旦配置了认证,coreAuthMiddleware 会在 handler 之前用 401 拒绝未认证请求,所以"无用户"在这里只可能意味着"未配置认证"。

8. UI 门控(按角色的侧边栏 / 操作入口)

在浏览器中以 --role 用户登录时:

  •  与该角色权限匹配的侧边栏项可见,未授权的项被隐藏
  •  创建/编辑/删除按钮与该角色的权限一致
  •  直接导航到被门控的路由(如 viewer 访问 /agent-builder/agents/:id/edit)会重定向到只读视图或被拒绝

如果 --role admin(或 owner),还应执行 ui.md 中的 UI 角色模拟子集——这是不重新认证就能演练 viewer/member UI 门控的唯一可靠途径。该功能由 role-impersonation-context.tsx 实现,仅为前端状态:页面顶部出现角色预览横幅,退出入口标记为 Exit role preview,且仅 admin/owner 能看到模拟菜单。

汇总清单(Checklist)

  •  /auth/me 的 roles 与 --role 一致
  •  所有角色的读操作通过
  •  写操作按矩阵通过/失败
  •  执行操作按矩阵通过/失败(member ≠ viewer)
  •  删除仅限属主
  •  可见性切换受归属 / admin 旁路 / 显式 :share 门控
  •  关闭认证旁路所有角色检查
  •  UI 操作入口随角色收窄

源码级补充:权限校验在 Mastra 中的落点

最后把上述行为映射到仓库的实现位置,方便深入阅读:

  • RBAC 提供方与角色映射:脚手架工程的 auth.tsMastraRBACWorkos(来自 @mastra/auth-workos)的 roleMapping 定义了本文全部角色矩阵的权限基础;AUTH_PROVIDER=workos 时同时构造 MastraAuthWorkos,未配置时两个 provider 均为 undefined,Mastra 构造器收到 server.auth: undefined,编辑器权限检查因此短路为"无调用者 authorId"。
  • 归属与访问断言辅助函数authorship.ts 集中实现了 getCallerAuthorIdgetCallerPermissionshasAdminBypasshasScopedPermissionresolveAuthorFiltermatchesAuthorFilter,以及 assertReadAccess / assertExecuteAccess / assertWriteAccess / assertShareAccess 四类断言。读/执行/写/分享四种动作的放行条件各不相同:例如 visibility: 'public' 足以放行读与执行,但不能放行写(编辑/删除)与分享(切换可见性),这从机制上防止了"任何能读到的人都能把私有记录公开"的越权。
  • 路由级门控元数据requiresPermission 声明在 packages/core/src/server/types.ts,是路由层 RBAC 的入口;而 :share / :publish 刻意不走该字段,而是在 handler 内部用上述断言函数做细粒度控制。
  • 测试用例印证:归属/可见性相关行为在 authorship.test.ts 中有对应测试,可作为矩阵预期之外的补充验证手段。
  • 认证模式切换与预检AUTH_PROVIDERWORKOS_* 变量的开关细节、会话 Cookie 提取、401 错误形状检查,见 auth.md

需要注意的是,本指南的矩阵与命令均面向 .claude/skills/builder-smoke-test 所生成的脚手架工程,其角色映射是刻意收窄的测试配置(admin 映射为 *、member 仅授予有限写权限);其他工程若自定义了 roleMapping,应以各自 auth.ts 中的实际授予为准,先核对映射再调整预期。

【免费下载链接】mastra Mastra is the modern TypeScript framework for AI-powered applications and agents. 【免费下载链接】mastra 项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值