ADR-007: Applet 層級權限模型
狀態
已採用 (Accepted) - 2025-12-20
變更歷史:
- 2025-12-19: 初版 - Role + Authority 雙層模型
- 2025-12-20: 簡化版 - 前端只用 Role,API 層用 R/W/X Authority
- 2025-12-20: 最終版 - 移除 Authority,前端只做 Applet 層級控制
- 2025-12-20: 角色格式統一 - 全系統使用 ROLE_XXX 格式
- 2025-12-20: 角色繼承與扁平化 - 模擬 Spring Security Role Hierarchy
- 2026-08-14: 重申 Role-based UI;login/
/me不投影 Authority,特殊資源能力改用allowedActions/capabilities - 2026-08-14: API 保護依風險分級;非敏感端點使用 Authenticated + Audit,敏感端點才使用 Authority(Server ADR-031)
背景 (Context)
問題陳述
原有的 Role + Authority 設計過於複雜:
- 前端維護 Authority 定義和 Role-to-Authority 映射
- Mock 層需要對每個 API 做權限檢查
- 組件層需要判斷用戶是否有權限執行特定操作
經過評估,決定大幅簡化權限模型,遵循以下原則:
- 前端權限使用 Role:選單、路由、Applet 與一般操作 UI 只使用後端展開的
roles[] - 不消費 Authority:前端不從 login/
/me取得 raw Authority,也不解析 token claim 控制介面 - 後端兜底:實際環境由 Spring Security 依風險提供 API 層保護;非敏感端點採 Authenticated + Audit,敏感端點才加 Authority
- 特殊能力由資源 API 投影:只有 Role 無法表達資源狀態或資料範圍時,回傳 UI 語意的
allowedActions/capabilities
簡化動機
- 前端是參考實作,不應承擔權限控制的責任
- 真正的權限控制由後端 Spring Security 處理
- Role 無法表達的資源狀態與資料範圍,由後端針對該資源計算操作能力,不把全域 Authority 暴露給前端
- Mock 可重現明確 SBE 的
403或資源能力,但不建立第二套正式授權引擎
決策 (Decision)
核心設計
┌─────────────────────────────────────────┐
│ UI 層:Role → 介面 │
│ 「誰可以看到/進入哪個功能」 │
│ - 前端只需要 Role │
│ - RoleGuard 保護路由入口 │
│ - 選單與一般操作 UI 亦只使用 roles[] │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ API 層:後端 Spring Security │
│ 「依風險決定認證、稽核、資料範圍與能力」 │
│ - 前端不做權限判斷 │
│ - 非敏感:Authenticated + Audit │
│ - 敏感:Authority + 適用資料範圍 + Audit │
│ - 前端收到 403 就顯示無權限 │
│ - 必要時由資源 API 回 allowedActions │
└─────────────────────────────────────────┘
Role 定義
全系統統一使用 Spring Security 格式(ROLE_XXX):
| Role | 說明 |
|---|---|
ROLE_ADMIN | 系統管理員 |
ROLE_OWNER | 店主 |
ROLE_MANAGER | 店長 |
ROLE_SALES | 銷售人員 |
ROLE_ACCOUNTANT | 會計 |
ROLE_PURCHASER | 採購 |
ROLE_FLORIST | 花藝師 |
ROLE_DELIVERY | 配送員 |
角色繼承(Role Hierarchy)
後端 Spring Security 使用角色繼承機制,前端收到扁平化的 roles 陣列:
ROLE_ADMIN
└── ROLE_OWNER
├── ROLE_MANAGER
│ ├── ROLE_SALES
│ └── ROLE_ACCOUNTANT
├── ROLE_PURCHASER
├── ROLE_FLORIST
└── ROLE_DELIVERY
扁平化 roles 陣列範例:
| 主要角色 | 回傳的 roles 陣列 |
|---|---|
ROLE_ADMIN | [ADMIN, OWNER, MANAGER, SALES, ACCOUNTANT, PURCHASER, FLORIST, DELIVERY] |
ROLE_OWNER | [OWNER, MANAGER, SALES, ACCOUNTANT, PURCHASER, FLORIST, DELIVERY] |
ROLE_MANAGER | [MANAGER, SALES, ACCOUNTANT] |
ROLE_SALES | [SALES] |
ROLE_FLORIST | [FLORIST] |
設計優點:
allowedRoles只需指定「最基本需要的角色」- 例如:訂單管理只需
[ROLE_SALES],店主自動有權限(因為繼承 SALES) - 大幅簡化前端配置
Role → Applet 對應(簡化版)
由於角色繼承,allowedRoles 只需指定最基本的角色:
| Applet | allowedRoles | 說明 |
|---|---|---|
| orders | [SALES] | OWNER/MANAGER 自動有權限 |
| customers | [SALES] | OWNER/MANAGER 自動有權限 |
| products | [PURCHASER, FLORIST] | OWNER 自動有權限 |
| sales | [SALES] | OWNER 自動有權限 |
| design | [FLORIST] | OWNER 自動有權限 |
| delivery | [DELIVERY] | OWNER 自動有權限 |
| reports | [ACCOUNTANT] | OWNER/MANAGER 自動有權限 |
| analytics | [ACCOUNTANT] | OWNER/MANAGER 自動有權限 |
| settings | [ADMIN] | 僅 ADMIN |
共用 Applet(空陣列 = 所有角色可見): home, calendar, messages
實施細節
1. 角色繼承映射
// src/types/auth.ts
export const ROLE_HIERARCHY: Record<RoleName, RoleName[]> = {
[Role.ADMIN]: [Role.ADMIN, Role.OWNER, Role.MANAGER, Role.SALES, Role.ACCOUNTANT, Role.PURCHASER, Role.FLORIST, Role.DELIVERY],
[Role.OWNER]: [Role.OWNER, Role.MANAGER, Role.SALES, Role.ACCOUNTANT, Role.PURCHASER, Role.FLORIST, Role.DELIVERY],
[Role.MANAGER]: [Role.MANAGER, Role.SALES, Role.ACCOUNTANT],
[Role.SALES]: [Role.SALES],
[Role.ACCOUNTANT]: [Role.ACCOUNTANT],
[Role.PURCHASER]: [Role.PURCHASER],
[Role.FLORIST]: [Role.FLORIST],
[Role.DELIVERY]: [Role.DELIVERY],
};
2. 路由守衛(檢查 roles 陣列交集)
// src/routes/role-guard.tsx
// 檢查用戶的 roles 陣列是否與 allowedRoles 有交集
const hasPermission =
allowedRoles.length === 0 ||
currentUser.roles.some((role) => allowedRoles.includes(role));
// 使用範例:只需指定最基本的角色
<RoleGuard allowedRoles={[Role.SALES]}>
<SalesApplet /> {/* OWNER 自動有權限 */}
</RoleGuard>
3. Applet 權限配置(簡化版)
// src/config/applet-registry.ts
import { Role } from '@/types/auth';
{
id: 'orders',
name: 'Order Management',
allowedRoles: [Role.SALES], // 簡化:OWNER/MANAGER 自動有權限
// ...
}
{
id: 'design',
name: 'Design Workbench',
allowedRoles: [Role.FLORIST], // 簡化:OWNER 自動有權限
// ...
}
4. Mock API 回傳扁平化 roles
// src/mocks/handlers/auth.ts
// 登入回應使用 ROLE_HIERARCHY 取得扁平化 roles
const roles = ROLE_HIERARCHY[user.role as RoleName] ?? [user.role];
return HttpResponse.json({
accessToken,
refreshToken,
user: {
...userWithoutPassword,
roles: [...roles], // 扁平化陣列
},
});
Authority 可保留在 Access Token claim,供 Resource Server 建立 Authentication;它不屬於前端 user projection,產品 UI 不得解析該 claim 控制介面。
5. usePermission Hook(僅提供用戶資訊)
// src/hooks/use-permission.ts
const { currentUser } = usePermission();
// 用於 UI 顯示用戶資訊
<span>Role: {currentUser?.role}</span>
6. Mock 認證(僅驗證 token)
// src/mocks/middleware/auth-middleware.ts
export function checkAuth(request: Request): AuthCheckResult {
// 只驗證 token 有效性
// 不做任何權限檢查
}
產品前端不提供的內容
| 項目 | 原因 |
|---|---|
Authority 常數定義 | 前端不需要 |
產品執行期的 ROLE_AUTHORITIES 映射表 | 不允許;同名資料若存在,只能供 mock backend 產生測試用 JWT,產品前端不得匯入或消費 |
通用 hasRole() hook | Role 判斷保持在既有導航/守衛或具名 UI 規則,不建立第二套 permission engine |
通用 hasAnyRole() hook | 同上 |
canAccess() hook | 不需要在組件層檢查 |
AuthorityGuard 組件 | 不需要細粒度權限控制 |
hasAuthority() hook | 前端不需要 |
src/mocks/utils/permissions.ts | Mock backend 依明確場景實作,不建立通用前端 permission engine |
src/utils/role-utils.ts | 統一格式後不需要轉換 |
| 組件層 Authority 配置 | 前端不消費 raw Authority |
決策後果 (Consequences)
正面影響 (Positive)
- ✅ 大幅簡化 - 移除所有 Authority 相關邏輯和格式轉換
- ✅ 職責分離 - 前端只管 UI 入口,權限控制交給後端
- ✅ 維護成本降低 - 不需要維護 Role-to-Authority 映射和格式轉換
- ✅ Mock 邊界清楚 - 只模擬明確場景,不建立完整授權引擎
- ✅ 概念清晰 - 一般 UI 使用 Role;資源特例使用後端計算的 UI 能力
- ✅ 格式統一 - 前端、Mock、後端使用同一格式(ROLE_XXX)
- ✅ 角色繼承 - 模擬 Spring Security Role Hierarchy,allowedRoles 只需指定最基本角色
- ✅ 配置簡化 - 例如訂單管理只需
[SALES],店主自動有權限
設計原則
-
⚠️ 如果某角色只需要部分功能
- 職務流程不同時,設計專屬 Applet(如 Sales Applet、Design Applet、Delivery Applet)
- 同一資源因狀態或資料範圍而不同時,由資源 API 回傳
allowedActions/capabilities
-
⚠️ 前端無法阻止無權限的 API 呼叫
- 解法:這是正確的,權限控制由後端負責
- 前端收到 403 時顯示錯誤訊息即可
保留的內容
| 項目 | 原因 |
|---|---|
RoleName 類型 | RoleGuard 和菜單過濾需要 |
Role 常數 | 便於使用,避免字串硬編碼 |
VALID_ROLES 常數 | 角色驗證需要 |
ROLE_HIERARCHY 常數 | 角色繼承映射,Mock API 使用 |
RoleGuard 組件 | Applet 層級存取控制(使用 roles 陣列交集檢查) |
ProtectedRoute 組件 | 認證檢查 |
menu-config.ts 的 allowedRoles | Applet 層級菜單過濾 |
applet-registry.ts 的 allowedRoles | Applet 層級應用啟動器過濾(簡化版) |
checkAuth 函數 | Mock 認證(token 有效性) |
影響範圍 (Impact)
修改的檔案
| 檔案 | 變更 |
|---|---|
src/types/auth.ts | 新增 ROLE_HIERARCHY 角色繼承映射 |
src/routes/role-guard.tsx | 使用 roles 陣列交集檢查 |
src/config/applet-registry.ts | 簡化 allowedRoles(只需最基本角色) |
src/components/application-launcher/ | 使用 roles 陣列過濾 |
src/mocks/utils/jwt.ts | 使用 ROLE_HIERARCHY 生成扁平化 roles |
src/mocks/handlers/auth.ts | 回傳扁平化 roles 陣列 |
src/features/iam/me-slice.ts | 移除格式轉換邏輯 |
src/hooks/use-permission.ts | 只保留 currentUser |
已刪除的檔案
src/mocks/utils/permissions.ts- 不建立可被產品 UI 消費的通用前端 permission enginesrc/utils/role-utils.ts- 統一格式後不需要轉換src/routes/authority-guard.tsx- 不需要細粒度權限控制
參考資料 (References)
- Spring Security - Authorities
- US-002:角色權限控制(對應的 User Story,文檔路徑待建立)
最後更新: 2026-08-14 決策者: 開發團隊