---
title: "存取管理 — 設計文件 (FR-132)"
brand: "Guidant AI · **FR-132** 存取管理"
eyebrow: "FR-132 · Access Management — 設計文件 · 2026-10-06"
h1: "存取管理：設計定案"
subtitle: "誰 × 角色 × 範圍 × 時效，公司層補完"
lede: "把公司層權限補成 Azure 式的四段模型：**三段式能力點＋萬用字元角色**、**部門範圍與起訖日真的進判定**、**33 支漏網 API 補守門**、**存取管理前端四頁**、**角色 JSON 匯出匯入**，外加一個預設關閉的**部門資料隔離開關**。D1–D12 已定案；專案層成員角色合併是 FR-B，另開。"
chips: [
  {text: "D1–D12 定案 2026-10-06", kind: ok},
  {text: "拆分 5 子需求 × 31 子任務", kind: accent},
  {text: "母案 CM-2510", kind: ok},
  {text: "守門補齊＝客戶可感的行為變更", kind: crit}
]
footer: "FR-132 · 存取管理 — 設計文件 · 2026-10-06 · 討論稿 discussion.md · 盤點 inventory/ 三份 · 前作 FR-048 統一授權守門、FR-062 License 控管、FR-069 jedi-iam"
---

> 狀態：**設計定案**（D1–D12 已拍板）｜日期：2026-10-06｜討論稿（含全部流程圖與現況細節）：[`discussion.html`](./discussion.html)｜母案：[CM-2510](https://app.notion.com/p/FR-132-5-31-3f1346da4cd08122b73dff2ed690ec32)（子卡 CM-2511～CM-2541）
> 前作：FR-048 統一授權守門（`common/authz/`）、FR-062 License 控管（授權照模組對照）、FR-069 jedi-iam 套件化

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-06 | 初版設計定案：D1–D12 全數拍板；部門範圍採「套在資料上」的判法（使用者所屬部門不參與判定）；`is_admin` 角色轉換規則定案 | FR-132 母案 CM-2510 |

## 需求背景與端到端流程 {#why nav="背景"}

### 2.1 要解決什麼

Guidant AI 的公司層權限是 **RBAC**（Role-Based Access Control，以角色為單位授權）：管理員建角色、在角色上勾**能力點**（系統裡「可以做某件事」的最小單位，例如 `project.read`），再把角色指派給使用者；API 用 `require_capability(...)` 擋人，判定在 jedi-iam 套件（帳號與權限套件）。骨架都在，但有四個地方沒接上：

| 缺口 | 白話 | 證據 |
|---|---|---|
| 部門範圍只存不判 | 指派表有部門欄，判定只比租戶——指到某部門的角色在整個租戶都有效，且無任何錯誤訊息 | jedi-iam `infra/repository/user_role_repo_impl.py:81-101`；FE 存部門指派時同時帶租戶 id（`UserMTRBACForm.vue:216-223`） |
| 起訖日判了但設不了 | 判定有看指派的起訖日，畫面只開了帳號層的生效／到期日 | `infra/repository/active_role_conditions.py:19-39`；FE `UserMTRBACForm.vue:489,501` |
| 33 支 API 只驗登入 | 公司沒辦法說「稽核部的人不准碰 SSP 匯出」 | 盤點 ① 第 3 節 J 組 |
| 角色只能逐筆勾 | 123 個能力點、三套命名寫法，沒有「整個模組」「全部唯讀」的寫法，也不能帶到別的環境 | 盤點 ① 第 4、6 節 |

另有兩個資安漏洞：選單明細與 **JWT**（登入後隨每個請求送出的身分憑證）裡的 `is_admin` **claim**（憑證內的一個欄位）不過濾已到期、已停用的指派（`app/service/user_service.py:522-529,547`、`middleware/jwt_mw.py:79`），會讓過期管理員仍看得到管理頁入口。

資料隔離今天只到租戶：資料庫用 **RLS**（Row-Level Security，資料庫列層級隔離）擋別租戶的資料，43 張表帶 `org_unit_id` 但沒有一條讀取 **policy**（RLS 的過濾規則）看它。要不要讓部門也擋資料，由 D12 做成租戶開關。

### 2.2 端到端流程

```{.mermaid cap="圖 1 — 從定義角色到一次請求放行的全流程"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
  subgraph DEF["管理員在「存取管理」設定"]
    R1["角色：一組 pattern<br/>例 grc.project.* 、 *.*.read"]
    R2["指派：誰 × 角色 × 範圍（租戶／部門）× 起訖"]
    R3["部門隔離開關（預設關）"]
  end
  subgraph REQ["一次 API 請求"]
    Q1["require_capability('grc.project.update')"] --> Q2{"super_admin？"}
    Q2 -- 是 --> OK["放行"]
    Q2 -- 否 --> Q3["取有效指派<br/>（起訖、啟用、租戶）"]
    Q3 --> Q4["展開 pattern 成能力點集合<br/>（Redis 快取，鍵含清單版本號）"]
    Q4 --> Q5{"集合內有這個能力點？"}
    Q5 -- 否 --> NO["403"]
    Q5 -- 是 --> Q6{"本租戶開了部門隔離？"}
    Q6 -- 否 --> OK
    Q6 -- 是 --> Q7["RLS 只回指派範圍內部門的資料<br/>單筆寫入另比「給這個能力點的指派」涵不涵蓋該筆部門"]
    Q7 --> OK
  end
  R1 --> Q4
  R2 --> Q3
  R3 --> Q6
  CHK["「檢查存取」頁"] -.->|吃同一份展開結果| Q4
  MENU["選單、選單明細、is_admin claim"] -.->|吃同一份展開結果| Q4
```

## 分工概述（30 秒版） {#overview nav="分工概述"}

**一句話**：先把能力點清單理乾淨定規則（A.1），再改資料表（A.2）與判定邏輯（A.3），最後同時補守門（A.4）和做新畫面（A.5）。

| 階段 | 做什麼（白話） | 產出 | 完成怎麼判定（決策者檢查法） | 依賴 |
|---|---|---|---|---|
| **A.1 盤點與規則** | 每支 API（含外部套件自帶的）對到一個三段式能力點；定模組清單、整棵能力點樹、出貨角色內容、D12 要改的表 | 能力點對照表、模組定稿表、出貨角色 JSON 草稿、D12 表清單 | 打開對照表：每一支要登入的 route 都有一列、每列都有新名字；出貨角色 JSON 看得到 `*.*.read` 這類寫法 | 無 |
| **A.2 資料地基** | 能力點改名並補欄；角色改存萬用字元；部門路徑回填；隔離開關欄；出貨角色改 JSON | migration、JSON seed、真實舊庫升級排練紀錄 | 拿一份客戶舊庫升級，用舊帳號登入：選單與能按的按鈕和升級前**一模一樣**；排練紀錄裡每個角色的能力點集合逐角色比對一致 | A.1 |
| **A.3 判定層** | jedi-iam 學會展開萬用字元、吃部門範圍、加 Redis 快取；選單、明細、`is_admin` 改吃同一份結果；D12 的 RLS 機制落地但開關預設關 | jedi-iam 新版、判定 API、D12 policy | 給某人一筆「A 部門、明天到期」的指派：開關關時他在整個租戶都能用；打開開關後 B 部門的專案看不到也改不了；把到期日改成昨天，重新登入就不能用 | A.2 |
| **A.4 守門補齊** | 33 支漏網 route 掛能力點；第一版只記錄不擋，下一版才真擋 | route 掛點、「本來會被擋」的 log 與統計 | 一般帳號點完全部頁面：第一版畫面完全沒變、log 看得到「若真擋會擋到誰」；下一版同一動作回 403 | A.3 |
| **A.5 存取管理前端** | 新模組四頁（角色、指派、檢查存取、使用者）＋部門隔離開關頁 | 新頁取代 `/auth/role-manage`、`/auth/user-manage` 等四條路由 | 在「檢查存取」選一個人，看得到他每個能力點從哪個角色、哪條 pattern、哪個範圍來、何時到期 | A.3 |

A.1 → A.2 → A.3 必須照順序；A.4 和 A.5 可以平行。版本落點：版本 1 出 A.1～A.3（客戶無感），版本 2 出 A.4 只記錄版＋A.5，版本 3 出 A.4 真擋。

## 決策定案 D1–D12 {#decisions nav="決策"}

| # | 題目 | 定案 | 理由 | 被排除方案 |
|---|---|---|---|---|
| **D1** | 模組命名 | 一個「存取管理」模組，四頁：角色、指派、檢查存取、使用者 | 四頁概念上是一組；名稱對得上 Azure 的「存取控制」，客戶 IT 一看就懂 | 保留「角色管理」「使用者管理」兩個舊頁各自加功能——檢查存取與指派沒有家 |
| **D2** | pattern 怎麼存 | 新表 `role_capability_patterns(role_id, pattern)`；`role_capabilities` 並存一版後退役 | 並存期間升級出問題可退回舊版程式讀舊表，回滾不必動資料 | `role_capabilities` 直接改欄——回滾要動資料 |
| **D3** | 快取失效 | 快取鍵含能力點清單版本號（新增或改名能力點時加一）＋角色改動時主動清該租戶快取；快取放 Redis | 權限的錯誤方向是「拔了還有效」，靠時間到期在稽核上說不過去；多台 api／worker 行程時只有共用快取能一致清除 | 純 TTL 到期；各行程記憶體快取 |
| **D4** | 部門繼承方向 | 上層部門的指派涵蓋所有子部門 | 一般人對部門的直覺，也是 Azure 做法；要嚴格限定就直接指派到子部門 | 嚴格只在該部門本身——部門一多，總公司主管要指派幾十次 |
| **D5** | 只記錄觀察期 | 一個 release；DEV＋STG 跑滿一週、log 無「非管理員會被擋」紀錄才真擋，有紀錄就逐筆判斷補勾或改掛點 | 客戶一般角色是客戶自己勾的，我們不知道內容；先觀察才不會上線就接到「某某人不能用」 | 直接真擋 |
| **D6** | 出貨角色 | 系統管理員 `*.*.*`：內建、不可刪不可改、每租戶必有；稽核主管／稽核員／唯讀（`*.*.read`）：出貨範例，可改可刪可複製 | 系統管理員是「至少有一個人能進來修權限」的保證；其餘只是起點，各家編制不同，硬鎖只會逼客戶複製近似角色 | 四個全鎖；全部可刪 |
| **D7** | `is_admin` 旗標 | 保留；判斷管理員只看旗標，不看是否持有 `*.*.*` | JWT claim、FE `requiresAdmin`、新租戶建角色、升級腳本都讀旗標；用「持有某能力點」反推管理員已實際踩過坑（升級腳本把非管理員塞滿權限） | 拿掉旗標改看 `*.*.*` |
| **D8** | 選單同源 | 選單可見性、選單明細、JWT `is_admin` claim、判定四者吃同一份「有效指派＋展開結果」，併 A.3 | 同時修掉兩個資安漏洞；各自展開必漏改一條，症狀是「選單看得到、點進去 403」且不報錯 | 各自改 |
| **D9** | A.1 切棒 | 按模組分五份平行盤點，首腦合併定樹並統一裁命名 | 61 支 BE route 檔＋外部套件端點＋123 個能力點，一份全掃 context 會爆；命名統一裁才不會每份一套慣例 | 一份全掃；各份各自定名 |
| **D10** | license 對應 | `capabilities` 加 `license_module` 欄存舊 `resource_type` 原值；license 判定與寫入端守門改讀它 | 授權照已簽出在客戶手上不能重簽；`resource_type` 今後是三段名中間段，繼續兼 license 鍵，有人改資源名就會讓整個模組在客戶端被判「沒買」 | 繼續拿 `resource_type` 當 license 鍵；重簽授權照 |
| **D11** | 舊名過渡 | 能力點表加舊名欄，判定一版內同時接受新舊名；守衛測試掃乾淨下一版才移除 | BE 約 116 處、jedi 套件 11 支檔、FE 約 14 支檔寫死舊名，跨套件跨 repo 一次改完任何一處漏改就是靜默 403 | 一次切換 |
| **D12** | 部門資料隔離 | 做機制＋租戶開關 `org_unit_isolation`，預設關；只改挑選的業務表；開啟前先出預覽清單 | 有客戶需要部門互不相見，但多數客戶的部門只是組織標記；預設關升級無感；RLS 擋掉的資料不報錯，預覽讓影響在開之前看得到 | 不做（需要的客戶無解）；全面開（升級即改行為）；改到 43 張全部（維護面過大、多數無業務意義） |

**部門範圍的判法（D4／D12 的落地方式）**：範圍套在**資料**上——一筆掛在部門 X 的指派，在使用者碰的資料列 `org_unit_id` 落在 X 子樹內時啟用；使用者本人所屬部門（`user_org_units`）**不參與判定**。被排除的判法是「拿使用者所屬部門與指派部門互比」：那會讓一個隸屬稽核部、被指派去管財務部專案的人完全用不到那筆指派，且兩種部門混在一起，管理員無法從指派畫面推出結果。

**明確不做**：拒絕規則（deny）、資料層動作（dataActions）、角色可指派範圍限制（assignableScopes）、IP／時段條件、政策語言、資源實例級統一授權表——理由見討論稿〈已定案事項〉。

## 現況接入點盤點 {#inventory nav="現況盤點"}

jedi-iam 路徑以 `jedi-iam/jedi_iam/` 為根，FE 以 `compliance-manager-fe/src/` 為根。完整證據見 [盤點 ①](inventory/01-authz-capabilities.md)、[盤點 ②](inventory/02-project-roles-and-flow.md)、[盤點 ③](inventory/03-frontend.md)。

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `capabilities` 表 | name／resource_type／action／is_platform；123 筆、37 種 resource_type、三套命名（`infra/models/capability.py:15-27`） | 改名三段式；加 `module`、`display_name`、`sort`、`assignable_scopes`、`license_module`、`legacy_name` |
| `role_capabilities` 表 | role_id＋capability_id，無 pattern 欄（`role_capability.py:14-23`） | 並存一版；新表 `role_capability_patterns` 為判定來源 |
| `user_roles` 表 | 已有 `org_unit_id`、`scope`、`starts_at`、`ends_at`（`user_role.py:20-84`） | 結構不改；部門欄開始進判定，起訖開到畫面 |
| `roles` 表 | `is_admin` int（`role.py:23-84`） | 加 `is_system`、`is_template` |
| `org_units.path` | 例 `/10/12/`＋索引 `(tenant_id, path)`（`infra/models/org_unit.py:17,32`） | 掃一次回填保證格式；子樹比對的基礎 |
| 有效指派條件 | `active_user_role_conditions` 判起訖、啟用、未刪除、租戶（`active_role_conditions.py:19-39`） | 沿用；取回時帶指派部門 path |
| 判定 `get_active_capability_names` | 只比 `tenant_id`，不看部門（`user_role_repo_impl.py:73-101`） | 改讀 pattern 並展開；部門指派納入 |
| `CapabilityGuard` | 單一請求快取於 `flask.g`、super_admin 放行（`authz/capability.py:48,51-66,99-106,115,135`） | 對外介面不變；加 Redis 跨請求快取、只記錄模式、`require_capability_on` |
| 選單可見性 | 有套有效條件（`ui_route_repo_impl.py:70-141`） | 改吃展開服務 |
| 選單明細 | 不過濾有效指派（`user_service.py:522-529,547`） | 改吃展開服務（資安修正） |
| JWT `is_admin` claim | 不過濾有效指派（`middleware/jwt_mw.py:79`） | 只看有效指派中的 `is_admin` 角色（資安修正） |
| RLS session 變數 | 注入 `app.allowed_tenant_paths`、`app.allowed_org_paths`（後者是所屬部門）（jedi-common `session/database/db.py:205-209`、jedi-iam `middleware/context.py:119-129`） | 新增 `app.allowed_org_unit_paths`（指派部門）與隔離開關變數 |
| 背景流程放行旗標 | `app.can_read_all_orgs`（`app/flow_engine/service/workflow_execution_service.py:581,706`） | D12 policy 必須尊重 |
| license 判定 | 讀 `resource_type`（`common/authz/license.py:79-80,283-297`）；豁免清單 `INFRASTRUCTURE_MODULES`（`:107-121`）；寫入端守門（`app/auth/service/role_app_service.py:54-91`） | 改讀 `license_module` |
| 出貨 seed | 只有 `Administrator`（id=2，`scripts/init/04-seed-core.sql:62`），`:272` 起 123 列逐筆配；名稱反查 id（`scripts/init/README.md:57-66`） | 改成 JSON seed：系統管理員一列 `*.*.*`＋三個範例 |
| 新租戶建角色 | `System Manager` 配全集扣排除清單（`tenant_provisioning_service.py:49,149`；排除清單 `core/plugins/identity.py:89-95`） | 改建系統管理員 `*.*.*`＋三個範例 |
| 既有租戶回補模板 | `scripts/init/migrate-capability-grants.sql` | 改寫成 pattern |
| 角色端點 | 只有 5 個 CRUD／狀態切換（jedi-iam `api/routing.py:107-111`） | 加 JSON 匯出匯入、檢查存取、指派 CRUD |
| FE 角色矩陣 | RoleForm 內嵌 TreeTable 三態（`views/role-manage/RoleForm.vue:456-516`，CSS `:531-605`） | 抽成可重用元件，列改成「模組 → 資源」 |
| FE 指派 | 扁平部門下拉、無每筆起訖（`UserMTRBACForm.vue:548-578,589-650`） | 移到「指派」頁，部門樹＋日期區間 |
| FE 能力點判定 | `hasCap` 約 13 支檔（`utils/userUtil.js:24-42`）；路由守衛只有 `requiresAdmin`、`requiresPlatformAdmin`（`config/router/index.js:1057-1100`） | 舊名改新名；守衛行為不變 |
| FE 通用元件 | 無部門樹、日期區間、矩陣、轉移清單（盤點 ③ 第 6 節） | A.5 新建 |
| 能力點補配舊角色範例 | `scripts/sql/2026-09-25-fr114-module-frame-read-capability-backfill.sql` | A.2 migration 參考寫法 |

## 詳細設計 {#detail nav="詳細設計"}

### 6.1 能力點命名與模組

**格式**：`<模組>.<資源>.<動詞>`，三段小寫，資源名內部用連字號。例：`grc.project.approve`、`oscal.ssp.export`、`system.ldap-config.update`。前兩段就是角色矩陣的兩層分組，**不另建群組表**。

**粒度規則**：

| 規則 | 說明 | 例 |
|---|---|---|
| 每個資源固定 read／create／update／delete | 四個基本動詞一律有 | `grc.project.read` … `.delete` |
| 獨立動詞只給「給了 update 卻不想一起給」的 | 限四類：狀態轉換（approve／close／publish）、對外送出（export／send）、指派（assign）、匯入（import） | `oscal.ssp.export` |
| 一個資源一個 read | 不拆列表與明細 | `oscal.ssp.read` |
| 跨資源動作歸被產出的資源 | 從 SSP 產生 AP，算 AP 的 create | `grc.assessment-plan.create` |
| 純查詢輔助端點不開能力點 | 下拉、menu 類跟主資源的 read | 專案下拉跟 `grc.project.read` |

**情境判準（2026-10-06 A.1 合併定案）**：

- **通則 A：在任務／專案情境裡做的事，看任務／專案的權限。** 功能模組的能力點只管該模組自己的管理頁。判準是使用者在哪個頁面按的，不是程式住在哪個套件。授權照與能力點是兩層，模組加值包硬規則管的是能力點，不影響此通則。例：任務頁開始掃描＝`grc.job.update`（非 detection）；任務頁填問卷＝`grc.job.update`（非 survey）；專案頁建雲端資料夾＝`grc.project.update`。
- **通則 B：挑選用的下拉／名單，看呼叫頁面的權限，被挑模組的 read 只管它自己的管理頁。** 例：建專案選流程範本＝`grc.project.create` 放行；專案／SSP 頁挑設備、資訊系統＝該頁權限；挑人挑部門下拉豁免。判準：回的是「挑選用的名單」還是「那個東西的內容本體」——後者走通則 C。
- **通則 C：一支 API 服務多種資源時，先解析資源歸屬，再檢查對應的單一權限；不開「擇一放行」。** 例：流程定義端點先查 BPMN 屬專案或資源庫。資源域守門放 service 層（與 6.3 一致）。

原廠層（`is_platform`）端點一律兩道都過：平台管理員判定保留，再加能力點檢查。

**模組判準**：客戶會不會把這群功能當一組來授權。模組不是 UI 選單（選單會重排，能力點名是對外契約——授權照、角色 JSON、客戶已勾好的角色都綁著它）；也不照程式住在哪個套件分。

**硬規則（模組邊界與授權照加值包對齊）**：一個加值包必須整包落在單一模組內，不可橫跨兩個模組；模組可以比加值包大。加值包見 `docs/features/FR-062-2608-license-management/design.md` 第 6 節：問卷（`survey`）、檢測三合一（`remote-agent-manage`、`detection-profile`、`plugin`）、雲端整合（`cloud_integration`）、AI 儀表板（`ai-dashboard`）。不受授權照管制的基礎設施七項見 `common/authz/license.py:107-121`。

**模組定稿（A.1 合併，2026-10-06）**：

| 模組 | 白話說明 | 主要資源 |
|---|---|---|
| `grc` | 專案稽核：專案、任務（含檢測執行、問卷填答）、稽核、流程執行、摘要報告 | project、job、job-comment、job-evidence、audit、summary-report |
| `oscal` | 合規框架、資源庫與專案 SSP | framework、resource-library、ssp |
| `flow` | 流程範本設計（全租戶共用設計資產，獨立授權） | template |
| `detection` | 檢測工具：遠端代理、外掛、基準、掃描網段 | remote-agent、plugin、profile、scan-zone |
| `evidence` | 證據：雲端整合與 AI 證據分類 | cloud-integration、classification、classification-profile |
| `survey` | 問卷設計（填答歸 grc.job） | survey |
| `system` | 系統設定：信件、LDAP、安全政策、儲存、授權之外的機房設定 | smtp-config、ldap-config、notify-config、security-policy、log-forwarding、system-config、storage-config、issue-integrate-config |
| `iam` | 身分與存取：使用者、角色、部門、租戶 | user、role、org-unit、tenant |
| `platform` | 原廠層（全部 `is_platform`，兩道守門）：操作紀錄、支援頁、AI 額度上限、授權管理、Drive 應用程式憑證 | api-log、support、ai-quota-ceiling、license、drive-app-credential |
| `asset` | 資產：設備與資訊系統 | device、information-system |
| `ai` | AI 相關：儀表板（加值包）、小幫手、呼叫紀錄、租戶自己的額度、AI 服務設定 | dashboard、assistant、call-log、quota、ai-provider-config |
| `workspace` | 全公司共用的協作功能：公告、意見回饋（送自己的）、回饋收集（看全部的） | bulletin、feedback、feedback-review |

已裁結果：`information-system` 歸 `asset`；`storage-config` 歸 `system`；`ai-quota` 拆為 `ai.quota` 與 `platform.ai-quota-ceiling`；`ai-dashboard` 獨立成 `ai` 模組。

### 6.2 資料模型

**`capabilities` 欄位（改後）**：

| 欄位 | 型別 | 說明 | 決策 |
|---|---|---|---|
| `name` | varchar(100) UNIQUE | 三段式新名 | A.1 |
| `module` | varchar(50) NOT NULL | 三段名第一段；CHECK `name LIKE module || '.%'` | 6.1 |
| `resource_type` | varchar(100) | 三段名第二段（新資源名） | 6.1 |
| `action` | varchar(50) | 動詞 | 6.1 |
| `display_name` | varchar(200) | i18n key，矩陣顯示用 | A.5 |
| `sort` | int | 矩陣內排序 | A.5 |
| `assignable_scopes` | text[] | 值域 `tenant`／`org_unit`／`project`（`project` 為 FR-B 預留） | FR-B 預留 |
| `license_module` | varchar(100) | 舊 `resource_type` 原值，license 判定專用 | D10 |
| `legacy_name` | varchar(100) | 舊名，判定一版內同時接受；守衛測試掃乾淨後下一版移除 | D11 |
| `is_platform` | bool | 不變 | — |

**新表與加欄**：

| 表 | 欄位 | 說明 | 決策 |
|---|---|---|---|
| `role_capability_patterns`（新） | `role_id` FK CASCADE、`pattern` varchar(100)，主鍵 (role_id, pattern) | 角色存的 pattern；一個完整名本身也是 pattern | D2 |
| `roles` | `is_system` bool、`is_template` bool | 內建（不可刪改）／出貨範例 | D6 |
| 能力點清單版本號 | 整數，新增或改名能力點時加一 | 快取鍵一部分；放 `system_configs` 一列或獨立小表，A.2 定 | D3 |
| 租戶設定 `org_unit_isolation` | bool 預設 false | 部門隔離開關；放 `tenants` 加欄或 `system_configs`，A.2 依 RLS 讀取成本定 | D12 |

新表照 SQL migration 鐵則：每語句加日期註解、`GRANT ... TO cm_app`、收尾 `INSERT public.schema_migrations`。

**pattern 語法**：三段，每段是名字或 `*`；`*` 只能整段，不支援 `grc.proj*`。合法例：`*.*.*`、`grc.*.*`、`*.*.read`、`grc.project.*`、`grc.project.read`。寫入時驗格式，不合法回 400。

**seed**：`scripts/init/04-seed-core.sql` 的 123 列 `role_capabilities` 改為系統管理員一列 `*.*.*`＋三個範例角色；出貨角色內容以 JSON 檔為來源（`scripts/init/` 下新增，A.2 定路徑），seed 由 JSON 產生。

**migration 自檢與升級相容**：

1. **行為零變化是硬條件**：每筆既有 `role_capabilities` 轉成一筆完整名 pattern；migration 結尾逐角色比對「轉換前能力點集合」與「新表展開後集合」，任一角色不一致整支 rollback（`psql --single-transaction -v ON_ERROR_STOP=1`）。
2. **`is_admin` 角色轉換規則**：租戶內「能力點＝該租戶全集」的 `is_admin` 角色轉內建系統管理員 `*.*.*`（設 `is_system`）；同一租戶有多個時取最早建的那個。其餘 `is_admin` 角色保留旗標，能力點原樣轉完整名，**不升級**。三環境實查：DEV、STG、190 的 `is_admin` 角色皆每租戶有一個配滿者（STG 的 121／101 為版本落後兩個能力點，升級會補）。
3. **`*.*.*` 展開範圍**：非 root 租戶展開時扣 `is_platform` 能力點與 `TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES`（`core/plugins/identity.py:89-95`），與新租戶 `System Manager` 拿到的集合一致（DEV：123 − 16 − 4 ＝ 103）。授權照另由第六軸判，不在展開時扣。
4. **改名的三種消費者**：BE `require_capability`／`viewer_has_capability` 約 116 處、jedi 套件 11 支檔（jedi_iam 4、jedi_issue 3、jedi_ai_dashboard 2、jedi_detection 1、jedi_system_core 1）、FE `hasCap` 約 14 支檔——一版內新舊名並存（D11）。
5. **全部是加欄、加表、改資料**，沒有刪欄、改型別；帶舊資料升級不會因 schema 失敗，會出事只會是資料改錯，靠第 1 點擋。
6. 所有 migration 落地後回報「出貨基線待重產」（`scripts/init/02-schema.sql` 與 seed），重產由決策者裁示。

### 6.3 判定層

判定仍在 jedi-iam `CapabilityGuard`，`require_capability("grc.project.read")` 對外介面不變，內部三步：

1. **取有效指派**：沿用 `active_user_role_conditions`，回傳每筆指派的角色 id 與指派部門 path（租戶指派為空）。
2. **展開 pattern**：角色 pattern × 能力點清單 → 具體能力點集合。展開只跟「角色有哪些 pattern」與「清單有哪些名字」有關、跟使用者無關，可跨請求快取。
3. **比對**：要求的能力點在不在集合內（新舊名皆認，D11）。

**快取**：

| 層 | 鍵 | 內容 | 失效 |
|---|---|---|---|
| Redis | `authz:exp:{tenant_id}:{catalog_version}:{role_id}` | 該角色展開後的能力點名集合 | 角色 pattern 改動、角色停用／刪除 → 刪 `authz:exp:{tenant_id}:*`；能力點新增或改名 → 版本號加一，舊鍵自然不命中 |
| `flask.g` | `_authz_capability_names` | 本請求的合併集合 | 請求結束 |

🔴 **Redis 讀不到時直接查 DB 展開，不可放行也不可回 403**——快取壞掉不能變成權限放寬或全站 403。指派的起訖、啟用每次都查 DB（不進 Redis），所以到期當下就失效，不受快取影響。

**部門範圍（範圍套在資料上）**：角色不分部門，指派才分部門——角色定義全租戶共用，`user_roles.org_unit_id` 是這筆指派的範圍。使用者所屬部門（`user_org_units`）不參與。

| 層 | 回答什麼 | D12 關 | D12 開 |
|---|---|---|---|
| `require_capability(cap)`（route 層） | 能不能做這類動作 | 任一有效指派（租戶或部門）給了就放行 | 同左 |
| RLS（資料可見性） | 看得到哪幾筆 | 只看租戶，與今天相同 | 資料列 `org_unit_id` 為空、或落在任一有效部門指派的子樹內、或此人有有效租戶指派，才看得到 |
| `require_capability_on(cap, org_unit_id)`（單筆寫入） | 對這一筆能不能做 | 等同 `require_capability` | 要求「給了這個能力點的那筆指派」範圍涵蓋該筆資料的部門（租戶指派一律涵蓋） |

第三層的必要性：某人在 A 部門是編輯、在 B 部門是唯讀，RLS 讓他兩邊都看得到，route 層也會因 A 那筆放行 update；只有比對「哪一筆指派給的」才擋得住他改 B 的資料。第三層只掛在 D12 挑選的業務表寫入路徑（6.4）。

**同源四條查詢（D8）**：

| 消費者 | 座標 | 改成 |
|---|---|---|
| 判定 | `user_role_repo_impl.py:73-101` | 展開服務 |
| 選單可見性 | `ui_route_repo_impl.py:70-141` | 展開服務的結果比對 `route_capabilities` |
| 選單明細 | `user_service.py:522-529,547` | 展開服務（修資安漏洞：不再走 `user.roles → role.capabilities`） |
| JWT `is_admin` claim | `middleware/jwt_mw.py:79` | 只看有效指派中的角色旗標（修資安漏洞） |

FE 路由守衛 `requiresAdmin` 吃 claim，claim 修正後自動跟著正確（FE `config/router/index.js:1057-1066`）。

### 6.4 部門資料隔離（D12）

```{.mermaid cap="圖 2 — 開了部門隔離後，一筆資料看不看得到"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
  Q["讀一筆業務資料"] --> T{"租戶允許？"}
  T -- 否 --> NO["看不到"]
  T -- 是 --> S{"本租戶開了部門隔離？"}
  S -- 否 --> YES["看得到（與今天相同）"]
  S -- 是 --> B{"can_read_all_orgs 或有有效租戶指派？"}
  B -- 是 --> YES
  B -- 否 --> N{"該列 org_unit_id 為空？"}
  N -- 是 --> YES
  N -- 否 --> P{"該列部門 path 落在<br/>有效部門指派的子樹內？"}
  P -- 是 --> YES
  P -- 否 --> NO
```

**session 變數**（照 jedi-common `session/database/db.py:205-209` 注入 `app.allowed_tenant_paths` 的做法）：

| 變數 | 值 | 誰讀 |
|---|---|---|
| `app.allowed_org_unit_paths` | 有效**部門指派**的部門 path 清單；有有效租戶指派、super_admin 或系統管理員時為全部門可見標記 | D12 policy |
| `app.org_unit_isolation` | 本租戶開關值，開 session 時從租戶設定讀出 | D12 policy（不在每列查設定表） |
| `app.allowed_org_paths`（既有） | **所屬部門** path，今天只給 INSERT 檢查用 | 既有 INSERT policy，不變 |

🔴 `app.allowed_org_unit_paths` 與既有 `app.allowed_org_paths` 名字相近、意思不同（指派部門 vs 所屬部門），兩個變數注入處都要寫一行註解寫明差別。

**policy 形狀**（挑選表的 SELECT／UPDATE／DELETE）：租戶允許 且（本租戶未開隔離 或 `app.can_read_all_orgs` 為真 或 該列 `org_unit_id` 為空 或 其 path 落在 `app.allowed_org_unit_paths` 子樹內）。`can_read_all_orgs` 是背景流程的系統放行旗標，漏了它，開隔離的租戶裡排程工作會讀不到資料且不報錯。

**挑選的表**（初估 8～12 張，A.1 定案）：`compliance.projects`、`public.devices`、`compliance.evidence_batches`、`compliance.detection_executions`、`survey.surveys`、`public.bulletins`（要確認與 `bulletin_org_units` 投遞規則不打架）、`compliance.information_systems`、`compliance.module_frames`。不挑工作紀錄、解析任務、子表、設定表；子表靠父表入口間接隔離，A.1 要確認沒有直接列子表的 API 繞過父表。

**前置**：`org_units.path` migration 掃一次回填，保證每列有值且格式 `/id/…/`；path 錯了子樹比對會靜默算錯。

**開啟流程**：管理員按開啟 → 系統產**預覽清單**（每位使用者在每張表會少看到幾筆、誰會整張表空掉）→ 確認後寫入開關並記一筆稽核事件。關閉不需預覽。只有系統管理員能動。

### 6.5 守門補齊

**33 支只驗登入的 route**（`api/` 下，盤點 ① 第 3 節 J 組）：

| 模組 | 支數 | 檔案 | A.4 分工 |
|---|---|---|---|
| oscal（SSP） | 13 | ssp_components、ssp_control_impl_import、ssp_control_implementation、ssp_document_pool、ssp_docx_import、ssp_excel_import、ssp_export、ssp_inventory_items、ssp_leveraged、ssp_party、ssp_resources、ssp_scoped_excel_import、ssp_system_characteristic | T-4.3 |
| oscal（framework） | 3 | framework_parse_job、framework_version_edit、oscal_framework_route | T-4.3 |
| flow_engine | 5 | flow_engine_route、job_evidence、stage_advance、stage_object、stage_rollback | T-4.2 |
| grc（flow_control） | 3 | assessment_plan、job_force_start、job_route | T-4.2 |
| grc（project） | 3 | ap_docx_import、ar_import、audit_round | T-4.2 |
| grc（project_summary_report） | 2 | project_summary_report_route、project_summary_report_history_route | T-4.2 |
| system | 2 | system_config/system_config_route、setup/setup_route | T-4.4 |
| 其他 | 2 | ai_quota/ai_quota_route、support/diagnostic_bundle_route | T-4.4 |

外部套件自帶的 route（jedi-iam `/roles` 等、jedi-survey 端點）不在這 33 支內，由 A.1 一併列入對照表、T-4.4 補掛。`setup_route` 是安裝精靈，安裝當下沒有帳號，A.1 確認是刻意不設守門就登記成豁免清單，不算漏網。SSP、flow_engine、grc 多數在 service 層已有專案成員角色守門，本案補的是租戶 RBAC 這層，兩層並存。

**兩階段上線（D5）**：

| 階段 | 行為 | 客戶感受 |
|---|---|---|
| 版本 2：只記錄 | 判定照跑，結果「不放行」時寫結構化 log（使用者、角色、能力點、route）但仍放行；另出統計 | 無感 |
| 版本 3：真擋 | 不放行就 403 | 沒被授權的一般角色開始被擋 |

只記錄模式以**每個能力點一個旗標**控制（新掛的 33 支標成只記錄，既有守門不受影響），不是全域開關——全域開關一開，既有守門也會被放寬。新掛的能力點靠 `*.*.*` 自動進系統管理員，一般角色由客戶自己勾。

### 6.6 存取管理前端

一個模組（D1），取代 `/auth/role-manage`、`/auth/role-form`、`/auth/user-manage`、`/auth/user-form-mtrbac`（FE `config/router/index.js:93-122`）。

| 頁 | 內容 | 重用／新建 |
|---|---|---|
| **角色** | 矩陣列＝「模組 → 資源」兩層樹、欄＝動詞；另一區列出這個角色實際存的 pattern；系統管理員整列鎖住只能看與複製；範例角色可改刪複製；JSON 匯出匯入；license 旗標與平台層過濾照搬（`RoleForm.vue:41-54,171-220,236-240`） | 矩陣從 `RoleForm.vue:456-516` 抽成元件 |
| **指派** | 一筆＝誰 × 角色 × 範圍（tenant／org_unit）× 起訖；起訖空白＝立即生效、永不到期；三個入口（從人、從角色、從部門）；範圍選部門時說明文字依開關切換——關：「只對該部門與子部門的資料有效；本租戶未開部門隔離時等同全租戶有效」；開：「只看得到、只能操作該部門與子部門的資料」 | 新建部門樹選擇器、日期區間選擇器 |
| **檢查存取** | 選一個人，列出每個有效能力點的來源角色、pattern、範圍、到期日；標出 super_admin、`is_admin`、平台管理員三種豁免；開了隔離時另列「看得到哪些部門的資料」。吃判定層展開服務，畫面＝API 實際放行 | 新頁 |
| **使用者** | 只管帳號本身（基本資料、啟用停用、帳號層生效到期、匯入）；原表單的角色指派欄（`UserMTRBACForm.vue:548-578`）移到指派頁，這裡只留連結 | 由現有頁瘦身 |
| **部門隔離設定** | 開關＋預覽清單＋確認；只有系統管理員能進 | 新頁 |

FE `hasCap` 約 14 支檔的能力點名改新名；版本 2 上線時 BE 已同時接受新舊名，FE 可一次切。

### 6.7 角色 JSON 匯出匯入

```json
{
  "name": "稽核員",
  "description": "執行稽核、可讀全部專案",
  "is_admin": 0,
  "capabilities": ["grc.*.read", "grc.audit-round.update", "oscal.ssp.read"]
}
```

| 規則 | 內容 |
|---|---|
| 用途 | 跨環境搬角色、出貨角色來源（取代 SQL seed）、範例被刪改後還原、稽核證據 |
| 命中檢查 | 每條 pattern 在目標環境至少命中一個能力點；命中零個就整份報錯並列出哪幾條，**不靜默跳過**（跳過會讓匯入後的角色比來源少權限且沒人發現） |
| 識別 | 不帶 id，以名字對應；同名由使用者選覆蓋或另存 |
| 系統管理員 | 匯入檔與內建系統管理員同名時一律另存，不改內建那個 |
| license | 不在 JSON 管；匯入後含未授權模組能力點，走既有寫入端 license 守門（`app/auth/service/role_app_service.py:54-91`，改讀 `license_module`） |

## 拆分（5 子需求 × 31 子任務） {#split nav="拆分"}

依賴鏈：**A.1 → A.2 → A.3 → （A.4 ∥ A.5）**。每卡粒度為單一 session 一到兩天。jedi-iam 開發期走 poetry path dependency，發版等決策者明示；所有 migration 只套 DEV。

### A.1 盤點與規則 — 依賴：無

T-1.1～T-1.5 五份平行；每份產出表欄位固定：route × method → 新三段名 → 現有能力點 → UI 頁，**外部套件自帶的 route 一併列入**。

| 編號 | 卡號 | 做什麼（白話） | 產出 | 驗收條件 | 依賴 | 建議 model |
|---|---|---|---|---|---|---|
| T-1.1 | [CM-2511](https://app.notion.com/p/T-1-1-grc-3f1346da4cd0819485c1f217f0a2d3ac) | 盤 grc 模組：專案、稽核輪次、流程控制、摘要報告、資訊系統的每支 route 對到新三段名 | `inventory/a1-grc.md` | 該模組每支要登入的 route（含 jedi-compliance-audit、jedi-task-platform 端點）都有一列且有新名 | — | opus |
| T-1.2 | [CM-2512](https://app.notion.com/p/T-1-2-oscal-flow-SSP-3f1346da4cd081529086d45862e597b4) | 盤 oscal＋flow：SSP、框架、資源庫、流程設計 | `inventory/a1-oscal-flow.md` | 同上；含 33 支 J 組中 oscal 16 支與 flow_engine 5 支 | — | opus |
| T-1.3 | [CM-2513](https://app.notion.com/p/T-1-3-detection-evidence-survey-agent-3f1346da4cd08103a579d58f8c9a1289) | 盤 detection＋evidence＋survey：設備、agent、檢測基準、插件、掃描網段、雲端整合、儲存設定、問卷 | `inventory/a1-detection-evidence-survey.md` | 同上；檢測三合一加值包整包落在同一模組 | — | opus |
| T-1.4 | [CM-2514](https://app.notion.com/p/T-1-4-system-iam-3f1346da4cd081ee85e8c5d056cb22db) | 盤 system＋iam：系統設定各頁、使用者、角色、部門、租戶；確認 `setup_route` 是否刻意不設守門 | `inventory/a1-system-iam.md` | 同上；含 jedi-iam `/roles` 等端點；`setup_route` 有結論 | — | opus |
| T-1.5 | [CM-2515](https://app.notion.com/p/T-1-5-platform-AI-AI-AI-log-3f1346da4cd0815eaf59ee5d388d065a) | 盤 platform：AI 儀表板、AI 呼叫紀錄、AI 額度、公告、意見回饋、log | `inventory/a1-platform.md` | 同上；標出每個 `is_platform` 能力點 | — | opus |
| T-1.6 | [CM-2516](https://app.notion.com/p/T-1-6-3f1346da4cd081fdb8c3ef4477cc17f9) | 合併五份成一棵能力點樹：統一資源命名與動詞歸屬、模組定稿、9 個孤兒能力點去留、出貨三個範例角色 pattern、D12 挑選表定案；邊界爭議送決策者裁 | `inventory/a1-capability-tree.md`（新名 → 舊名 → license_module → 模組全表）、出貨角色 JSON 草稿、D12 表清單 | 每個現有能力點都有新名或「刪除」結論；加值包未被切開；6.1 列出的四項歸屬爭議有決策者裁示 | T-1.1～1.5 | fable（首腦裁，文件外包） |

### A.2 資料地基 — 依賴：A.1

| 編號 | 卡號 | 做什麼（白話） | 產出 | 驗收條件 | 依賴 | 建議 model |
|---|---|---|---|---|---|---|
| T-2.1 | [CM-2517](https://app.notion.com/p/T-2-1-system-config-read-3f1346da4cd0816d88dec6091a1b9ba9) | `capabilities` 加欄（module、display_name、sort、assignable_scopes、license_module、legacy_name）＋依 A.1 全表改名＋補 `system.system-config.read`＋孤兒處理 | migration（jedi-iam ORM 同步加欄） | DEV 套用後每列 name 符合三段式、module 與 name 前綴一致、license_module 等於舊 resource_type | T-1.6 | opus |
| T-2.2 | [CM-2518](https://app.notion.com/p/T-2-2-pattern-3f1346da4cd081b0b87fc68bedb8cb3f) | 新表 `role_capability_patterns`；`roles` 加 `is_system`、`is_template`；既有 `role_capabilities` 逐筆轉完整名 pattern；`is_admin` 角色依 6.2 規則轉 `*.*.*`；結尾逐角色集合比對、不一致 rollback | migration | DEV 套用成功；故意改壞一筆轉換時整支 rollback（突變驗證自檢有牙齒） | T-2.1 | fable |
| T-2.3 | [CM-2519](https://app.notion.com/p/T-2-3-3f1346da4cd0816da4dacd9d69275316) | `org_units.path` 掃描回填與格式檢查；租戶 `org_unit_isolation` 開關欄（預設 false）；能力點清單版本號 | migration | 每列 path 非空且格式 `/id/…/`；開關欄存在且全為 false | T-1.6 | sonnet |
| T-2.4 | [CM-2520](https://app.notion.com/p/T-2-4-JSON-seed-3f1346da4cd081e3b574f5e739efa135) | 出貨角色改 JSON 來源：系統管理員＋三範例；改寫 `04-seed-core.sql`、`tenant_provisioning_service.py:49,149`、`migrate-capability-grants.sql` 模板 | JSON seed、改後 seed 與 provisioning | 乾淨新裝後每租戶有一個 `is_system` 系統管理員＋三範例；新建租戶同樣；回報「出貨基線待重產」 | T-2.2 | opus |
| T-2.5 | [CM-2521](https://app.notion.com/p/T-2-5-license-license_module-3f1346da4cd0818a993fc9cacd7e7048) | license 判定改讀 `license_module`：`common/authz/license.py:79-80,107-121,283-297`、`app/auth/service/role_app_service.py:54-91` | 程式改動 | 舊授權照在改名後 DEV 開同樣的模組；未授權模組的能力點仍不能勾進角色 | T-2.1 | sonnet |
| T-2.6 | [CM-2522](https://app.notion.com/p/T-2-6-3f1346da4cd0812cacc2f2d2a58a07db) | 升級排練：拿一份隔一版以上的真實客戶舊庫跑 migrate，逐角色比對升級前後能力點集合；列每租戶選到哪個系統管理員、哪些 `is_admin` 沒轉 | 排練紀錄（`inventory/a2-upgrade-rehearsal.md`） | 逐角色 diff 全部一致（不抽樣）；舊帳號登入選單與按鈕不變 | T-2.1～2.5 | opus |

### A.3 判定層 — 依賴：A.2

| 編號 | 卡號 | 做什麼（白話） | 產出 | 驗收條件 | 依賴 | 建議 model |
|---|---|---|---|---|---|---|
| T-3.1 | [CM-2523](https://app.notion.com/p/T-3-1-jedi-iam-pattern-3f1346da4cd081f78c4be6f3e9ed908a) | jedi-iam pattern 展開服務：pattern 語法驗證、展開、`*.*.*` 依租戶扣平台與排除清單、新舊名皆認 | jedi-iam 展開服務＋單元測試 | `*.*.read`、`grc.*.*`、完整名三類展開正確；非 root 租戶 `*.*.*` 等於 103 個 | T-2.2 | opus |
| T-3.2 | [CM-2524](https://app.notion.com/p/T-3-2-Redis-Redis-DB-3f1346da4cd081b3bc53d2f599d38f13) | Redis 快取：鍵含租戶、清單版本號、角色；角色改動主動清租戶快取；Redis 失敗退回查 DB | 快取層 | 改角色後下一個請求即生效（多行程）；停掉 Redis 判定結果不變、不放行不全擋 | T-3.1 | opus |
| T-3.3 | [CM-2525](https://app.notion.com/p/T-3-3-3f1346da4cd081739f28ebbb460c5ed4) | 判定改讀新表：`get_active_capability_names` 走有效指派＋展開；加守衛測試掃程式內舊能力點名 | jedi-iam 判定、守衛測試 | DEV 全部既有角色判定結果與改前相同；守衛測試列出剩餘舊名 | T-3.1 | opus |
| T-3.4 | [CM-2526](https://app.notion.com/p/T-3-4-3f1346da4cd0812ba904d3b356d2d236) | 同源：選單可見性、選單明細、JWT `is_admin` claim 改吃展開服務（修兩個資安漏洞） | jedi-iam 改動 | 把某管理員指派到期日改成昨天，重新登入後 claim 為假、管理頁入口消失、選單明細不再列能力點 | T-3.3 | opus |
| T-3.5 | [CM-2527](https://app.notion.com/p/T-3-5-require_capability_on-3f1346da4cd0818da1eaf2761ffd6079) | 部門範圍：有效指派帶回指派部門 path；新增 `require_capability_on(cap, org_unit_id)`；D12 關時等同 `require_capability` | jedi-iam guard | A 部門編輯＋B 部門唯讀的帳號，開關開時改 B 部門資料被擋、改 A 放行；開關關時兩邊都放行 | T-3.3 | fable |
| T-3.6 | [CM-2528](https://app.notion.com/p/T-3-6-3f1346da4cd08150a6c7f3b70c3a4f66) | D12 RLS：注入 `app.allowed_org_unit_paths`、`app.org_unit_isolation`；挑選表 SELECT／UPDATE／DELETE policy migration（尊重 `can_read_all_orgs`） | jedi-common／jedi-iam 注入、policy migration | 開關關時所有查詢結果與改前相同；開時只看得到指派範圍內部門資料；背景流程在開隔離租戶仍讀得到 | T-2.3、T-3.5 | fable |
| T-3.7 | [CM-2529](https://app.notion.com/p/T-3-7-JSON-API-3f1346da4cd0810e9352cc1f3feedb24) | 角色 JSON 匯出匯入 API：命中檢查、同名覆蓋或另存、系統管理員不可覆蓋、走 license 守門 | jedi-iam 端點 | 匯入含零命中 pattern 的檔整份報錯並列出；匯入同名系統管理員一律另存 | T-3.1 | sonnet |
| T-3.8 | [CM-2530](https://app.notion.com/p/T-3-8-API-3f1346da4cd08101aa94f9421d292378) | 指派 CRUD（含起訖、部門）、檢查存取（每個能力點的來源角色／pattern／範圍／到期）、隔離開關預覽與切換（含稽核事件）API | jedi-iam 與 BE 端點 | 檢查存取回傳與判定放行逐項一致；預覽清單在 DEV 跑得出每人每表少看筆數 | T-3.4、T-3.6 | opus |

### A.4 守門補齊 — 依賴：A.3

| 編號 | 卡號 | 做什麼（白話） | 產出 | 驗收條件 | 依賴 | 建議 model |
|---|---|---|---|---|---|---|
| T-4.1 | [CM-2531](https://app.notion.com/p/T-4-1-log-3f1346da4cd0812aa8dbcda23762118d) | guard 加「只記錄」模式：每個能力點一個旗標，不放行時寫結構化 log 仍放行；統計查詢 | jedi-iam guard、統計腳本 | 標成只記錄的能力點在無權帳號下放行且有 log；未標的既有守門照擋 | T-3.3 | opus |
| T-4.2 | [CM-2532](https://app.notion.com/p/T-4-2-13-API-3f1346da4cd081e28045de959388a41f) | grc＋flow 13 支 route 掛能力點（flow_engine 5、flow_control 3、project 3、摘要報告 2），標只記錄 | route 掛點 | 每支都有守門；一般帳號點完相關頁面畫面不變、log 有紀錄 | T-4.1 | sonnet |
| T-4.3 | [CM-2533](https://app.notion.com/p/T-4-3-SSP-16-API-3f1346da4cd08141a470e0277bed3116) | oscal 16 支 route 掛能力點（SSP 13、framework 3），標只記錄 | route 掛點 | 同上 | T-4.1 | sonnet |
| T-4.4 | [CM-2534](https://app.notion.com/p/T-4-4-AI-API-3f1346da4cd0810ea445e44b290c5ead) | system 2 支＋其他 2 支＋外部套件 route 掛點；`setup_route` 進豁免清單；D12 挑選表寫入路徑掛 `require_capability_on` | route 掛點、豁免清單 | 每支 route 要嘛有守門要嘛在豁免清單；D12 開時跨部門寫入被擋 | T-4.1、T-3.5 | opus |
| T-4.5 | [CM-2535](https://app.notion.com/p/T-4-5-log-release-note-3f1346da4cd081ff9141c0788029506b) | 觀察期報告與真擋切換：DEV＋STG 一週 log 統計、逐筆判斷補勾或改掛點；版本 3 拿掉只記錄旗標；兩版 release note 文字 | 統計報告、真擋 commit、release note 草稿 | 統計無「非管理員會被擋」或每筆有處理結論；真擋後同一動作回 403 | T-4.2～4.4 | opus |
| T-4.6 | [CM-2542](https://app.notion.com/p/T-4-6-T-1-1-3f1346da4cd0810f9f30c0642738324d) | 補 A.1 盤點發現的十三個守門缺口（grc 四項、公告列表範圍、設定讀取無守門、第三方登入綁定查詢無本人檢查、兩處守錯動詞、問卷兩處無身分檢查、AI 模型清單、解析單詳情、資源庫列表、確認端、流程定義端點） | 守門修補（含套件） | 非成員讀留言回 403；摘要報告受授權模組控制；管理員不能旁路刪專案；流程圖端點退役或有守門 | A.3 | opus |

### A.5 存取管理前端 — 依賴：A.3（T-3.7、T-3.8）

| 編號 | 卡號 | 做什麼（白話） | 產出 | 驗收條件 | 依賴 | 建議 model |
|---|---|---|---|---|---|---|
| T-5.1 | [CM-2536](https://app.notion.com/p/T-5-1-3f1346da4cd081a59994cd47785d315b) | 共用元件：能力點矩陣（從 RoleForm 抽出，列改「模組 → 資源」）、部門樹選擇器、日期區間選擇器 | 三個元件 | 矩陣勾整列存 `grc.*.*`、勾整欄存 `*.*.read`；部門樹可選子部門 | T-3.1 | opus |
| T-5.2 | [CM-2537](https://app.notion.com/p/T-5-2-JSON-3f1346da4cd081e7ac5fd7a67884d872) | 角色頁：矩陣、pattern 清單區、系統管理員鎖定、範例角色、JSON 匯出匯入 | 角色頁 | 系統管理員不能改刪只能複製；匯出再匯入另一環境得到相同角色 | T-5.1、T-3.7 | opus |
| T-5.3 | [CM-2538](https://app.notion.com/p/T-5-3-3f1346da4cd081a0a43fe31ea5f97c62) | 指派頁：三入口、每筆起訖、範圍說明文字依開關切換 | 指派頁 | 設一筆部門指派＋明天到期，檢查存取頁看得到；到期後消失 | T-5.1、T-3.8 | opus |
| T-5.4 | [CM-2539](https://app.notion.com/p/T-5-4-3f1346da4cd0816d967feb33fb49ba71) | 檢查存取頁 | 檢查存取頁 | 選人後每個能力點有來源角色、pattern、範圍、到期；豁免三種有標示 | T-3.8 | opus |
| T-5.5 | [CM-2540](https://app.notion.com/p/T-5-5-3f1346da4cd081be9138d467db23d50e) | 使用者頁瘦身、路由替換、舊四條路由退役、`hasCap` 改新名、i18n | 使用者頁、路由、i18n | 舊網址導到新頁；全站 `hasCap` 無舊名；中英文齊 | T-5.2～5.4 | sonnet |
| T-5.6 | [CM-2541](https://app.notion.com/p/T-5-6-3f1346da4cd08177821bfc40482d3ace) | 部門隔離設定頁：開關、預覽清單、確認 | 設定頁 | 非系統管理員進不去；按開啟先看到預覽，確認後才生效並有稽核事件 | T-3.8 | opus |

## 端到端驗收 {#acceptance nav="端到端驗收"}

| 項目 | 怎麼驗 | 通過條件 |
|---|---|---|
| **升級排練（真實舊庫）** | 拿隔一版以上的真實客戶舊庫跑 migrate（新裝環境永遠會過，不算數） | 逐角色比對能力點集合完全一致；舊帳號登入選單與按鈕不變；排練紀錄列出每租戶選到的系統管理員 |
| **開關關閉的回歸** | 版本 1 升級後跑既有 site-regression | 全綠；資料可見性與今天相同 |
| **D12 開啟** | test repo 另寫案子：開隔離、看不到別部門資料、改不了別部門資料、預覽清單 | 新案全綠 |
| **守門兩階段** | 版本 2 一般帳號點完全站；版本 3 同一動作 | 版本 2 無畫面變化且有 log；版本 3 回 403 |
| **資安漏洞** | 過期管理員重新登入 | 管理頁入口消失、選單明細不列能力點 |

**E2E 連動（test repo）**：190 E2E 環境的 82 筆角色指派全掛在部門上。開關預設關，這些指派照舊等同全租戶有效，既有回歸不壞；D12 開啟後的行為要另寫測試案，不能拿既有案子打開開關硬跑。E2E 屬母案收口動作，arc 完成後到 test repo 統一補。

**release note 要寫的行為變更**：

| 版本 | 寫什麼 |
|---|---|
| 版本 1 | 能力點改用三段式名稱（舊名仍可用一版）；出貨角色改為系統管理員＋三個範例；過期或停用的管理員角色不再顯示管理頁入口 |
| 版本 2 | 新的「存取管理」取代角色管理與使用者管理；可設每筆指派起訖日；可開啟部門資料隔離（預設關）；預告下一版 33 個功能開始檢查能力點，附只記錄期間的統計，請管理員到存取管理補勾 |
| 版本 3 | 33 個功能開始檢查能力點，未授權的一般角色會被擋 |

## FR-B 預覽：專案層合併 {#fr-b nav="FR-B 預覽"}

開始時間由決策者與 PM 討論後定；FR-132 只替它預留 `capabilities.assignable_scopes` 的 `project` 值。

**要做什麼**：把專案成員角色（manager／reviewer／auditor／viewer）收進同一套「誰 × 角色 × 範圍 × 時效」，範圍多一層 project；專案內按鈕改由能力點判定，成員角色變成「一組 pattern 的預設角色」。至少要等 A.1 定出能力點樹，FR-B 的按鈕對照表才寫得出來。

**已知難處**：硬寫角色判斷約 70 處，橫跨主專案與 6 個 jedi 套件（盤點 ② 第 2 節）；五張參與者表同形（第 1 節）；control → group → project 回退規則與只認專案層的精確判法不可互相冒充（`participant_role_service.py:34-85`、`common/authz/project.py:28-90`）；流程綁角色寫在 BPMN XML 的 `main_role`，DEV 有 1150 筆進行中實例各帶一份範本 XML 快照（盤點 ② 第 3、5 節）——翻譯 `main_role` 要連實例 XML 一起改，或在 `_resolve_main_roles`（`app/flow_engine/service/stage_advance_service.py:584-591`）做讀取時轉換。

## 參考座標 {#refs nav="參考"}

- 討論稿：[discussion.md](discussion.md)
- 盤點：[inventory/01-authz-capabilities.md](inventory/01-authz-capabilities.md)、[inventory/02-project-roles-and-flow.md](inventory/02-project-roles-and-flow.md)、[inventory/03-frontend.md](inventory/03-frontend.md)
- 授權守門決策表：`common/authz/__init__.py`
- 統一授權守門設計（FR-048）：`docs/analysis/2026-07-07-unified-auth-guard-design.md`
- 權限與選單系統設計說明書：`docs/system-design/permission/Permission-System-權限與選單系統設計說明書.md`
- license 第六軸與基礎設施豁免：`common/authz/license.py`
- 授權照基礎包／加值包：`docs/features/FR-062-2608-license-management/design.md` 第 6 節
- 新租戶排除清單：`core/plugins/identity.py:89-95`
- SQL migration 規範：`docs/claude/sql-migration-conventions.md`、`sql-migration` skill
