跳至主要内容

ADR-020: auth-admin optional feature(帳號/角色管理端點收編)

ADR 編號: 020 狀態: 已被取代 (Superseded) 決策日期: 2026-07-20 決策者: Development Team 取代: 無 被取代: ADR-022——adoption 後的 fleet 回饋顯示帳號來源異質(特殊管道建立的帳號不宜經本地 CRUD 管理)、管理面分歧擴大,canonical service 的統一模型前提不成立;auth-admin 整域降級為業務層參考實作


摘要

新增 optional feature slice auth-admin(requires auth):帳號管理(CRUD、啟停、解鎖、密碼)+角色指派+**完整角色管理(含自訂角色 CRUD)**的參考端點面。service/DTO 為 canonical(ADR-017 泛型基底模式,對 seam 化的 Account 操作);controller 列 seam(per-project 欄位與端點的落點,對齊 auth 既有 AuthController 先例);參考實作 Account@Version opt-in(樂觀鎖,完整往返 pattern)。


背景 (Context)

收編訊號:fleet 兩家各自重造

參考實作的 auth feature 只有認證面(AuthController:login/refresh/me),沒有管理面——帳號怎麼建、角色怎麼派,每個下游自己發明。實測兩家已上線/接近釋出的下游:

面向ict(controller/auth/tts(controller/system/
帳號 CRUDlist / get / create / patch / delete
帳號生命週期activate / deactivate / unlock / patch passwordpatch status / reset-password
角色指派patch {id}/rolesget + patch {id}/roles
角色管理唯讀(list / get / authorities)完整 CRUD + patch authorities + 成員 list/patch(SYSTEM/CUSTOM 分流)

共同核心高度重合(帳號 CRUD+生命週期+角色指派+角色/權限查詢),分歧僅在角色管理深度。第三個新專案 scaffold 後大概率再造第三份——這正是「app 層實務沉澱成熟、該收編為框架能力」的訊號(同 ADR-019 / FU-40 的治理節奏)。

為何是現在

  • 多個新專案即將 scaffold(同 ADR-018 的動機):收編要趕在增殖之前——scaffold 之後才收編,會重演「下游先自造→客製深→難歸隊」。
  • 前置已備:ADR-017(泛型基底+entity 工廠)、ADR-018(AccountBase canonical+Account seam、啟停日期上 base)、ADR-009 dual-mode RS(resource:action 詞彙)、隔離 variant 軸(方法論 ADR-009)全部落地,auth-admin 需要的每一種模式都有先例。
  • 參考實作兩頭皆缺:app-server 無管理端點、app-office-mockup 無 account applet——收編順帶補齊參考實作的管理面故事(server 先行,UI applet 之後另走正規 US 流程)。

傳播機制(與 jar 發版解耦)

feature slice 隨 app-server 源碼/feature provisionsync 傳播(ADR-006/007),不走 jar 班車——時程不綁 4.0.0 發版。


決策 (Decision)

D-A:optional feature auth-admin,requires auth;自訂角色管理進 core

  • 為何 optional 而非併入 required 的 auth:認證是每專案必要;管理端點不是(headless 服務可能只用 service account、或帳號由外部 IdP 管理)。optional 使不需要的專案不裝、不曝露端點面。
  • core scope = 兩家共同核心 ∪ 自訂角色管理:
    • 帳號:list(分頁/搜尋)、get、create、patch、delete、activate/deactivate(承 ADR-018 的啟停日期)、unlock、密碼管理(admin 重設)
    • 角色指派:get/patch {accountId}/roles(首位=主要角色,契約同 UserResponse
    • 角色管理:list(SYSTEM/CUSTOM 分流)、get、create/patch/delete(限 CUSTOM;SYSTEM 內建角色唯讀)、patch authorities、成員 list/patch(角色視角的反向指派)
    • 權限查詢:list authorities(供角色編輯 UI 的選單)
  • 自訂角色進 core 的理由:Role entity 的 SYSTEM/CUSTOM 型別本就是 canonical 既有設計(含 tenantId),tts 已實證需求,entity 零改動、邊際成本低;切成 phase 2 只會製造第二次 provision 的攤銷成本。

D-B:service canonical、controller(含 DTO 映射)列 seam

  • service 直接引用 seam 型別,不需 jar 級泛型:feature slice 住 app-server 源碼(非 jar),依 Account參考實作空體紀律(ADR-018),canonical 碼可引用具體 AccountAccountRepository、但只觸 AccountBase 成員與建構子——AccountRepository(canonical、typed to Account)即既有先例。ADR-017 的泛型基底模式是 jar 專用(jar 看不到應用類),此處不適用。
  • service 出 entity、收 canonical command + customizer hookAccountAdminService 的 create/update 收「base 欄位 command record + Consumer<Account> customizer」——seam controller 以 customizer 在同一交易內套 per-project 擴充欄位,canonical 碼不認識任何擴充成員。
  • DTO 映射住 seam controller(巢狀 record):request/response 形狀本質 per-project(擴充欄位、@Version 往返欄位),故不做 canonical DTO——controller 自持 request/response record、自做 entity↔DTO 映射。canonical 面=service 方法簽章(command record + entity 回傳)。
  • controller 列 seam(catalog seams):per-project 擴充欄位、端點增刪、授權微調全在 controller 層落地——對齊 auth 的 AuthController 既有先例。不採 canonical controller+extension SPI:SPI 只能開洞給「欄位」,端點形狀的客製仍得 fork。
  • 參考 controller 隨 provision 帶下(初版即可用),之後屬下游擁有、框架不覆蓋(seam 語意)。

D-C:隔離中性——政策 SPI,不做 overlay

管理面的租戶邊界(tenant 模式:TENANT_ADMIN 限本租戶、SUPER_ADMIN 跨租戶;ownership 模式:無租戶邊界、角色授權承擔)走政策 SPI(先例:GlobalTemplateWritePolicy):

  • canonical service 經 AccountAdminPolicy 裁決管理範圍(managedTenantId():有值=限該租戶、範圍外一律 404 不洩漏存在性;empty=不設邊界);宣告一個 bean 即覆蓋預設。
  • 預設實作由 principal 的 tenantId claim 導出範圍PrincipalTenantAccountAdminPolicy):tenant 模式下 TENANT_ADMIN 的 token 帶本租戶 claim → 限本租戶,SUPER_ADMIN(tenant=null)claim 缺席 → 跨租戶;ownership 模式所有帳號 claim 恆缺席 → 恆不設邊界。同一份預設依資料自然分流兩種隔離策略——零 TenantContext 依賴、無租戶機制耦合、不需 tenant 組裝額外接線、實體面零 overlay(Account 本就 tenant-optional、Role.tenantId 既有)。

D-D:參考實作 Account@Version opt-in(ADR-018 決策四的回答)

帳號管理 UI 出現後,「admin 改角色 vs 使用者改 profile」的並發覆蓋成為真實場景;角色指派錯誤直接影響權限,屬「會因覆蓋受損」的 opt-in 判準(見 concurrency.md)。決定:

  • 參考實作的 Account(seam)加 @Version,走完整往返 pattern:version 進管理面 DTO、比對不符拋 ConflictException(映 409)、真併發 flush 由 Hibernate 樂觀鎖兜底。
  • version 往返的碼全部落在 seam controller——這是空體紀律的必然:canonical service 只觸 AccountBase 成員,看不到 version;response 的 version 欄位、update 前的 version 比對皆由 seam controller(可觸具體 Account)承擔。未 opt-in 的下游從自己的 controller 拿掉 version 欄位即可,canonical 零影響。
  • Account 是 seam——此為參考示範而非強制:下游依自己的併發風險決定跟不跟。AccountBase 不加(維持 ADR-018 決策四的「不在基類全域啟用」)。

D-E:權限詞彙——feature 自帶 account:*role:*

  • 新增 AuthAdminAuthorityaccount:read/write/execute/delete(execute=啟停/解鎖等狀態變更)、role:read/write/deleteresource:action 形式(ADR-009)。
  • feature 自帶 AuthoritySeedProvider 宣告入庫(機制既有);Role.json 連結 SUPER_ADMIN/TENANT_ADMIN。未裝 auth-admin 時權限自然不存在(warn-and-skip 語意)。

考量的方案 (Alternatives)

方案說明不採理由
不收編(現狀)各下游自造第三家 scaffold 後再造第三份;共同核心已實證高度重合
併入 required 的 auth管理端點隨認證必裝headless/外部 IdP 專案不需要管理端點面;optional 讓不裝=不存在
canonical controller + extension SPI框架同步 controller、開 SPI 給擴充欄位SPI 只解欄位、解不了端點形狀客製;seam 有 AuthController 先例、語意一次講清楚
自訂角色管理延後 phase 2core 只做帳號+角色唯讀entity 零改動、tts 已實證需求;拆兩段徒增 provision 攤銷
@Version 不開維持 last-write-wins管理面使帳號成為多寫者實體,角色覆蓋屬權限事故;seam 上示範 opt-in、下游可自行退出

後果 (Consequences)

正面

  • 新 scaffold 專案自帶帳號/角色管理端點面;參考實作管理面故事補齊。
  • 共同核心單點演進;權限詞彙(account:*role:*)fleet 一致。

負面 / 成本

  • ict/tts 不強制歸隊(客製已深、已上線);收編價值主要在增量專案。兩家若日後想歸隊,走 /feature provision 的 drift 三分類逐檔裁決。
  • controller 為 seam = 框架對端點面的後續演進不會自動下行(seam 不覆蓋);重大端點變更需下游自行跟進(於 CHANGELOG/sync 報告揭露)。

中性

  • UI(account applet)不在本 ADR 範圍——之後以正規 US 流程(/us/impl)補 Prototype 與 Integration。
  • 工作量估 2–4 天:service/DTO/authority/參考 controller/測試/catalog 登錄/I3・I4/authority-model.md 管理面補充/api-spec。

落地檢查清單

  • feature/authadmin/AccountAdminServiceRoleAdminService(canonical,依空體紀律只觸 base 成員)+AuthAdminAuthorityAuthAdminAuthoritySeedProvider
  • 參考 controller(帳號+角色,DTO 為巢狀 record)+@PreAuthorize 接權限常數;catalog 登錄(controller 列 seams、requires auth
  • AccountAdminPolicy SPI+預設 [PrincipalTenantAccountAdminPolicy](principal tenantId claim 導出範圍,兩 variant 天生正確、零 TenantContext 依賴——比原 D-C 草案更輕,不需 tenant 組裝額外接線)
  • 參考實作 Account@Version+seam controller 的 version 往返+409 測試
  • Role.json 連結 SUPER_ADMIN/TENANT_ADMIN 的 account:*role:*
  • API 整合測試 AuthAdminIT(12 案:範圍政策×3、403、CRUD+version 往返、重複 409、啟停/解鎖/自我保護、密碼重設可登入、角色指派保序、SYSTEM 唯讀、CUSTOM 全程+in-use 409、權限清單+未知權限 404)
  • authority-model.md 補「管理面」一節;llms.txt
  • I3(feature-inventory.py --check PASSED)/variantCheck/完整 I4(ownership 組裝樹建置)綠
  • api-spec(accounts/roles)+ Bruno 測試集——另行以 /api-spec 反向萃取(追蹤於 FU-66)

拍板紀錄(2026-07-20)

維護者拍板三個設計題:① controller 走 seam(不採 canonical+SPI);② 自訂角色管理進 core(不拆 phase 2);③ 參考 Account@Version opt-in。實作時修訂 D-B/D-C 的機制細節:泛型基底改為「空體紀律直接引用」(app-server 源內不需 jar 級泛型);租戶邊界政策的預設實作由 principal claim 導出、兩 variant 通用,免除 tenant 組裝的額外政策接線。


相關

  • ADR-017——泛型基底+entity 工廠模式(service 泛型化的依據)
  • ADR-018——AccountBaseAccount seam 邊界;決策四的 @Version 評估由本 ADR D-D 回答
  • ADR-009——resource:action 權限詞彙
  • 方法論 ADR-009——隔離 variant 軸(D-C 的中性要求)
  • Authority 權限模型設計——權限模型權威指南(實作後補管理面)