跳至主要内容

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,特殊資源能力改用 allowedActionscapabilities
  • 2026-08-14: API 保護依風險分級;非敏感端點使用 Authenticated + Audit,敏感端點才使用 Authority(Server ADR-031)

背景 (Context)

問題陳述

原有的 Role + Authority 設計過於複雜:

  1. 前端維護 Authority 定義和 Role-to-Authority 映射
  2. Mock 層需要對每個 API 做權限檢查
  3. 組件層需要判斷用戶是否有權限執行特定操作

經過評估,決定大幅簡化權限模型,遵循以下原則:

  1. 前端權限使用 Role:選單、路由、Applet 與一般操作 UI 只使用後端展開的 roles[]
  2. 不消費 Authority:前端不從 login//me 取得 raw Authority,也不解析 token claim 控制介面
  3. 後端兜底:實際環境由 Spring Security 依風險提供 API 層保護;非敏感端點採 Authenticated + Audit,敏感端點才加 Authority
  4. 特殊能力由資源 API 投影:只有 Role 無法表達資源狀態或資料範圍時,回傳 UI 語意的 allowedActionscapabilities

簡化動機

  • 前端是參考實作,不應承擔權限控制的責任
  • 真正的權限控制由後端 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 只需指定最基本的角色:

AppletallowedRoles說明
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() hookRole 判斷保持在既有導航/守衛或具名 UI 規則,不建立第二套 permission engine
通用 hasAnyRole() hook同上
canAccess() hook不需要在組件層檢查
AuthorityGuard 組件不需要細粒度權限控制
hasAuthority() hook前端不需要
src/mocks/utils/permissions.tsMock 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 回傳 allowedActionscapabilities
  • ⚠️ 前端無法阻止無權限的 API 呼叫

    • 解法:這是正確的,權限控制由後端負責
    • 前端收到 403 時顯示錯誤訊息即可

保留的內容

項目原因
RoleName 類型RoleGuard 和菜單過濾需要
Role 常數便於使用,避免字串硬編碼
VALID_ROLES 常數角色驗證需要
ROLE_HIERARCHY 常數角色繼承映射,Mock API 使用
RoleGuard 組件Applet 層級存取控制(使用 roles 陣列交集檢查)
ProtectedRoute 組件認證檢查
menu-config.tsallowedRolesApplet 層級菜單過濾
applet-registry.tsallowedRolesApplet 層級應用啟動器過濾(簡化版)
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 engine
  • src/utils/role-utils.ts - 統一格式後不需要轉換
  • src/routes/authority-guard.tsx - 不需要細粒度權限控制

參考資料 (References)


最後更新: 2026-08-14 決策者: 開發團隊