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/) |
|---|---|---|
| 帳號 CRUD | list / get / create / patch / delete | 同 |
| 帳號生命週期 | activate / deactivate / unlock / patch password | patch status / reset-password |
| 角色指派 | patch {id}/roles | get + 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(
AccountBasecanonical+Accountseam、啟停日期上 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 provision/sync 傳播(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 的理由:
Roleentity 的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 碼可引用具體Account/AccountRepository、但只觸AccountBase成員與建構子——AccountRepository(canonical、typed toAccount)即既有先例。ADR-017 的泛型基底模式是 jar 專用(jar 看不到應用類),此處不適用。 - service 出 entity、收 canonical command + customizer hook:
AccountAdminService的 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:*
- 新增
AuthAdminAuthority:account:read/write/execute/delete(execute=啟停/解鎖等狀態變更)、role:read/write/delete,resource: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 2 | core 只做帳號+角色唯讀 | 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/:AccountAdminService+RoleAdminService(canonical,依空體紀律只觸 base 成員)+AuthAdminAuthority+AuthAdminAuthoritySeedProvider - 參考 controller(帳號+角色,DTO 為巢狀 record)+
@PreAuthorize接權限常數;catalog 登錄(controller 列seams、requiresauth) -
AccountAdminPolicySPI+預設 [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 --checkPASSED)/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——
AccountBase/Accountseam 邊界;決策四的@Version評估由本 ADR D-D 回答 - ADR-009——
resource:action權限詞彙 - 方法論 ADR-009——隔離 variant 軸(D-C 的中性要求)
- Authority 權限模型設計——權限模型權威指南(實作後補管理面)