Boogy 授權(服務內授權) 是做什麼的?
Boogy 的授權分為三層。請不要自行實作任何一層—— SDK 會產生經過驗證的輔助函式,以保持安全不變性。
三層模型
- 入口准入 — 平台的入口模式決定請求是否到達你的服務。 當你的程式碼執行時,呼叫者已經是已知的 主體(principal) 或匿名者。
- 守衛守護路由 — 在處理器(handler)執行前進行每路由檢查: 要求登入,或要求對被定址資源的所有權。
- 輔助函式限定資料列範圍 — 處理器僅列出/載入屬於呼叫者的資料列。
使用 auth::current_principal() -> Option<String> 解析呼叫者。
主體是 不透明的 — 切勿解析、移除前綴或假設其為 UUID;只將其用作擁有者資料欄的值以及 auth::* 的輸入。
每路由入口:受限服務中的公開路由
第 1 層(入口)通常是一個服務範圍的模式。但有時候一個路由必須能被服務其餘部分拒絕的呼叫者訪問——
典型的例子是在一個原本為 authenticated(或 internal/mixed)服務中設定一個公開的 /webhook 接收端。
透過每路由覆寫將其獨立出來;服務的其餘部分維持受限:
[ingress]
mode = "authenticated" # 服務範圍預設值 — 其他所有路由
[[ingress.routes]]
path = "/webhook" # 服務相對路徑;ALL 方法
mode = "public" # 任何人都可以訪問 /webhook
安全契約(請仔細閱讀 — 它是預設拒絕的):
| 規則 | 為何重要 |
|---|---|
服務相對的 literal 路徑。 精確(/webhook)或前綴(/stripe/*,邊界為路徑段)。不支援 {param} 捕獲。 | path 是你的路由器看到的相同路徑,也就是你的處理器所處的框架。 |
| 最特定匹配勝出;只應用一種模式。 精確勝過前綴;較長前綴勝過較短。 | 公開的子路由永遠不會 擴大 兄弟路由:/webhook 公開不會放開 /data。 |
預設值 = 服務範圍模式。 未匹配的路徑(或 .. 跳躍嘗試)會落入受限的預設值,永遠不會落入權限最大的路由。 | 忘記列出路由會使其保持受保護,而非暴露。 |
每個覆寫攜帶自己的模式 + 允許清單。 覆寫不會繼承 allowed_agents/allowed_origins。 | 在路由區塊上列出它們;同樣的非空驗證會套用,因此筆誤不會無聲地全部拒絕。 |
按路徑,而非按方法(目前)。 path = "/webhook" 同樣適用於 GET、POST 等。 | 公開的 /webhook 可由任何 HTTP 動詞訪問——因此仍須在處理器內驗證請求(無關的 GET 應不做任何事)。 |
| 在邊緣由主機強制執行。 入口在 wasm 實例化之前執行。 | 你不應在程式碼中自行關閉公開路由;但公開路由意味著任何人都能訪問它——需以其他方式驗證(例如 HMAC 簽章;請參閱 boogy:boogy-webhooks)。 |
| 委派閘門 + 速率限制保持服務範圍。 | 公開路由的例外無法繞過 [ingress.delegation] 閘門,且共用速率限制桶。 |
一個沒有 [[ingress.routes]] 的 manifests 行為與之前完全相同——這完全是新增性的。
反過來:開放服務中的受限子樹(擁有者專用的 /admin)
另一種方向——服務預設為開放(authenticated)但 /admin/* 子樹只能由服務擁有者(佈建者)訪問。
陷阱:一個可佈建的模組是由任何人部署的,因此你不能在 Manifest 中硬編碼身分(@alice、boogy://alice/services/*)——
該 literal 擁有者對其他每個佈建者都是錯誤的,且入口允許清單字串在部署時不會被替換。入口沒有「與我相同的擁有者」匹配器。
使用未閘門的 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 無法解析代理的 handle,但主機可以——這就是能力的作用),而且擁有者的後端可以作為工作負載運作。預設拒絕:匿名、不同的擁有者或無法解析的呼叫者 → 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 在 body 中而非路徑)的單一載入 + 所有權檢查。None = 不存在或不屬於你。 |
auth::require_scope(scope) -> Guard | 粗粒度能力閘門:匿名回傳 401,已登入但缺乏 scope 則回傳 403。 |
擁有者資料欄始終為 DEFAULT_OWNER_COL("owner_principal")——切勿自行發明 owner_id/created_by。將項目路由連接為
.group([auth::owns_resource("things", DEFAULT_OWNER_COL, "id")], |g| …)。
該守衛後方的處理器應讀取存放在 ctx 中的資料列——不要重新提取。
權杖中的擁有者規則: 在建立時,從 auth::current_principal() 蓋印擁有者資料欄。永遠不要從請求 body 讀取擁有者——
客戶端可能聲稱另一個使用者的 id。
鐵律:拒絕原則:透過存在性遮蔽
不存在和存在但不屬於你都回傳 404 — 永遠不回傳 403。 這是平台安全不變性,而非風格選擇。403 與 404 的分離是一個列舉預言機:已驗證的使用者可以探測 id,並得知哪些存在(403)與哪些不存在(404)。SDK 守衛強制執行此規則。
「但 UI 想告訴使用者原因。」 請在客戶端處理:統一將 404 視為「對你不可用」並渲染該訊息;線路狀態保持 404。請不要分支或修補 SDK 守衛來拆開遮蔽。(require_scope 的 403 是不同的軸——粗粒度能力,而非資源所有權——而且是正確的。)
程式化呼叫者的 API 金鑰(已驗證配方)
在 wit_glue! 旁邊呼叫 api_keys_glue!(bindings),接著:
- 在
init_tables中呼叫api_key_routes::install_table()。 - 透過
ApiKeyRoutes擴展特徵掛載管理路由:Router::new().with_api_key_routes()(/_keys)或.with_api_key_routes_at("/admin/keys")。 - 關閉你的路由:
.group([api_key_routes::guard], |g| ...)。
| 事實 | 詳細資訊 |
|---|---|
| 雙重憑證 | guard 接受 session bearer 或 sk_* 金鑰;兩者統一為 current_principal(),因此 owns_resource/find_owned 無需修改即可運作。 |
| 管理金鑰 | 需要 session 身分;sk_* 金鑰不能建立金鑰。 |
| 無權限提升 | 金鑰僅攜帶建立者已持有的 scope(否則回傳 403)。 |
| 儲存 | 金鑰以雜湊形式存在於服務自己的儲存中;切勿自行建立表格。 |
| 格式 | sk_<env>_<…>_<crc>。 |
紅旗
| 想法 | 現實 |
|---|---|
| 「403 '不屬於你' 能告訴使用者原因。」 | 這是 id 列舉預言機。對兩者都回傳 404;在客戶端解釋。 |
| 「我將在處理器中載入資料列後檢查所有權。」 | owns_resource 在一個守衛中同時完成載入 + 檢查 + 存入 ctx。讀取已存放的資料列;不要重新實作檢查。 |
| 「我會新增自訂的 api_keys 表格。」 | api_keys_glue! 提供一個已雜湊、隔離、具 scope 感知的表格。請使用它。 |
| 「從請求 body 讀取擁有者 id。」 | 從 current_principal() 蓋印。body 是由攻擊者控制的。 |
「公開的 [[ingress.routes]] 路由仍需在 wasm 內進行驗證檢查。」 | 公開意味著任何人都能訪問它——需以其他方式驗證(webhook 的 HMAC 簽章)。覆寫不會自行關閉。 |
整合
← boogy:designing-boogy-services 選擇為第 1 層提供輸入的入口模式。boogy:boogy-account-auth 是主體的來源(登入 → 你讀取的權杖)。關於跨服務代表使用者行動,請參閱 boogy:boogy-obo-delegation。用於簽署回呼的公開每路由例外是 boogy:boogy-webhooks 的前半部分。