搜尋開發指南

試試「多租戶」、「啟動」或「outbox」。
搜尋僅使用本站文件,不會傳送至外部服務。

文件目錄

NEXUSPRO / HANDBOOK

身分、IAM 與權限目錄

從目前身分投影到角色、群組、升權、選單與欄位政策,沿著真實 API 與前端頁面理解 NexusPro 的授權治理。

內容核對 2026-09-28·圖文指南

本章目標與前置條件

讀完後,你應能分辨「目前是誰」「此人持有哪些常駐權限」「此刻是否正在升權」以及「此頁為何可見」。 前置條件是了解 HTTP、UUID、角色與資料範圍;不需要先修改任何帳號或角色。

核心概念

身分投影與授權投影分開

GET /v1/me 回傳 account、tenant、可為 null 的 employee、effective_permissions、groups 與 elevation_session。 employee 為 null 表示該帳號沒有 active 員工檔。effective_permissions 只包含常駐權限;升權會話獨立回傳, 避免會話撤銷後畫面仍把臨時權限當成永久權限。elevation_session 是該帳號目前有效的會話,與這次請求有沒有帶升權 header 無關。

GET /v1/me/menus 回傳 nodes、攤平的 page_keys 與已排列的 navigation。兩者(連同 POST /v1/authz/check)都是 auth_only 路由。不存在可單獨授予的 menu permission:頁面可見性由掛在該頁的 read point 推導(管理員改綁 point 所屬頁面後,選單跟著改),另有三條規則(internal/service/iam/menus.go):

  • tenant-wide 頁(例如所有 iam.* 頁)要求該 read point 的資料範圍達 tenant 以上;只有 self 等窄範圍的 read point 不會讓管理頁出現。
  • baseline 首頁 platform.home 不綁任何 point,除非租戶把它隱藏,否則對所有登入者可見。
  • 待辦審核頁 workflow.approval 另依 authz.Decide 對 workflow.action read(TaskKind inbox)的判定顯示;這條判定走 task 關係分支,沒有常駐 read point 的帳號也可能看到(inbox 只列呼叫者自己的任務)。

按鈕操作仍須檢查精確的 resource.action。

認證鏈:從 token 到 principal

後端不持有登入 session,只接受 Bearer JWT:

  1. 前端 proxy.ts 把 httpOnly cookie _t 的 access token 注入 Authorization header,瀏覽器 JS 拿不到 token。
  2. identityMiddleware(internal/api/v1/middleware.go)要求恰好一個 Bearer credential,交給 identity.Resolver(internal/service/identity/resolver.go)。
  3. internal/platform/keycloak/verifier.go 依 kid 從 JWKS 取公鑰驗簽,檢查 iss、aud(client ID)、exp/nbf(含 leeway)、sub,以及必須是合法 UUID 的 tenant_id claim。
  4. resolver 以 tenant_id claim 開該租戶的交易(RLS 租戶),在交易內依 (issuer, subject) 查 user_identities 取得 account,再讀 tenant 與該帳號的 active employee。
  5. account 狀態與 tenant lifecycle 都必須是 active,通過後 TenantID、AccountID 才寫進 RequestContext。
情況回應(internal/api/v1/identity.go 的 identityError)
缺 token、驗簽或 claim 不合法、租戶內查無綁定401 10100 unauthenticated
帳號不是 active403 10101 account_disabled
租戶不是 active403 10102 tenant_suspended
JWKS 或資料庫等依賴失敗503 10500 service_unavailable

前端 libs/api/client.ts 遇到 10100 先呼叫 /api/auth/refresh 一次並重送,失敗才登出;10101、10102、10104 直接登出;503 不登出。登入不會自動建立帳號或 user_identities 綁定;帳號開通與密碼重置見 組織與員工。

