FR-132 · Access Management — 設計文件 · 2026-10-06

存取管理:設計定案

誰 × 角色 × 範圍 × 時效,公司層補完

把公司層權限補成 Azure 式的四段模型:三段式能力點+萬用字元角色、部門範圍與起訖日真的進判定、33 支漏網 API 補守門、存取管理前端四頁、角色 JSON 匯出匯入,外加一個預設關閉的部門資料隔離開關。D1–D12 已定案;專案層成員角色合併是 FR-B,另開。

D1–D12 定案 2026-10-06 拆分 5 子需求 × 31 子任務 母案 CM-2510 守門補齊=客戶可感的行為變更

狀態:設計定案(D1–D12 已拍板)|日期:2026-10-06|討論稿(含全部流程圖與現況細節):discussion.html|母案:CM-2510(子卡 CM-2511~CM-2541) 前作:FR-048 統一授權守門(common/authz/)、FR-062 License 控管(授權照模組對照)、FR-069 jedi-iam 套件化

§1

變更紀錄

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

需求背景與端到端流程

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 端到端流程

%%{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
圖 1 — 從定義角色到一次請求放行的全流程
§3

分工概述(30 秒版)

一句話:先把能力點清單理乾淨定規則(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 真擋。

§4

決策定案 D1–D12

# 題目 定案 理由 被排除方案
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/時段條件、政策語言、資源實例級統一授權表——理由見討論稿〈已定案事項〉。

§5

現況接入點盤點

jedi-iam 路徑以 jedi-iam/jedi_iam/ 為根,FE 以 compliance-manager-fe/src/ 為根。完整證據見 盤點 ①、盤點 ②、盤點 ③。

元件 現況 本案動作
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 參考寫法
§6

詳細設計

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

模組判準:客戶會不會把這群功能當一組來授權。模組不是 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 起點,A.1 合併時定稿):

模組 資源(現有 resource_type) 住在哪 A.1 分工
grc 專案稽核 project、audit、workflow、project-summary-report、information-system api/project、flow_control、jedi-compliance-audit、jedi-task-platform T-1.1
oscal 合規框架 compliance-framework、module-frame api/oscal、api/module_frame T-1.2
flow 流程設計 flow_template api/flow_engine、jedi-flow-engine T-1.2
detection 檢測 device、remote-agent-manage、detection-profile、plugin、scan-zone jedi-detection、jedi-remote-agent T-1.3
evidence 證據 cloud_integration、storage-config jedi-evidence-classification、api/cloud_integration T-1.3
survey 問卷 survey jedi-survey T-1.3
system 系統設定 smtp-config、ldap-config、notify_config、security-policy、log-forwarding、system_config、system-menu、issue-integrate-config、license api/system_config 等 T-1.4
iam 身分與存取 user、role、department、tenant jedi-iam T-1.4
platform 平台(原廠) ai-dashboard、ai-call-log、ai-quota、bulletin、bulletin-list、feedback、feedback-view、log is_platform 與平台層 T-1.5

A.1 合併時要請決策者裁的歸屬:information-system(grc 或 oscal)、storage-config(evidence 或 system)、ai-quota(platform 或 system)、ai-dashboard 是否獨立成模組(它是賣給租戶的加值包,放「平台(原廠)」名下會誤導)。

6.2 資料模型

capabilities 欄位(改後):

欄位 型別 說明 決策
name varchar(100) UNIQUE 三段式新名 A.1
module varchar(50) NOT NULL 三段名第一段;CHECK `name LIKE module
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)

%%{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
圖 2 — 開了部門隔離後,一筆資料看不看得到

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 匯出匯入

{
  "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)
§7

拆分(5 子需求 × 31 子任務)

依賴鏈: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 盤 grc 模組:專案、稽核輪次、流程控制、摘要報告、資訊系統的每支 route 對到新三段名 inventory/a1-grc.md 該模組每支要登入的 route(含 jedi-compliance-audit、jedi-task-platform 端點)都有一列且有新名 — opus
T-1.2 CM-2512 盤 oscal+flow:SSP、框架、資源庫、流程設計 inventory/a1-oscal-flow.md 同上;含 33 支 J 組中 oscal 16 支與 flow_engine 5 支 — opus
T-1.3 CM-2513 盤 detection+evidence+survey:設備、agent、檢測基準、插件、掃描網段、雲端整合、儲存設定、問卷 inventory/a1-detection-evidence-survey.md 同上;檢測三合一加值包整包落在同一模組 — opus
T-1.4 CM-2514 盤 system+iam:系統設定各頁、使用者、角色、部門、租戶;確認 setup_route 是否刻意不設守門 inventory/a1-system-iam.md 同上;含 jedi-iam /roles 等端點;setup_route 有結論 — opus
T-1.5 CM-2515 盤 platform:AI 儀表板、AI 呼叫紀錄、AI 額度、公告、意見回饋、log inventory/a1-platform.md 同上;標出每個 is_platform 能力點 — opus
T-1.6 CM-2516 合併五份成一棵能力點樹:統一資源命名與動詞歸屬、模組定稿、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 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 新表 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 org_units.path 掃描回填與格式檢查;租戶 org_unit_isolation 開關欄(預設 false);能力點清單版本號 migration 每列 path 非空且格式 /id/…/;開關欄存在且全為 false T-1.6 sonnet
T-2.4 CM-2520 出貨角色改 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 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 升級排練:拿一份隔一版以上的真實客戶舊庫跑 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 jedi-iam pattern 展開服務:pattern 語法驗證、展開、*.*.* 依租戶扣平台與排除清單、新舊名皆認 jedi-iam 展開服務+單元測試 *.*.read、grc.*.*、完整名三類展開正確;非 root 租戶 *.*.* 等於 103 個 T-2.2 opus
T-3.2 CM-2524 Redis 快取:鍵含租戶、清單版本號、角色;角色改動主動清租戶快取;Redis 失敗退回查 DB 快取層 改角色後下一個請求即生效(多行程);停掉 Redis 判定結果不變、不放行不全擋 T-3.1 opus
T-3.3 CM-2525 判定改讀新表:get_active_capability_names 走有效指派+展開;加守衛測試掃程式內舊能力點名 jedi-iam 判定、守衛測試 DEV 全部既有角色判定結果與改前相同;守衛測試列出剩餘舊名 T-3.1 opus
T-3.4 CM-2526 同源:選單可見性、選單明細、JWT is_admin claim 改吃展開服務(修兩個資安漏洞) jedi-iam 改動 把某管理員指派到期日改成昨天,重新登入後 claim 為假、管理頁入口消失、選單明細不再列能力點 T-3.3 opus
T-3.5 CM-2527 部門範圍:有效指派帶回指派部門 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 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 角色 JSON 匯出匯入 API:命中檢查、同名覆蓋或另存、系統管理員不可覆蓋、走 license 守門 jedi-iam 端點 匯入含零命中 pattern 的檔整份報錯並列出;匯入同名系統管理員一律另存 T-3.1 sonnet
T-3.8 CM-2530 指派 CRUD(含起訖、部門)、檢查存取(每個能力點的來源角色/pattern/範圍/到期)、隔離開關預覽與切換(含稽核事件)API jedi-iam 與 BE 端點 檢查存取回傳與判定放行逐項一致;預覽清單在 DEV 跑得出每人每表少看筆數 T-3.4、T-3.6 opus

A.4 守門補齊 — 依賴:A.3

編號 卡號 做什麼(白話) 產出 驗收條件 依賴 建議 model
T-4.1 CM-2531 guard 加「只記錄」模式:每個能力點一個旗標,不放行時寫結構化 log 仍放行;統計查詢 jedi-iam guard、統計腳本 標成只記錄的能力點在無權帳號下放行且有 log;未標的既有守門照擋 T-3.3 opus
T-4.2 CM-2532 grc+flow 13 支 route 掛能力點(flow_engine 5、flow_control 3、project 3、摘要報告 2),標只記錄 route 掛點 每支都有守門;一般帳號點完相關頁面畫面不變、log 有紀錄 T-4.1 sonnet
T-4.3 CM-2533 oscal 16 支 route 掛能力點(SSP 13、framework 3),標只記錄 route 掛點 同上 T-4.1 sonnet
T-4.4 CM-2534 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 觀察期報告與真擋切換:DEV+STG 一週 log 統計、逐筆判斷補勾或改掛點;版本 3 拿掉只記錄旗標;兩版 release note 文字 統計報告、真擋 commit、release note 草稿 統計無「非管理員會被擋」或每筆有處理結論;真擋後同一動作回 403 T-4.2~4.4 opus

A.5 存取管理前端 — 依賴:A.3(T-3.7、T-3.8)

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

端到端驗收

項目 怎麼驗 通過條件
升級排練(真實舊庫) 拿隔一版以上的真實客戶舊庫跑 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 個功能開始檢查能力點,未授權的一般角色會被擋
§9

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)做讀取時轉換。

§10

參考座標

  • 討論稿:discussion.md
  • 盤點:inventory/01-authz-capabilities.md、inventory/02-project-roles-and-flow.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