Skip to content

feat(identity): refuse requests of suspended tenants on every request - #797

Merged
Ruisi-Lu merged 2 commits into
mainfrom
feat/777-tenant-suspended-check
Oct 30, 2026
Merged

Ruisi-Lu merged 2 commits into
mainfrom
feat/777-tenant-suspended-check

Conversation

@Ruisi-Lu

Copy link
Copy Markdown
Member

摘要

清單 B1「每次請求檢查停權」:租戶被停權或已刪除之後,同一個 session 的下一個請求就被擋下。入口的 cookie 檢查(身分服務的 ext_authz)在讀成員角色的同一個查詢、同一個租戶交易裡讀租戶的狀態:路由到身分服務以外的服務時回 403 tenant_suspended、不簽 token;路由到身分服務時照常簽,token 帶新的選填 claim tenant_suspended,由 service-kit 的 Requirement::Tenant 以 403 tenant_suspended(與 permission_denied 分開)拒絕。token 交換與 AI 的 run 委派(建立與每次換 token)也回 tenant_suspended。讀 session、切換租戶、登出、偏好設定與平台管理者在身分服務的平台端點(含恢復)照常可用。

變更

  • crates/contract:InternalClaims 加選填的 tenant_suspended(serde(default),false 時不序列化)。
  • crates/service-kit:Requirement::Tenant 在比對角色前拒絕帶 claim 的 token,回 ErrorCode::TenantSuspended;UserIdentity::is_tenant_suspended();OnBehalfError 與 RunDelegationError 照傳 tenant_suspended。
  • services/identity:
    • session.rs:MEMBERSHIP 查詢以 LEFT JOIN identity.tenants 一起讀角色與狀態;沒有租戶列時視為 active,不認得的狀態回 503(fail closed)。
    • ext_authz.rs:停權租戶只給身分服務簽 token,其他服務 403 tenant_suspended,日誌 reason 是 tenant_suspended。
    • issuer.rs:只有帶租戶時才寫 claim。
    • exchange.rs:拒絕帶 claim 的 token(第二道防線)。
    • delegation.rs:建立時看 claim 與當下的狀態,每次換 token 重查狀態;恢復之後同一個委派又換得到。
    • current_session.rs:TenantMembership 加 state(active / suspended / deleted),不含停權原因。
  • api/openapi.json 與 frontend/src/api/schema.gen.ts 重新產生;frontend/src/api/identity.ts 的 zod schema 讀 state(畫面在 SPA 停權說明頁接上真實後端,並以瀏覽器實測(B1) #780)。
  • 測試:新增 services/identity/tests/tenant_suspension.rs(真的 binary 與 migration Job 套用的產品 schema),以及 ext_authz、exchange、delegation、issuer、session、current_session、service-kit permission / on_behalf / run_delegation 的單元測試。tests/ext_authz.rs 與 tests/adversarial.rs 的資料庫多套用 0002_tenants.sql 與 0008_tenant_state.sql。
  • 文件:services/identity/README.md(新的「停權時的每次請求檢查」一節,含代價)、crates/service-kit/README.md、crates/contract/README.md、docs/architecture/permissions.md、docs/requirements/rebuild-checklist.md B1。
  • 沒有新的 migration。

驗證

  • cargo fmt --all --check、cargo run -p openapi -- --check、cargo clippy --locked -p service-kit --no-default-features --all-targets -- -D warnings、cargo test --locked -p service-kit --no-default-features:通過(exit 0 at 19:11:59)。
  • CI=1 cargo test --locked --workspace --exclude agent:2034 passed、0 failed、1 ignored,含 tenant_suspension.rs 的兩個測試與上列單元測試(同一次執行)。
  • cargo clippy --locked --workspace --all-targets -- -D warnings:通過(exit 0 at 18:26:13)。
  • 前端:bun run --cwd frontend api:check、typecheck、lint、test(252 pass、0 fail)、build、check:dist 都通過。
  • 之後與 main 的整合只多了一個 docs/test-reports 的 commit(docs(test-reports): install the agent on the demo gateway #796),沒有改程式。
  • 安全審查(獨立 reviewer):沒有 Critical / High / Medium。Low 的文件錯誤已修正,其他 Low 列在下方。

驗收條件逐條對照

  1. 停權之後同一個 session 的下一個請求回 403 tenant_suspended:tenant_suspension.rs 的 a_suspension_and_a_resumption_apply_to_the_next_request。涵蓋:

    • 其他服務的路由:assets、assistant 的 GET/POST,三個 session 都擋下;
    • 身分服務自己需要租戶的端點:租戶的事件紀錄;
    • AI run 委派:換 token 與建立委派。

    a_deleted_tenant_is_cut_off_like_a_suspended_one 驗證 deleted 也一樣被擋下。單元測試 ext_authz::tests::a_suspended_tenant_reaches_the_identity_service_only 驗證回應不帶身分標頭。

  2. 停權期間照常可用的操作,同一個測試都驗證過:

    • 讀 session(租戶帶 suspended)、改偏好、切換到另一個租戶、切回、登出;
    • 目前租戶被停權的平台管理者讀得到狀態並恢復(PUT active,版本 3)。
  3. 恢復之後的下一個請求:其他服務、事件紀錄與同一個 run 委派都通過,session 顯示 active。

  4. 指定的檢查:見上面「驗證」,全部通過。

已知代價與 Low

  • 平台管理者的目前租戶被停權時,其他服務(assets、certs)的平台端點也回 tenant_suspended,要先切換租戶;只屬於這個租戶的平台管理者在恢復之前用不了(README 已記載)。
  • 已簽出的內部 token 在剩下的效期內(最多 60 秒)仍有效,也換得到 token 交換的 token;run 委派每次重查。
  • 沒有 identity.tenants 列的成員資格視為 active:目前狀態 API 需要這一列,identity_app 也不能刪租戶。合約終止:資料保留、匯出交付、期滿刪除與閘道器回收(B1) #109 刪除資料時,租戶列要和成員資格、session 一起刪,或比它們晚刪。
  • assistant 進行中的 run 對 AI 閘道的模型呼叫不經停權檢查。工具呼叫在下一次換 token 時就被擋下,但模型呼叫最多可能持續到委派到期(900 秒)。
  • 前端的 state 是嚴格的列舉:伺服器以後加了新的狀態,SPA 讀 session 會失敗。由 SPA 停權說明頁接上真實後端,並以瀏覽器實測(B1) #780 決定怎麼容錯。

審查分類建議

建議 review:human。這個 PR 改了存取控制(入口 cookie 檢查、Requirement::Tenant、token 交換、run 委派),也改了對外 API 契約(CurrentSession 的 TenantMembership 多了必填的 state,前端 schema 也跟著改)與內部 token 的 claims。

Closes #777

🤖 Generated with Claude Code

https://claude.ai/code/session_01EnmrdxG91NgAkjWi1afk7W

Ruisi-Lu and others added 2 commits October 30, 2026 19:12
The ingress cookie check now reads the tenant's state with the membership
role, in the same tenant transaction. A suspended or deleted tenant gets
403 tenant_suspended without a token for every service but identity;
identity's own token carries the new optional tenant_suspended claim,
which service-kit's Requirement::Tenant refuses with 403
tenant_suspended, separate from permission_denied. The token exchange
refuses that claim, and run delegations refuse to start or exchange
while their tenant is suspended; OnBehalf and RunDelegations pass the
code through to the user.

The current session lists each tenant's state, so the app can explain
the suspension. Reading the session, switching tenants, logging out,
preferences and identity's platform endpoints (resuming the tenant)
keep working. A membership whose tenant has no row reads as active.

Refs #777

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnmrdxG91NgAkjWi1afk7W
The allow row had lost its third cell, and the tenant_suspended row
described forwarding instead of returning the denial (security review).

Refs #777

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnmrdxG91NgAkjWi1afk7W
@Ruisi-Lu Ruisi-Lu added the review:human 需要人類 reviewer 複審才能合併:介面、UX 功能,或無法明確判定可併入的變更;AI 不得自行合併 label Oct 30, 2026
@Ruisi-Lu
Ruisi-Lu merged commit 774d24c into main Oct 30, 2026
15 checks passed
@Ruisi-Lu
Ruisi-Lu deleted the feat/777-tenant-suspended-check branch October 30, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:human 需要人類 reviewer 複審才能合併:介面、UX 功能,或無法明確判定可併入的變更;AI 不得自行合併

Projects

None yet

Development

Successfully merging this pull request may close these issues.

每次請求檢查停權:入口的 cookie 檢查、run 委派與內部身分回 403 tenant_suspended(B1)

1 participant