Authority 權限模型設計
文檔版本: v3.1.0 最後更新: 2026-08-14 適用對象: 開發團隊、AI Agent、使用 app-tenant-server 範本的團隊 相關 ADR: ADR-009 雙模式 Resource Server、ADR-013 委派與模擬、ADR-018 auth 身分模型、ADR-031 風險導向 API 授權
本文檔記錄參考實作的 Authority 權限模型:RBAC(Account → Role → Authority)搭配風險導向的
business capability、object/data scope 與 Audit。非敏感端點預設以 Authenticated + Audit 保護;
Authority 只用於敏感資訊或敏感/高影響操作,命名採 domain:capability。既有 R/W/X/D 權限是歷史
inventory,不再是新增 domain 的設計模板。
目錄
1. 設計概述
雙層權限模型
┌─────────────────────────────────────────────────────────────────────────┐
│ 前端(Role-based UI) │
│ • 使用 Role 控制選單、Applet、路由與一般操作 UI │
│ • login//me 的 user projection 只提供展開後的 roles │
│ • 不消費 raw Authority;特殊資源能力由 API 回 allowedActions │
│ • 前端判斷只用於 UX;真正的權限控制由後端 API 執行 │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ 後端(API 層級) │
│ • 非敏感資訊/低風險操作:Authenticated + Audit │
│ • 敏感資訊/操作:Authority + 適用 data scope + Audit │
│ • Role 透過 seed 配置獲得對應的 Authority │
└─────────────────────────────────────────────────────────────────────────┘
核心原則
- 帳戶授予 Role - 每個帳戶分配一個或多個 Role(列表第一個為主要角色)
- Authority 有風險門檻 - 非敏感端點使用明確 authenticated policy + Audit;敏感資訊/操作才用
hasAuthority,後端不用hasRole - Role 授予 Authority - 透過 seed 配置(
data/auth/Role.json),Role 獲得對應的 Authority - 權限詞彙的 SoT 在後端 - 前端 mock(MSW)的權限映射是後端的衍生鏡像(見 ADR-009)
- Session UI projection 不含 Authority - login/
/me的 user DTO 只回傳roles[];Access Token 可保留 Authority claim 供 Resource Server 使用,產品前端不得解析它控制介面 - Capability 不機械映射 CRUD - 相同角色、風險與責任的操作先收斂;只有具體差異才拆分
- 保護按風險組合 - authentication 是底線,data scope 獨立適用;Audit 可作低風險端點的正式事後保護,敏感端點才加 capability
實體模型(RBAC,持久化於 DB)
Account ──多對多──> Role ──多對多──> Authority
| 實體 | 位置 | 說明 |
|---|---|---|
Account / AccountBase | feature/auth/ | 帳號。canonical 出 AccountBase(@MappedSuperclass),具體 Account 由各專案擁有(見 ADR-018) |
Role | feature/auth/Role.java | 角色。type 分 SYSTEM(內建、不可修改/刪除、tenantId 為 null)與 CUSTOM(租戶自訂) |
Authority | feature/auth/Authority.java | 業務能力,name 採 domain:capability 字串 |
帳號的有效權限 = 其所有角色的權限聯集(AccountBase.getAllAuthorities())。認證層實體繼承純稽核基類 AuditableBase、不走租戶隔離基類——登入必須在租戶已知之前運作。
權限載入走 fetch plan,不是 EAGER(改動前必讀)
Account.roles 與 Role.authorities 兩層關聯都是 LAZY。組裝 UserDetails 需要一次載完整個 graph,但那是查詢的需求、不是欄位的性質——由 repository 顯式宣告:
@EntityGraph(attributePaths = {"roles", "roles.authorities"})
Optional<Account> findWithRolesAndAuthoritiesById(UUID id);
不要把它改回 FetchType.EAGER(entity-field-types 禁止),也不要靠 spring.jpa.open-in-view:UserDetailsService/AuthPrincipalLookup 跑在認證鏈上、seed contributor 跑在 CommandLineRunner 上,兩者都沒有交易、也不在 HTTP 請求緒上,OSIV 對它們無效。
新增任何載入認證主體的入口時,一律用帶 @EntityGraph 的 finder;漏用的症狀是執行期 LazyInitializationException,且只在沒有 OSIV 的路徑上出現(走 MockMvc 的測試看不見)。參考實作以一支關掉 OSIV、直接呼叫服務的迴歸測試釘住此不變量。
2. Authority 命名規則
命名格式:domain:capability
{domain}:{capability}
domain:穩定的業務/feature 邊界,小寫 snake_case(如application、reference_data)capability:可授予的業務能力,小寫 snake_case(如manage、read_all、export)
歷史備註:既有參考域仍有
{RESOURCE}_{R|W|X|D}常數與resource:read|write|execute|delete。 ADR-031 不要求未盤點消費者就批次改名;它要求新設計不再複製此模式,既有權限則經 capability review 與revokedAuthoritiesmigration 漸進收斂。
Capability 收斂詞彙
| Capability | 適用情境 | 不應使用的情境 |
|---|---|---|
manage | 相同角色、相近風險、同一管理流程中的讀寫與可逆操作 | delete、export 或敏感讀取有獨立責任 |
delete | 不可逆刪除或責任與一般管理不同 | 可安全復原且與 manage 同責任的操作 |
read_all | 越過 ownership/一般資料範圍 | 一般使用者只讀自己可見的資料 |
export | 大量或可攜出資料 | 普通列表/單筆讀取 |
| 領域動詞 | approve、resend、rotate_secret 等風險或職責明確不同的操作 | 只是在重述 HTTP method |
這些是候選詞彙,不是每個 Entity 都必備的固定套餐。拆分前必須能指出角色、風險、資料敏感度或 職責分離的具體差異。
專屬 capability
| Authority | 說明 | 來源 |
|---|---|---|
notification_outbox:resend | 重送通知(觸發真實遞送) | notification feature |
impersonation:enter | 進入模擬(同租戶) | impersonation feature(ADR-013) |
impersonation:cross-tenant | 跨租戶模擬 | impersonation feature(ADR-013) |
目前的 Resource 清單
下表是既有參考實作 inventory,不是新 domain 的產生模板;新設計先走本節的 capability review。
| Resource | 擁有者 | read | write | execute | delete | 其他 |
|---|---|---|---|---|---|---|
product | sales 業務域 | ✅ | ✅ | ✅ | ✅ | |
order | order 業務域 | ✅ | ✅ | ✅ | ✅ | |
customer | customer 業務域 | ✅ | ✅ | ✅ | ✅ | |
reference_data | reference-data feature | ✅ | ✅ | — | ✅ | |
notification_template | notification feature | ✅ | ✅ | — | ✅ | |
notification_outbox | notification feature | ✅ | — | — | — | resend |
impersonation | impersonation feature | — | — | — | — | enter、cross-tenant |
account | authadmin/(業務層參考實作) | ✅ | ✅ | ✅ | ✅ | execute=啟停/解鎖 |
role | authadmin/(業務層參考實作) | ✅ | ✅ | — | ✅ |
授權設計流程
- Consumer/lifecycle:盤點 UI、外部 client、背景工作、workflow、framework replacement 與 obsolete/reference 來源,決定保留、取代或退役。
- Endpoint classification:標記 Public、Authenticated、Capability protected、Object scoped 或 System/operation only;非敏感端點優先 Authenticated + Audit,catch-all authenticated 不算明確分類。
- Risk:評估敏感度、可逆性、損害範圍、可偵測性、職責分離與跨組織/IDOR。
- Authority threshold:只有敏感資訊或敏感/高影響操作才進入 capability convergence;其餘不建 Authority。
- Data scope:把 owner、organization、assignee、creator 或 tenant policy 集中到 service/policy。
- Audit:Authenticated 端點必須有事後稽核決策;敏感端點同樣保留 Audit 作補償控制。
- Tests:建立 401/403/成功/跨 scope/public-state/Audit 矩陣。
API Spec 必須保存逐 endpoint 的授權設計矩陣;任一保留 endpoint 缺分類或必要 scope 時不得實作。
3. 權限的宣告與入庫(分散宣告 + SPI)
權限由擁有者宣告,不集中維護
權限名稱由擁有該資源的 feature 或業務領域宣告,不集中在 auth 的一份清單(早期的集中式 Authority.json / RoleAuthorityMapper 已廢除)。每個域有自己的權限常數類,作為常數與 seed 的單一事實來源:
| 常數類 | 位置 | 宣告的權限 |
|---|---|---|
SalesAuthority | sales/ | product:* |
OrderAuthority(OrderSeedContributor 宣告) | order/ | order:* |
CustomerAuthority(CustomerSeedContributor 宣告) | customer/ | customer:* |
ReferenceDataAuthority | security/referencedata/ | reference_data:* |
NotificationAuthority | feature/notification/ | notification_template:*、notification_outbox:* |
ImpersonationAuthority(ImpersonationAuthorityContributor 宣告) | feature/impersonation/ | impersonation:* |
AuthorityContributor SPI(框架契約)
框架(io.leandev.appfuse.security.AuthorityContributor)開出貢獻點,各域實作它宣告自己的權限:
/// Authority 名稱的貢獻點。
public interface AuthorityContributor {
/// 本 feature/業務領域擁有的權限名稱(domain:capability 形式)。
List<String> authorityNames();
}
存在的理由是依賴方向:認證是必要能力,不得依賴任何選擇性 feature 或業務碼。未安裝的 feature 不貢獻權限,其權限自然不存在——角色連結階段對找不到的權限發 warn 並跳過,正是「feature 沒裝」的語意。契約只宣告「有哪些名字」,消費方式歸消費端——參考實作 seed 進 DB,其他消費端可改餵外部 IAM 或只做啟動期校驗。
入庫流程(AuthSeedContributor)
AuthSeedContributor 開機時依序 seed:Authority → Role → Account。
| 階段 | 行為 | 冪等策略 |
|---|---|---|
| Authority | 收集所有 AuthorityContributor 宣告的權限名,DB 無則新建 | 冪等補缺,不以 count()==0 為 gate——否則新增權限永遠進不了既有持久 DB,導致角色缺權限、端點 403 |
| Role | 讀 data/auth/Role.json;補上 authorities,撤回 revokedAuthorities | 新增與撤回皆冪等;撤回只適用 seed 擁有的 SYSTEM role,CUSTOM role/人工授權不動。找不到的權限 warn 並跳過 |
| Account | 讀 data/bootstrap/auth/Account.json 建立 structural 特權帳號;每個帳號各自產生隨機 UUID 密碼,成功寫入後以 WARN 輸出完整初始密碼 | first-install snapshot:僅在 Account table 全空時安裝整份檔案;既有帳號不重設密碼,也不再次輸出 |
初始密碼不從設定檔或環境變數讀取。操作者須在首次啟動時從受保護的啟動 log 取得密碼,
登入後立即變更;若錯過該次輸出,重新啟動不會再次顯示,應由既有具帳號管理權限的管理者
執行密碼重設。tenant 變體預設建立 superadmin 與 admin,tenantless 變體預設只建立
admin。
Demo 帳號是另一條明確啟用的資料路徑:僅在 app.seed.demo.enabled=true 時讀取
data/demo/auth/Account.json。該檔只宣告 structural 帳號以外的業務示範帳號,每筆必須
明確提供密碼;參考實作統一使用 Password123!,方便直接操作 demo。缺少或空白密碼視為
seed 錯誤,不以隨機值退回。固定 demo 密碼不適用於 production,production 不應啟用 demo seed。
4. Role 定義
結構角色與示範業務角色(type: SYSTEM)
Role.name 存 DB 時不帶 ROLE_ 前綴;輸出給前端契約(login / /me 回應)時補上前綴。
| Role | 顯示名稱 | 說明 |
|---|---|---|
SUPER_ADMIN | 超級管理員 | 平台/跨租戶能力(含 impersonation:cross-tenant),不單獨承載 tenant applet 權限 |
USER | 使用者 | tenant 範圍內的完整 applet 權限;權限模型尚未細分時所有一般帳號均使用此角色 |
MANAGER | 店長 | 既有 demo 的完整業務權限;其中 CRUD authority 屬待遷移的 legacy inventory |
SALES | 銷售人員 | 訂單與客戶管理 |
FLORIST | 花藝師 | 製作流程(訂單狀態變更) |
DELIVERY | 配送員 | 配送流程(訂單狀態變更) |
ACCOUNTANT | 會計 | 唯讀 |
租戶可另建 type: CUSTOM 的自訂角色(如 STORE_MANAGER、CASHIER),由租戶管理、可修改/刪除。
MANAGER、SALES 等角色屬 demo domain;建立真實權限模型時可替換。
角色階層(RoleHierarchyService)
後端是角色階層的 single source of truth:
ROLE_USER
├── ROLE_MANAGER
│ ├── ROLE_SALES
│ └── ROLE_ACCOUNTANT
├── ROLE_FLORIST
└── ROLE_DELIVERY
SUPER_ADMIN 與 USER 刻意不畫階層邊:前者代表平台範圍,後者代表 tenant applet 能力。
Bootstrap 平台帳號直接指派 [SUPER_ADMIN, USER],一般 tenant 帳號只指派 [USER]。
兩個關鍵設計決定:
- 階層只用於「回應的角色陣列展開」:帳號在 DB 只指派直接角色(如 manager 帳號 =
[MANAGER]);RoleHierarchyService在 login //me回應中把直接角色展開為「主角色置首 + 所有可達角色」的扁平陣列(如 MANAGER 也算 SALES / ACCOUNTANT),供前端 RoleGuard 判定。主角色置首是契約的一部分——前端以roles[0]推導主要角色。 - 刻意不註冊為 Spring Security 的
RoleHierarchybean:後端授權一律走hasAuthority(domain:capability),Role.json已把階層攤平進各角色的權限集;階層若進授權層會產生第二套語意。
前端 mock(types/auth.ts 的 ROLE_HIERARCHY)鏡像本階層定義,使真後端輸出與 Prototype 的 MSW mock 一致,整合階段 guard 行為不分歧。
攤平不變量的機械守護:階層邊是 UX 能力承諾(宣告 A > B 即「A 可當 B 用」,guard 會放 A 進 B 的 applet),後端授權卻只看攤平後的權限集——兩者一致與否由 RoleHierarchyAuthorityConsistencyTest 於 build 時檢查:每條已宣告的階層邊,父角色權限集必須 ⊇ 可達子角色的權限集。紅掉時二擇一:補權限進父角色(兌現承諾)或刪除該階層邊(收回承諾——職務分離等「上級不得代行」情境本就該用不畫邊表達)。此檢查使「前端 guard 放行、後端 API 403」的攤平漂移在 build 時現形,而非在使用者面前。
5. Role → Authority 映射
映射的 SoT 是 seed 檔 src/main/resources/data/auth/Role.json(角色階層已攤平進各角色的權限集):
業務資源映射表
| Role | product | order | customer |
|---|---|---|---|
| SUPER_ADMIN | - | - | - |
| USER | R W X D | R W X D | R W X D |
| MANAGER | R W X D | R W X D | R W X D |
| SALES | R | R W X | R W |
| FLORIST | R | R X | R |
| DELIVERY | - | R X | - |
| ACCOUNTANT | R | R | R |
Feature 資源映射表
| Role | reference_data | notification_template | notification_outbox | impersonation |
|---|---|---|---|---|
| SUPER_ADMIN | - | - | - | cross-tenant |
| USER | R W D | R W D | R + resend | enter |
| 其他角色 | - | - | - | - |
詳細說明
SALES(銷售人員)
product:read- 查看商品(銷售時需要)order:read/order:write/order:execute- 查看/新增/修改訂單、確認訂單customer:read/customer:write- 查看/新增/修改客戶
FLORIST(花藝師)
product:read- 查看商品(設計時參考)order:read/order:execute- 查看訂單、變更設計相關狀態(開始設計、完成設計)customer:read- 查看客戶(了解需求)- 注意:沒有
order:write,不能修改訂單內容
DELIVERY(配送員)
order:read/order:execute- 查看訂單、變更配送相關狀態(開始配送、確認簽收)- 注意:沒有
product:*和customer:*
ACCOUNTANT(會計)
- 僅有
*:read(查看權限),只能查看數據,不能修改
6. 實作指南
6.1 權限常數定義(各域自帶)
位置: 擁有該 domain 的 package。
/// `announcement` 領域的 capability 常數(見 ADR-031)。
///
/// 由該領域的 AuthorityContributor 宣告入庫——常數與 seed 的單一事實來源在此。
public final class AnnouncementAuthority {
private AnnouncementAuthority() {}
/** 同一敏感管理責任下的建立、編輯與發布。 */
public static final String MANAGE = "announcement:manage";
/** 不可逆刪除由獨立責任保護。 */
public static final String DELETE = "announcement:delete";
}
6.2 權限宣告入庫(SPI 實作)
該域的 SeedContributor 同時實作 AuthorityContributor:
@Component
public class AnnouncementSeedContributor implements SeedContributor, AuthorityContributor {
@Override
public List<String> authorityNames() {
return List.of(MANAGE, DELETE);
}
// ... 商品 seed 資料 ...
}
選擇性 feature 則用獨立的 provider(未安裝時 bean 不存在、權限不入庫):
/// 模擬權限——impersonation feature 自帶(見 ADR-013)。
@Component
public class ImpersonationAuthorityContributor implements AuthorityContributor {
@Override
public List<String> authorityNames() {
return List.of("impersonation:enter", "impersonation:cross-tenant");
}
}
6.3 Role → Authority 映射(seed JSON)
位置: src/main/resources/data/auth/Role.json
[
{
"name": "CONTENT_ADMIN",
"displayName": "Content Admin",
"displayNameZh": "內容管理員",
"description": "Manages public content",
"type": "SYSTEM",
"authorities": [
"announcement:manage",
"announcement:delete"
],
"revokedAuthorities": [
"announcement:read",
"announcement:write"
]
}
// ... 其他角色 ...
]
6.4 SecurityConfig 配置
@Configuration
@EnableMethodSecurity // 啟用 @PreAuthorize
public class SecurityConfig {
// RS 核心採標準 oauth2ResourceServer().jwt(),兩模式共用 @PreAuthorize 授權,
// 差異只在 token 來源——由 app.security.auth.mode(standalone / federated)切換
}
權限如何進入請求(見 ADR-009):
- standalone 模式:登入時
JwtTokenProvider把帳號的權限(角色權限聯集)寫入自簽 JWT 的authclaim;StandaloneJwtAuthenticationConverter直接讀該 claim 還原GrantedAuthority——無狀態、不回查 DB(權限變更於 token 換發時生效) - federated 模式:IdP token 的 scope 經
ScopeJwtAuthenticationConverter對映到同一套domain:capability
6.5 Controller 使用
依 classification 使用明確 authenticated 或 Authority 表達式;需要 capability 時以常數組合,避免手打字串漂移:
import static io.leandev.app.announcement.AnnouncementAuthority.*;
@RestController
@RequestMapping("/api/v1/announcements")
public class AnnouncementController {
@GetMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<List<Announcement>> list() { ... }
@PostMapping
@PreAuthorize("hasAuthority('" + MANAGE + "')")
public ResponseEntity<Announcement> create(@RequestBody Announcement command) { ... }
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('" + DELETE + "')")
public ResponseEntity<Void> delete(@PathVariable String id) { ... }
}
上例假設列表內容非敏感,因此使用明確 authenticated policy,service 仍須記錄安全 Audit;建立/刪除
公開內容因責任與影響較高才使用 Authority。若一般使用者讀取自己擁有的非敏感文件,可用
Authenticated + owner/組織/assignee scoped lookup + Audit,不必建立 document:read;只有敏感文件才
增加該 capability。具 document:read_all 才能越過一般資料範圍。Public 端點則由明確 permitAll
matcher 擁有,repository 查詢直接帶 published/expiry/revoked predicate。
6.6 新增或審查一個 API domain 的 checklist
- 盤點 consumer 與生命週期,無消費者/已有替代 feature 時先退役。
- 在 API Spec 完成 endpoint security classification 與 risk matrix。
- 先判斷是否敏感;非敏感端點採 Authenticated + Audit,只有跨過門檻才收斂最小 capability。
- 對 Object scoped endpoint 定義 service/policy 層的資料範圍與 403/404 masking。
- 對 Public endpoint 明確宣告
permitAll,並把公開狀態 predicate 放進查詢。 - 為 Authenticated 端點及敏感操作決定 Audit event 與安全 payload。
- 以
{Domain}Authority+AuthorityContributor宣告 capability,Role seed 連結新權限;改名/收斂時 同步填revokedAuthorities。 - Controller annotation 或
SecurityContributormatcher 必須讓每個 endpoint 都有明確 policy owner。 - 自動化 401/403/成功/跨 scope/public state/Audit 矩陣。
- 前端只鏡像 mock token 所需 Authority;產品 UI 仍以 Role 或 API 回傳的
allowedActions判斷。
7. 使用範例
7.1 訂單狀態變更的權限控制
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
// 確認訂單 - SALES 可執行
@PatchMapping("/{id}/confirm")
@PreAuthorize("hasAuthority('" + ORDER_X + "')") // order:execute
public ResponseEntity<Order> confirmOrder(@PathVariable String id) { ... }
// 開始/完成設計 - FLORIST 可執行
@PatchMapping("/{id}/start-production")
@PreAuthorize("hasAuthority('" + ORDER_X + "')")
public ResponseEntity<Order> startProduction(@PathVariable String id) { ... }
// 開始配送/確認簽收 - DELIVERY 可執行
@PatchMapping("/{id}/start-delivery")
@PreAuthorize("hasAuthority('" + ORDER_X + "')")
public ResponseEntity<Order> startDelivery(@PathVariable String id) { ... }
// 修改訂單內容 - 需要 order:write(SALES 以上)
@PatchMapping("/{id}")
@PreAuthorize("hasAuthority('" + ORDER_W + "')")
public ResponseEntity<Order> update(@PathVariable String id, @RequestBody Order order) { ... }
}
7.2 前端 Applet 入口守衛(RoleGuard)
// 路由層以 Role 控制 Applet 入口;後端回應的 roles 已依階層展開為扁平陣列
<RoleGuard allowedRoles={['ROLE_SALES', 'ROLE_MANAGER']}>
<OrdersApplet />
</RoleGuard>
- 前端權限檢查僅用於 UX,真正的權限控制由後端 API 執行
- 例如店長登入後
roles = [ROLE_MANAGER, ROLE_SALES, ROLE_ACCOUNTANT, ...],與allowedRoles有交集即放行
7.3 特殊資源的操作能力投影
一般 UI 只使用 Role。只有 Role 無法表達資源狀態、資料範圍或其他動態政策,且 SBE 明確需要預先提示時,資源 API 才回傳貼近畫面語意的操作能力:
{
"id": "order-123",
"status": "CONFIRMED",
"allowedActions": ["cancel", "refund"]
}
allowedActions/capabilities由後端針對該資源計算,可同時納入 Authority、資料範圍與狀態- 值使用 UI/業務操作詞彙,不直接輸出全域
domain:capabilityAuthority 清單 - 它只改善 UX;執行操作時對應 API 仍須重新授權與驗證狀態
- 不需要這類提示的畫面直接呼叫 API,並統一處理
403
7.4 前端收到 403 的處理
// API 攔截器
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 403) {
showToast('您沒有權限執行此操作', 'error');
}
return Promise.reject(error);
}
);
8. 管理面(業務層參考實作)
本節前述的模型(帳號、角色、權限、映射)如何被管理,由業務層參考實作({controller|service|security}/authadmin/)提供參考端點面(ADR-022;原為 optional feature auth-admin,adoption 後 fleet 回饋顯示帳號來源異質、管理面分歧擴大,降級為 @reference-surface 生命週期——消費端依自己的帳號管理模式 retarget,或整域 prune)。
端點面
| 面向 | 端點 | 權限 |
|---|---|---|
| 帳號 CRUD | GET/POST /api/v1/accounts、GET/PATCH/DELETE /{id} | account:read / account:write / account:delete |
| 帳號生命週期 | POST /{id}/activate、/{id}/deactivate、/{id}/unlock | account:execute |
| 密碼管理 | PATCH /{id}/password(admin 重設,不驗舊密碼) | account:write |
| 角色指派 | PATCH /{id}/roles(全量替換;首位=主要角色) | account:write |
| 角色管理 | GET/POST /api/v1/roles、GET/PATCH/DELETE /{id}、PATCH /{id}/authorities | role:read / role:write / role:delete |
| 角色成員 | GET/PATCH /{id}/members(角色視角的反向指派) | role:read / role:write |
| 權限清單 | GET /api/v1/roles/authorities(供角色編輯選單) | role:read |
關鍵語意
- SYSTEM 內建角色唯讀:修改/刪除回 409;只有 CUSTOM 自訂角色可 CRUD。自訂角色被帳號引用時不可刪(409)。
- SUPER_ADMIN 成員關係受保護:
RoleMembershipPolicy集中守住帳號角色與角色成員兩條異動路徑;只有目前已具ROLE_SUPER_ADMIN的操作者能新增或移除 SUPER_ADMIN 成員。專案若新增其他可提升管理範圍的 SYSTEM role,retarget 時應一併納入。 - 租戶邊界走政策 SPI(
AccountAdminPolicy):tenant 模式下USER依目前TenantContext限本租戶;缺少 tenant context 時 fail closed。SUPER_ADMIN是唯一 root scope,可跨租戶。範圍外的資源一律 404(不洩漏存在性)。 - 角色權限禁止向上授予:非
SUPER_ADMIN的角色管理者只能查看與指派自己目前持有的 authority;SUPER_ADMIN可管理完整 authority catalog。SUPER_ADMIN成員關係仍由RoleMembershipPolicy額外保護。 - service 收 command+customizer、controller 承載形狀:per-project 擴充欄位與
@Version樂觀鎖往返(參考實作的Account已 opt-in)都落在 controller 層(原 ADR-020 D-B/D-D 的設計,作為參考實作內部分工保留)。 - 權限
account:*/role:*由業務層參考實作(security/authadmin/,ADR-022 自 auth-admin feature 降級)的AuthorityContributor宣告入庫,Role.json連結USER。
附錄:設計決策記錄
| 日期 | 決策 | 原因 |
|---|---|---|
| 2025-12-23 | 採用 R/W/X/D Authority 模型 | 細粒度權限控制、類似 Linux 概念易理解 |
| 2025-12-23 | X(execute)用於狀態變更,W(write)用於資料修改 | 區分「執行操作」和「修改內容」的權限層級 |
| 2025-12-23 | D(delete)獨立於 W | 刪除是較危險的操作,需要獨立控制 |
| 2026-06 | 權限詞彙收斂為 resource:action(ADR-009) | standalone / federated 兩模式對映到同一套詞彙;SoT 在後端,前端 mock 為衍生鏡像 |
| 2026-06 | 廢除集中式權限清單,改為分散宣告 + 貢獻點 SPI(時名 AuthoritySeedProvider;2026-07 上收框架並改名 AuthorityContributor) | 權限由擁有該資源的 feature/業務域宣告;auth(必要 feature)不依賴選擇性 feature,未安裝的 feature 權限自然不存在 |
| 2026-06 | 角色階層由 RoleHierarchyService 承擔、僅用於回應展開(FU-29) | 授權層單一語意(只走 hasAuthority,Role.json 攤平階層);前端 guard 拿到展開後的扁平 roles 陣列,與 MSW mock 一致 |
| 2026-07 | Account 降為應用宣告的 seam 實體(ADR-018) | canonical 出 AccountBase,具體 Account 由各專案擁有,正當客製不再與同步互斥 |
| 2026-07-20 | 管理面收編為 optional auth-admin feature(ADR-020) | fleet 兩家下游各自重造帳號/角色管理端點;service canonical+controller seam、政策 SPI 隔離中性、參考 Account 開 @Version opt-in |
| 2026-08-14 | 採用風險導向 capability + object scope + Audit(ADR-031) | Authority 不再依 Entity × CRUD 膨脹;公開狀態、資料範圍與 seed 撤權有可驗證契約 |
| 2026-08-14 | 非敏感端點改採 Authenticated + Audit | Authority 只保護敏感資訊與操作,以事後稽核降低權限數量與管理複雜度 |
文檔維護者: Development Team + AI Assistant 最後審閱: 2026-08-14