Communitygithub.com

Boogy 授权(服务内鉴权)

为构建 Boogy 服务提供多层授权机制,涵盖设计、数据建模、认证、任务、MCP/REST 接口及平台限制。

Boogy 授权(服务内鉴权) 是什么?

此技能文档详细描述了 Boogy 平台的三层授权模型:入口准入、路由守卫和行级数据作用域。它强调不应手动实现任何授权逻辑,而应使用 SDK 提供的已验证辅助函数来保持安全不变性。文档深入介绍了如何在受限服务中开放公共路由(例如 `/webhook`)以及在开放服务中限制管理员子路由的最佳实践,并提供了路由、守卫和辅助函数的快速参考表。同时,它明确了“按存在掩码拒绝”的安全铁律(缺失和不属于你的资源统一返回 404,绝不返回 403),并提供了 API 密钥的集成方案和常见误区警示。该技能适用于设计 Boogy 服务的开发者,确保服务授权安全、一致且可维护。支持平台包括 Boogy 框架,集成点涉及入口配置、路由守卫和数据访问层。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add Boogy-ai/boogy-superpowers

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Boogy 授权(服务内鉴权) 是做什么的?

Boogy 的授权分为三层。切勿手动实现任何一层——SDK 会生成已验证的辅助函数,确保安全不变性不被破坏。

三层模型

  1. 入口准入——平台的入口模式决定请求是否能到达你的服务。当你的代码运行时,调用者已经是已知的 主体 或匿名者。
  2. 守卫门控路由——每个路由在处理器执行前进行检查:要求登录,或要求对目标资源拥有所有权。
  3. 辅助函数限定行范围——处理器仅列出/加载调用者自己的数据行。

使用 auth::current_principal() -> Option<String> 解析调用者。主体是不透明的——切勿解析、去除前缀或假定为 UUID;只能将其用作所有者列的值和 auth::* 函数的输入。

路由级入口:受限服务中的公开路由

第 1 层(入口)通常是一个服务级的 mode。但有时某个路由必须能被其他路由拒绝的调用者访问——典型情况是,在需要 authenticated(或 internal/mixed)的服务内部需要一个公开的 /webhook 接收者。使用路由级覆盖将其分离出来;服务的其余部分保持受限:

[ingress]
mode = "authenticated"          # 服务级默认值——其他所有路由

[[ingress.routes]]
path = "/webhook"               # 服务相对路径;所有 HTTP 方法
mode = "public"                 # 任何人都可以访问 /webhook

安全契约(请务必阅读——它是失效关闭的):