升權會話如何生效

  1. 資格:管理員以 PUT /v1/iam/roles/:id/assumers/:accountId 授予帳號承擔某個 active break_glass 角色的資格(須附理由;不能授予自己、升權中不能授予,對象帳號須為 active)。
  2. 開會話:該帳號 POST /v1/iam/elevation-sessions(role_id、reason、ttl_seconds),成功回 201。角色必須是 active、break_glass 且 requestable,帳號必須在 role_assumers 內;ttl_seconds 必須大於 0、不超過 4 小時,角色的 max_ttl_seconds 大於 0 時也不得超過它;同一帳號已有有效會話時回 409 20205 session_active。
  3. 使用:會話不會自動套用。請求必須帶 X-Elevation-Session header(前端只在呼叫端傳入 config.custom.elevationSessionId 時由 libs/api/client.ts 附加),authz.Decide 才會考慮它:會話不存在、不屬於本人、已過期、已撤銷或 header 不是 UUID,一律 403 20102 elevation_invalid;會話角色涵蓋該 point 但會話帶有 boundary 時,因 boundary 格式尚未定案,目前一律拒絕並回 403 20103;常駐授權已命中時,判定與沒帶 header 完全相同。
  4. 撤銷:DELETE /v1/iam/elevation-sessions/:id 寫入 revoked_at 並回 204;會話判定不走快照,下一個帶該 header 的請求就被拒。

授權資料的來源

來源語意目前狀態
account/role binding角色綁定,principal 可為帳號或群組,可含起訖時間;break_glass 與 system 角色不能綁,也不能綁給自己或自己所在的群組已實作(原始碼層級)
user group有效時間窗內的成員,繼承群組角色;不能把自己加入群組已實作(原始碼層級)
role assumer帳號對某個 break_glass 角色的承擔資格,是開升權會話的前提,屬高風險治理backend route 已實作;前端尚無 UI
elevation session有理由與 TTL 的 break-glass 臨時授權,只能收窄邊界;須由請求 header 明示使用backend route/service 已實作;前端尚無管理頁
data scopeself、direct_reports、department、department_subtree、tenant、all已實作(原始碼層級)
field policy依 clearance 套用 readonly、mask、hide 或 denybackend API 已實作;前端尚無 UI

判定使用精確 permission key,不用前綴或萬用字元推測。資料範圍是額外約束,不能由前端選單取代。

功能清單與現況

功能主要 API/頁面現況說明
角色/v1/iam/roles、/iam/rolesfrontend iam/roles 有列表、新增、編輯(含 SoD、requestable、max TTL、clearance)與帶 version 的樂觀鎖更新/刪除
角色綁定/v1/iam/roles/:id/membersbackend 可綁帳號或群組;frontend 只在 /iam/groups 建立與解除「群組」綁定,沒有帳號直綁入口,也沒有角色成員列表
群組與成員/v1/iam/user-groups、/iam/groupsfrontend iam/groups 有成員時間窗、群組角色檢視與群組綁定
帳號/權限檢視/v1/iam/accounts、/iam/accessfrontend iam/access 有帳號選取與來源呈現
可承擔者/v1/iam/roles/:id/assumersbackend 已有 GET/PUT/DELETE;frontend 尚無 UI,只有權限點文案
升權/v1/iam/elevation-sessionsbackend 已有 route;frontend 沒有 /iam/elevation 頁,也沒有呼叫此 API 的 hook
權限目錄與選單/v1/iam/catalog、/iam/catalogfrontend 有頁面、群組、point 綁定與轉換工具
欄位政策/資料範圍/v1/iam/field-policies、/v1/iam/data-scopesAPI 已有;資料範圍第一階段為固定內建六項且唯讀(角色表單會讀取);欄位政策沒有前端 UI
資源級預檢POST /v1/authz/check拒絕也回 200,body 為 allowed、scope、reason_code,並納入 X-Elevation-Session;前端 hooks/api/useAuthzApi.ts,目前用於員工匯出逐筆預檢

因此「升權 API 已存在」與「升權管理頁已完成」必須分開描述;不要把 iam.elevation 的 page metadata 當成前端 route 已存在的證據。HR 的組織、員工、職位等畫面在缺少會話或收到 20102 時會顯示連到 /iam/elevation 的「前往升權會話」,該路徑落到 catch-all app/(platform)/[...segments]/page.tsx:頁面可見時只顯示「開發中」的 PendingRoutePage,不可見時顯示 not-found,目前無法從前端建立會話。

