Mastra Agent Builder 权限与 RBAC 冒烟测试指南:角色门控、权限矩阵与归属校验的端到端验证
【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: 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-agents、stored-skills、stored-workspaces等由用户创建并拥有(owner)的存储型实体。当接入 WorkOS 认证后,这些实体带上了authorId与visibility元数据,所有读写、执行、发布、删除操作都必须经过角色与归属双重校验。为了在不依赖完整示例工程的前提下覆盖这条 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、*:execute、stored-agents:write、stored-skills:write、stored-workspaces:write |
viewer | *:read |
模板中MastraRBACWorkos的构造参数值得注意两点:
cache: { ttlMs: 1 }—— 注释明确说明这是刻意为之:1ms 的 TTL 等效于禁用缓存,每次请求都会从 WorkOS 重新拉取角色与权限,保证测试过程中对roleMapping的改动和对上游角色的调整能立即生效,这正是冒烟测试断言"实时 RBAC 行为"的前提。_default: []—— 任何未在映射中列出的 WorkOS 角色默认不获得任何权限。
此外还有三条重要的语义说明:
- 公共存储实体的短路读取:公开的 stored skills/agents 会短路读检查(见 authorship.ts 中
assertReadAccess对visibility === '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 行为,只有两个途径:
- 把 WorkOS 角色改为
viewer,重启mastra dev,再用--role viewer重跑; - 保持
--role admin运行,走 UI 专属的角色模拟流程(详见 ui.md 第 8 步)。
注意:角色模拟是纯 UI 行为。在模拟 viewer 的同时用 curl 请求同一端点,得到的仍是 admin 的响应——这是预期结果,应在报告中如实记录,而不是当作 bug 上报。
角色预期矩阵(Role expectation matrix)
以下矩阵按角色给出代表性端点的通过标准,Agent 在--role为非 admin 时会据此为每个小节设置预期状态码:
| 端点 / 动作 | owner | admin | member | viewer |
|---|---|---|---|---|
GET /stored/agents | 200 | 200 | 200 | 200 |
GET /stored/skills | 200 | 200 | 200 | 200 |
POST /stored/agents(创建) | 200 | 200 | 200 | 403 |
POST /stored/skills(创建) | 200 | 200 | 200 | 403 |
PATCH /stored/agents/:id(自己的) | 200 | 200 | 200 | 403 |
PATCH /stored/agents/:id(他人的) | 200 | 200 | 404 | 404 |
DELETE /stored/agents/:id(自己的) | 200 | 200 | 403 | 403 |
PATCH /stored/skills/:idvisibility | 200 | 200 | 200 | 403 |
POST /stored/skills/:id/publish | 200 | 200 | 403 | 403 |
POST /agents/:id/chat(执行) | 200 | 200 | 200 | 403 |
GET /editor/builder/infrastructure | 200 | 200 | 200 | 200 |
PUT /stored/agents/:id/favorite | 200 | 200 | 200 | 200 |
矩阵背后的两个设计要点值得展开:
- member 之所以能对自己的记录创建/PATCH,是因为脚手架授予了
stored-{agents,skills,workspaces}:write;而 publish/delete/share 仍然仅限 admin(即*)。 - member PATCH 他人记录返回
404 Not Found而非 403:可见性/归属过滤器在 handler 执行前就隐藏了该行,非属主无法区分"记录不存在"与"被禁止访问"。这是 REST 中标准的"不暴露存在性"(don't reveal existence)模式,其实现依据可在 authorship.ts 的resolveAuthorFilter中看到:非属主查询他人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 等价)。只有viewer和member在 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):
- 记录没有属主(遗留/无主记录);
- 调用者是该记录的
authorId; - 调用者持有该资源的 admin 旁路权限(如不带记录过滤的
stored-skills:write); - 调用者的角色授予中显式持有
<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)均可见
- 新建记录的
authorId为null
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.ts 中
MastraRBACWorkos(来自@mastra/auth-workos)的roleMapping定义了本文全部角色矩阵的权限基础;AUTH_PROVIDER=workos时同时构造MastraAuthWorkos,未配置时两个 provider 均为undefined,Mastra 构造器收到server.auth: undefined,编辑器权限检查因此短路为"无调用者 authorId"。 - 归属与访问断言辅助函数:authorship.ts 集中实现了
getCallerAuthorId、getCallerPermissions、hasAdminBypass、hasScopedPermission、resolveAuthorFilter、matchesAuthorFilter,以及assertReadAccess/assertExecuteAccess/assertWriteAccess/assertShareAccess四类断言。读/执行/写/分享四种动作的放行条件各不相同:例如visibility: 'public'足以放行读与执行,但不能放行写(编辑/删除)与分享(切换可见性),这从机制上防止了"任何能读到的人都能把私有记录公开"的越权。 - 路由级门控元数据:
requiresPermission声明在 packages/core/src/server/types.ts,是路由层 RBAC 的入口;而:share/:publish刻意不走该字段,而是在 handler 内部用上述断言函数做细粒度控制。 - 测试用例印证:归属/可见性相关行为在 authorship.test.ts 中有对应测试,可作为矩阵预期之外的补充验证手段。
- 认证模式切换与预检:
AUTH_PROVIDER与WORKOS_*变量的开关细节、会话 Cookie 提取、401 错误形状检查,见 auth.md。
需要注意的是,本指南的矩阵与命令均面向.claude/skills/builder-smoke-test所生成的脚手架工程,其角色映射是刻意收窄的测试配置(admin 映射为*、member 仅授予有限写权限);其他工程若自定义了roleMapping,应以各自auth.ts中的实际授予为准,先核对映射再调整预期。
【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考