规则为何重要
服务相关的字面路径。 精确匹配(/webhook)或前缀匹配(/stripe/*,按段边界)。不支持 {param} 捕获。path 是你的路由器看到的路径,与处理器的帧相同。
最具体匹配胜出;始终只有一个模式生效。 精确匹配优先于前缀匹配;较长前缀优先于较短前缀。公开子路由永远无法扩大同级路由的范围:/webhook 公开不会放宽 /data。
默认值 = 服务级模式。 不匹配的路径(或 .. 遍历尝试)将回退到受限的默认值,绝不回退到权限最宽的路由。忘记列出某路由意味着它保持受保护状态,而非暴露。
每个覆盖路由携带自己的模式 + 允许列表。 覆盖路由不会继承 allowed_agents/allowed_origins。在路由块中列出它们;同样应用非空验证,因此拼写错误不会静默地拒绝所有请求。
按路径而非按方法(目前)。 path = "/webhook" 适用于 GET、POST 等所有方法。公开的 /webhook 可通过任何动词访问——因此仍需在处理器内验证请求(误发的 GET 应无操作)。
在边缘由主机强制执行。 入口在你的 wasm 实例化之前运行。你不必在代码中自行门控公开路由;但公开路由意味着任何人都能访问——通过其他方式(例如 HMAC 签名;参见 boogy:boogy-webhooks)对其进行身份验证。
委托门控 + 速率限制保持服务级。公开路由覆盖无法绕过 [ingress.delegation] 门控,并且共享速率限制桶。

没有 [[ingress.routes]] 的清单完全保持之前的行为——这纯粹是新增功能。

反向场景:开放服务中的受限于树(仅服务所有者可访问 /admin)

另一种情况——服务的默认模式是开放的(authenticated),但其 /admin/* 子树仅允许服务所有者(服务提供者)访问。陷阱在于:可被提供的模块由任何人部署,因此你绝不能在清单中硬编码身份(@alice,boogy://alice/services/*)——这样的字面所有者对于其他提供者都是错误的,并且入口允许列表字符串在部署时不会被替换。入口没有“与我有相同所有者”的匹配器。

使用无门控的 caller_is_service_owner() 能力——主机证明调用者是否是此服务的所有者(它们的代理令牌,由主机端根据代理注册表解析,或者是它们自己的工作负载)。无需路由级入口,无需硬编码身份:

[ingress]
mode = "authenticated"          # 所有路由;处理器自行门控 /admin
// require_operator():由主机证明,无硬编码。
fn require_operator() -> Result<(), ApiError> {
    if caller_is_service_owner() { return Ok(()); }   // 所有者:其代理或自身工作负载
    // OBO:所有者的后端代表用户——被证明的角色是工作负载。
    let id = current_identity();
    let actor_owner = id.and_then(|i| i.actor).and_then(workload_owner);
    if actor_owner == Some(self_identity().owner) { return Ok(()); }
    Err(ApiError::forbidden("仅限操作员"))
}

为何这是合适的原语:人类所有者可以直接使用其令牌 curl /admin(wasm 无法解析代理的句柄,但主机可以——这正是该能力的作用),并且所有者的后端可以作为工作负载工作。失效关闭:匿名、不同的所有者或无法解析的调用者 → false。(参见 resend-base 目录模块。)之前的“internal + 相同所有者工作负载”模式也可行,但排除了人类直接 curl——对于仅所有者界面,推荐使用 caller_is_service_owner。

守卫与辅助函数快速参考(已验证)

项目用途
auth::required() -> Guard如果匿名则返回 401。用于集合路由(list,create)。
auth::owns_resource(table, owner_col, id_param) -> Guard用于条目路由(GET/DELETE /things/{id})。加载该行,如果缺失或不属于你则返回 404,并将其存储在 req.ctx 中。使用 .slot("name") 处理多次加载。
auth::find_owned(table, owner_col) -> Result<Vec<Row>, _>用于索引端点,按主体限定范围列出。匿名时返回 401。
auth::load_owned(table, owner_col, id) -> Result<Option<Row>, _>用于 MCP/JSON-RPC(id 在请求体中,而非路径中)的单次加载 + 所有权检查。None 表示缺失或不属于你。
auth::require_scope(scope) -> Guard粗粒度能力门控:匿名时返回 401,已登录但缺少该范围时返回 403。

所有者列始终是 DEFAULT_OWNER_COL("owner_principal")——切勿发明 owner_id/created_by。将条目路由编写为 .group([auth::owns_resource("things", DEFAULT_OWNER_COL, "id")], |g| …)。该守卫后的处理器读取上下文中存储的行——不要重新获取。

来自令牌的所有者规则: 创建时,从 auth::current_principal() 填充所有者列。切勿从请求体中读取所有者——客户端可能声称是另一个用户的 id。

铁律:按存在掩码拒绝

缺失和存在但不属于你统一返回 404 —— 绝不返回 403。 这是平台安全不变性,而非风格选择。403-vs-404 的区别是一个枚举 oracle:经过身份验证的用户探测 id 并得知哪些存在(403)与哪些不存在(404)。SDK 守卫强制执行这一规则。

“但 UI 想告诉用户原因。” 在客户端处理:将 404 统一视为“对你不可用”并渲染相应消息;网络状态码保持 404。不要分叉或修补 SDK 守卫来拆分这个掩码。(require_scope 的 403 是不同的维度——粗粒度能力,而非资源所有权——这样是正确的。)

程序化调用者的 API 密钥(已验证方案)

在 wit_glue! 旁边调用 api_keys_glue!(bindings),然后:

  1. 在 init_tables 中调用 api_key_routes::install_table()。
  2. 通过 ApiKeyRoutes 扩展特质挂载管理路由: Router::new().with_api_key_routes()(/_keys)或 .with_api_key_routes_at("/admin/keys")。
  3. 门控你的路由:.group([api_key_routes::guard], |g| ...)。
事实详情
双重凭证guard 接受会话 bearer 令牌或 sk_* 密钥;两者统一为 current_principal(),因此 owns_resource/find_owned 无需更改即可工作。
管理密钥需要会话身份;sk_* 密钥不能创建密钥。
无权限提升密钥仅携带创建者已拥有的范围(否则返回 403)。
存储密钥以哈希形式存储在服务自身的存储中;切勿自行创建表。
格式sk_<env>_<…>_<crc>。

常见误区

想法现实
“返回 403 ‘不属于你’ 可以告诉用户原因。”这是一个 id 枚举 oracle。对两种情况都返回 404;在客户端解释原因。
“我将在加载行后在处理器中检查所有权。”owns_resource 在单个守卫中完成了加载 + 检查 + 上下文存储。读取已存储的行;不要重新实现检查。
“我会添加一个自定义的 api_keys 表。”api_keys_glue! 提供了一个已哈希、隔离、具有范围意识的表。使用它。
“从请求体中读取所有者 id。”从 current_principal() 填充。请求体是攻击者可控的。
“一个公开的 [[ingress.routes]] 路由仍需在 wasm 中进行身份验证检查。”公开意味着任何人都能访问它——通过其他方式(例如 Webhook 的 HMAC 签名)进行身份验证。覆盖不会自行门控。

集成

← boogy:designing-boogy-services 选择入口模式,供给第 1 层。boogy:boogy-account-auth 是主体的来源(登录→你读取的令牌)。关于跨服务代表用户操作,请参见 boogy:boogy-obo-delegation。用于已签名回调的公开路由覆盖是 boogy:boogy-webhooks 的前半部分。

相关技能