實際請求鏈

text
瀏覽器 same-origin /v1/**
  → proxy.ts 以 httpOnly cookie _t 注入 Authorization: Bearer
  → identityMiddleware → identity.Resolver(Keycloak JWT、tenant_id claim、user_identities)
  → GET /v1/me:EffectivePoints + IAM Profile
  → GET /v1/me/menus:exact page_keys / navigation
  → PageGate(path membership)+ permissionSet.can(point)
  → 業務 API 由 authorizeMiddleware 以 authz.Decide 再判定(含 X-Elevation-Session 與 tenant-wide)

前端 useGetMe 的 Zod schema 在 runtime 要求六段都存在,但各層都是 .passthrough()(容許未知欄位,狀態值以字串接收、 由判定函式 fail closed);「恰好六段的 closed object」由後端 api/openapi.yaml 的 MeData(additionalProperties: false) 保證,前端漂移由 test/contract/openapiShape.test.ts 抓。hooks/api/useMenusApi.ts 的 useGetMenus 只負責取數; 精確 membership 在 libs/permission/menuAccess.ts 的 hasPage/hasPath,PageGate 依 path 判斷,不從父頁推導子頁。 libs/permission/permissionSet.ts 的 can() 是 Set 查找,不能改成 startsWith;升權不會改變 can() 的結果。

開發步驟

  1. 先在 internal/api/v1/routes.go(IAM 路由都在這裡;其他域在各自的 *_routes.go 或 handler 檔的 register*Routes)找 route.Point(...) 宣告的 point、risk、tenant-wide 與 page,對照全量快照 internal/route/testdata/registry.golden 與頁樹 internal/route/pages.go;api/openapi.yaml 只提供 wire schema,不含 point/risk/page 等權限 metadata。
  2. 寫方案時先列端點、權限點、資料範圍、欄位效果與錯誤碼;不要先在 React 中藏按鈕。
  3. 角色或 group binding 變更由 service/iam 交易處理,確認 SoD、版本 CAS、有效時間窗與權限版本失效。後端會以 422 10430 拒絕 break_glass/system 角色的綁定,以及綁給自己、綁給自己所在的群組或把自己加入群組(欄位碼 break_glass_not_bindable、immutable、self_binding)。
  4. 若新增頁面或 point,改 internal/route/pages.go 與路由宣告(API 啟動時由 catalog reconcile 逐租戶同步),再確認 /v1/me/menus 的呈現排序與 tenant-wide 可見性。
  5. 欄位政策只對已宣告 field 寫入;未宣告欄位回 422 10430(欄位碼 undeclared),而不是靜默新增。
  6. 前端以 useGetMe、useGetMenus、IAM hooks 串接,保留 loading、403、503 與未知 enum 的 fail-closed 呈現;治理寫入後用 libs/permission/revalidateIdentity.ts 同時 revalidate /v1/me 與 /v1/me/menus;需要依單筆資源或目標員工判定時用 POST /v1/authz/check,不要用 effective_permissions 推測資料範圍。

預期結果與驗證

預期結果是:同一帳號在 /v1/me 看見常駐權限與獨立升權狀態;/v1/me/menus 只列可見頁; IAM 管理頁的 mutation 仍由 backend permission、tenant-wide 與資料庫 RLS 共同守門。

建議按以下順序做驗證:

  1. 靜態核對 internal/route/testdata/registry.golden 與路由宣告的 permission triple 完全一致(tests/contract/route_snapshot_test.go 以 golden 比對)。
  2. 檢查角色綁定的生效/到期邊界,以及 group 半開時間窗。
  3. 用兩個受控租戶做正、負向查詢,確認 account/role/catalog 不跨租戶。
  4. 以升權 session 驗證「只收窄、不放寬」:沒帶 X-Elevation-Session 時判定不變;撤銷後 /v1/me 的 elevation_session 變成 null,再帶同一 header 的請求回 403 20102;effective_permissions 自始至終不含升權點。
  5. 檢查 field policy 的 mask/hide 輸出與資料範圍的條件,再進行前端頁面驗收。

以上是核對清單,不代表本輪已執行這些測試或 API 操作。

常見錯誤

  • 把 effective_permissions 當成包含 elevation 的完整權限集。
  • 以為 /v1/me 出現 elevation_session 就代表請求已升權;沒帶 X-Elevation-Session 的請求不會使用會話。
  • 只隱藏按鈕,卻沒有在 backend route/service 重做授權。
  • 用 iam.* 前綴猜測權限,導致未知或相鄰 point 被意外放行。
  • 將 menu visibility 當成 grant,或以父頁可見推導所有子頁;也別忘了 tenant-wide 頁需要 tenant 以上的資料範圍。
  • 想把 break-glass role 綁成 standing access;後端會以 break_glass_not_bindable 拒絕,應改走 assumer 資格加升權會話。
  • 把目前找不到 frontend elevation page 寫成「升權功能不存在」;應標成 backend 已實作、UI 待補。
  • 把 source-level test 檔存在寫成已通過,或把登入頁能開啟寫成 IAM mutation 已驗收。

相關文件

內容來源與核實範圍

以下路徑相對於所列業務倉庫;核實層級為「已讀原始碼」,不是本輪業務測試或部署驗收。

  • nexus-pro-be-plus/internal/api/v1/routes.go
  • nexus-pro-be-plus/internal/api/v1/middleware.go
  • nexus-pro-be-plus/internal/api/v1/authorize.go
  • nexus-pro-be-plus/internal/api/v1/authz_check.go
  • nexus-pro-be-plus/internal/api/v1/identity.go
  • nexus-pro-be-plus/internal/api/v1/me_menus.go
  • nexus-pro-be-plus/internal/api/v1/iam_roles.go
  • nexus-pro-be-plus/internal/api/v1/iam_groups.go
  • nexus-pro-be-plus/internal/api/v1/iam_access.go
  • nexus-pro-be-plus/internal/api/v1/iam_elevation.go
  • nexus-pro-be-plus/internal/api/v1/iam_catalog.go
  • nexus-pro-be-plus/internal/service/identity/resolver.go
  • nexus-pro-be-plus/internal/platform/keycloak/verifier.go
  • nexus-pro-be-plus/internal/service/iam/elevation.go
  • nexus-pro-be-plus/internal/service/iam/role_assumers.go
  • nexus-pro-be-plus/internal/service/iam/bindings.go
  • nexus-pro-be-plus/internal/service/iam/self_binding.go
  • nexus-pro-be-plus/internal/service/iam/field_policies.go
  • nexus-pro-be-plus/internal/service/iam/menus.go
  • nexus-pro-be-plus/internal/authz/effective.go
  • nexus-pro-be-plus/internal/authz/elevation.go
  • nexus-pro-be-plus/internal/route/pages.go
  • nexus-pro-be-plus/internal/route/testdata/registry.golden
  • nexus-pro-be-plus/tests/contract/route_snapshot_test.go
  • nexus-pro-be-plus/docs/infra/11-iam-governance.md
  • nexus-pro-be-plus/docs/infra/12-page-permissions.md
  • nexus-pro-web-plus/proxy.ts
  • nexus-pro-web-plus/libs/api/client.ts
  • nexus-pro-web-plus/hooks/api/useMeApi.ts
  • nexus-pro-web-plus/hooks/api/useMenusApi.ts
  • nexus-pro-web-plus/hooks/api/useIamAccessApi.ts
  • nexus-pro-web-plus/hooks/api/useAuthzApi.ts
  • nexus-pro-web-plus/libs/permission/menuAccess.ts
  • nexus-pro-web-plus/libs/permission/permissionSet.ts
  • nexus-pro-web-plus/test/contract/openapiShape.test.ts
  • nexus-pro-web-plus/app/(platform)/iam/roles/page.tsx
  • nexus-pro-web-plus/app/(platform)/iam/groups/page.tsx
  • nexus-pro-web-plus/app/(platform)/iam/catalog/page.tsx
  • nexus-pro-web-plus/app/(platform)/[...segments]/page.tsx
NexusPro 開發指南以程式碼為準 · 以驗證為據