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
2026-10-06 T-1.6 合併定稿:能力點樹、出貨五角色(補「一般成員」)、D12 改為本版不做(T-3.6、T-5.6 整卡延後;T-2.3/T-3.8/T-4.4 的隔離部分延後) T-1.6 CM-2516
2026-10-07 D12 由「本版不做」改為「不做、記錄為設計決定」:部門只當屬性;T-3.6/T-5.6 結案;analysis 落地 首腦第 4 棒
2026-10-07 6.6 存取管理前端依定稿 mockup 改寫:三頁(角色列表、角色明細、指派),檢查存取併入指派頁,介面用語「權限」;A.5 五卡重開 CM-2548~2552
§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、不用 *.*.read,避免露出 system/iam 設定頁;定稿 inventory/a1-shipped-roles.json) 系統管理員是「至少有一個人能進來修權限」的保證;其餘只是起點,各家編制不同,硬鎖只會逼客戶複製近似角色 四個全鎖;全部可刪
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 部門資料隔離 不做,記錄為設計決定(2026-10-07 決策者拍板;理由與未來調整路徑見 docs/analysis/2026-10-07-fr132-d12-org-unit-isolation-not-doing.md):部門只當屬性——用來指派角色範圍(T-3.5,本版開關固定關)與列表篩選,不用 RLS 擋資料可見性;tenants 不加開關欄;§6.4 與 inventory/a1-d12-tables.md 留作日後第三層輸入。原設計:機制+租戶開關預設關、只改挑選的業務表、開啟前出預覽清單 有客戶需要部門互不相見,但多數客戶的部門只是組織標記;預設關升級無感;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)、觸發外部服務或花費(execute)(2026-10-07 T-1.7 後加:按下 AI 分類、重建全租戶雲端資料夾這類「做了就花錢或動到外部系統」的動作,和整理資料不是同一個決定) oscal.ssp.export、evidence.classification.execute
一個資源一個 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。 例外:任務產出物結案後視為證據——已完成問卷的內容、答案、歷史、討論與檢測工具執行紀錄,持 grc.job-evidence.read 也能讀(與證據檔同一顆);未結案問卷仍只認 grc.job.read,只持證據權限的人回 404。寫入不受影響。
  • 通則 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
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(FE config/router/index.js:93-122),舊網址導向新頁;/auth/user-manage 帳號管理頁保留、瘦身。/auth/user-form-mtrbac 是管理員編輯帳號的表單(UserMTRBACForm.vue,基本資料+角色),只拿掉角色區塊;路由要不要改名由 T-5.5 回寫決策者。定稿畫面:mockup/role-detail-v3.html(角色明細,頂部切換看新增模式)、mockup/roles-and-assignments-v3.html(角色列表、指派頁、新增指派對話框);同目錄的 access-management.html、access-management-v2.html 不是定稿,不要照著做。

介面用語(FE i18n 規範):對使用者一律說「權限」、單位「項」,不出現「能力點」「pattern」「capability」;程式碼、API、資料表照舊用 capability。錯誤訊息(error-code.json)同一規則。

頁 內容 元件與資料來源
角色列表 一張表一行一角色:名稱(內建系統管理員帶鎖頭圖示,只能檢視與複製)+一句描述、權限數(展開後)、使用者數、可見頁面數、最後修改(出貨附帶的四個範例顯示「系統建立」)、操作(編輯/複製/刪除)。不分類型欄、不分篩選(只有一筆內建,分類會誤導),只有搜尋框與「顯示已停用」。頂部「匯入角色 JSON」「新增角色」 DataTable、IconField 搜尋、Tag;POST /roles、匯入 POST /roles/import
角色明細 主視角「依功能選單」三層樹:選單分組 › 畫面 › 該頁的權限,每項一行「動作名+涵蓋哪些操作」;右上切換「依資源」(模組 × 資源 × 動詞矩陣)當進階視角。右欄三張可收合卡:可見選單、角色資料、權限規則。新增、編輯、複製三種進入方式同一頁;底部固定儲存列列出未儲存的變更 見下方「角色明細」細節
指派 主表一筆一行:使用者、角色、生效、到期(空=永久)、狀態(有效/未生效/已到期)、操作(編輯/移除);「依人/依角色」=同一張表換分組列;角色下拉篩選、「顯示已到期」開關(預設隱藏)。頂部「檢查存取」搜尋框:輸入人後表格切成「這個人每項權限來自哪個角色、何時到期」+可見選單。「新增指派」開對話框 DataTable(rowGroupMode="subheader")、Dialog、Calendar、MultiSelect;/role-assignments、/role-assignment、GET /users/<uid>/effective-access
帳號管理 只管帳號本身(基本資料、啟用停用、帳號層生效到期、匯入);帳號編輯表單的角色指派區塊(UserMTRBACForm.vue:548-578)為指派摘要,可就地編輯起訖與移除(走指派 API,不經帳號存檔),另有「新增指派」「管理指派」連到指派頁;個人資料頁(UserProfileForm.vue:456)本來就只唯讀顯示,不動 由現有頁瘦身

沒有的東西(決策者定):檢查存取不另開頁;部門隔離設定頁不做(D12 不做);指派沒有範圍欄、沒有部門篩選——部門指派與整公司判定相同,欄位藏掉,API 的 org_unit_id 參數保留、前端不送(一律 scope=tenant)。

角色明細細節:

區塊 規則
三層樹 第一層=側邊選單分組(ui_routes root,DEV 8 組;授權管理組只有原廠頁與登入即可見頁,樹上不出現);第二層=側欄頁;內頁(is_menu=false)預設不上樹,權限上移最近的母頁;ui_routes.tree_page=true 的內頁是例外,上樹成側欄頁底下的子頁(專案管理 › 專案規劃/專案總覽/稽核計畫——三個工作面權限各四十幾顆,壓成一列管理員分不出誰管什麼),權限留在子頁、它底下的內頁上移進它;最內層=該頁的權限。內頁只列自己的進入門檻與母頁沒列的顆,母頁已列的權限不重複。同一母頁下多支內頁同門檻才併成一列,單支內頁顯示頁名。空殼權限(沒綁頁、沒功能點、後端沒守;矩陣 orphan_capabilities)不列,「依資源」視角也不列。進入該頁所需那項(route_capabilities 的 ALL 規則,每頁至多一顆)標「進入此頁所需」。名稱一律走 i18n(lang.menu.<name>、權限顯示名),不寫死 mockup 上的字
權限歸到哪一頁 ① capabilities.home_route_name 有值就放那頁(記 ui_routes.name,不記 id——同一頁在各環境 id 不同);值為 __global__ 的放最後的「全站功能」組(如 AI 小幫手);② route_capabilities 綁在哪頁也列在哪頁;③ 都沒有的落「其他動作」組,正常是空的、空組不顯示。歸屬欄只管排哪裡,不影響誰看得到哪頁(可見性只看 route_capabilities)——不直接綁頁是因為綁上去會讓只有 SSP 權限的人側欄冒出專案列表、點進去 403。值由程式碼宣告(Capability.home_route),乾淨安裝與每次升級都照宣告寫入;守衛測試 test/test_fr132_capability_home_route.py 擋「沒綁頁又沒填」與「填了不存在的頁名」
勾選 分組列與畫面列各一個三態勾選框(全/半/空);畫面列另有「只給檢視」一鍵只勾門檻項;工具列「全部展開 | 全部收合」。未授權模組的權限反灰不可勾、附原因(照搬 RoleForm.vue:236-240 的 is_licensed 判斷);編輯非 root 租戶角色時原廠項不出現(照搬 RoleForm.vue:41-54,171-180)
可見選單卡 41 頁即時預覽,照選單分組:可見綠點、不可見灰點加「加入」(一鍵勾該頁門檻項);分組可收合並顯示「可見 N / M」,標題旁「展開 | 收合」;License 授權管理頁標「原廠管理員專用」不給加入。資料來自 POST /ui-routes/visible-by-capabilities,與側欄 /user/web-menu 同一條判定
角色資料卡 狀態、最後修改、權限數、使用者數、「查看指派清單」(帶角色篩選跳指派頁)
權限規則卡 這個角色實際存的規則唯讀清單,每條標展開幾項,說明「含星號的規則會自動涵蓋日後新增的權限」
新增模式 標題變名稱輸入框、描述輸入、「以既有角色為範本」下拉(預設不使用;選了帶入該角色的勾選,之後各自獨立);列表頁「複製」走同一頁預選範本。底部「建立角色」,未填名稱或一項都沒勾時停用並提示「填好名稱並至少勾一項才能建立」。右欄只有可見選單卡
內建系統管理員 樹唯讀、無刪除與儲存,只有「複製成新角色」與「匯出 JSON」
寫入 POST /role、PUT /role/<uid> 送勾選的權限 id 清單,BE 同時寫規則表(T-3.9);畫面不自己合成萬用字元

新增指派對話框:選人(可多選,每人各產生一筆)、角色下拉、期間(永久/指定起訖兩個日期);右側即時預覽「可見選單變化」——他目前就有的灰、加上這個角色後新增的綠色加號;按「建立指派」。

BE 要補的三件(前端需要、今天沒有):① POST /ui-routes/visible-by-capabilities:給一組權限名或角色 uid,回可見頁清單;② 角色回應補 is_system、is_template、規則清單(entity 已有,RoleDTO/RoleResponse 沒帶出);③ 列表每列補展開後權限數與可見頁數。另有一個陷阱要在帳號管理頁瘦身時一起修:update_user(jedi-iam user_service.py:277-288,352)存帳號時把該使用者的指派全刪再照 payload 重建,重建時不帶起訖;管理員改帳號(PUT /user/<uid>)與本人改個人資料(PUT /user-profile/<uid>,user_route.py:246-255 從既有指派重組、同樣漏起訖)都走這條,存一次帳號,指派的到期日就被清成永久。BE 要改成「沒送 user_roles 就不動指派」,帳號頁不再送。

FE hasCap 約 15 支檔的權限名改新名;版本 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

拆分(6 子需求)

依賴鏈: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 掃描回填與格式檢查;能力點清單版本號 migration 每列 path 非空且格式 /id/…/;版本號存在 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 端點 檢查存取回傳與判定放行逐項一致 T-3.4、T-3.6 opus
T-3.9 CM-2546 角色頁新增/修改角色同時寫 role_capability_patterns(修 UI 建的角色對判定無效) jedi-iam RoleDomainService、BE DI 建角色勾 3 項新表 3 列;改勾選判定同步變;系統管理員 pattern 不動 T-3.3 opus
T-3.10 CM-2553 存帳號不再全刪重建指派:沒送 user_roles 就不動、有送改差異更新保留起訖 jedi-iam user_service 設到期日的指派,管理員改帳號與本人改個人資料後 ends_at 仍在 T-3.8 補件 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 進豁免清單 route 掛點、豁免清單 每支 route 要嘛有守門要嘛在豁免清單 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
T-4.6 CM-2542 補 A.1 盤點發現的十三個守門缺口(grc 四項、公告列表範圍、設定讀取無守門、第三方登入綁定查詢無本人檢查、兩處守錯動詞、問卷兩處無身分檢查、AI 模型清單、解析單詳情、資源庫列表、確認端、流程定義端點) 守門修補(含套件) 非成員讀留言回 403;摘要報告受授權模組控制;管理員不能旁路刪專案;流程圖端點退役或有守門 A.3 opus

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

定稿畫面與規則見 6.6。

編號 卡號 做什麼(白話) 產出 驗收條件 依賴 建議 model
T-5.1 CM-2548 共用元件:功能選單 › 畫面 › 權限三層樹(三態全選、只給檢視、展開收合、「依資源」切換、沒綁頁的權限歸頁規則)、可見選單預覽;BE 新端點 POST /ui-routes/visible-by-capabilities(走既有可見判定,守 iam.role.read) PermissionTree.vue、VisibleMenuPreview.vue、jedi-iam 端點 拿出貨稽核員打端點,回的頁面與只被指派稽核員的帳號登入後側欄完全相同;206 項權限每項都有家、不重複 T-3.4 opus
T-5.2 CM-2549 角色列表(一張表、無類型欄與分類篩選)+角色明細(新增/編輯/複製同一頁、右欄可見選單/角色資料/權限規則三卡、底部儲存列)、JSON 匯出匯入;BE 角色回應補 is_system/is_template/規則清單、列表補權限數與可見頁數 角色兩頁、jedi-iam 回應補欄 系統管理員只能看、複製、匯出;新建勾 3 項後規則表 3 列;匯出再匯入得到相同角色 T-5.1、T-3.7、T-3.9 opus
T-5.3 CM-2550 指派頁(一筆一行、依人/依角色分組、角色篩選、「顯示已到期」預設關、無範圍欄)+新增指派對話框(多選人、永久或起訖、即時預覽可見選單變化);BE 列表補 hide_expired 篩選 指派頁、對話框 未生效指派在預設檢視看得到;到期指派預設藏起、開關打開出現;多選兩人得兩筆 T-5.1、T-3.8 opus
T-5.4 CM-2551 檢查存取:指派頁頂部搜尋框,輸入一人後表格切成「每項權限來自哪個角色、何時到期」+可見選單,豁免帳號有說明 指派頁結果視圖 重疊權限列兩個來源;指派過期後該角色貢獻的權限消失;可見選單與實際登入側欄一致 T-5.1、T-3.8、T-5.3 opus
T-5.5 CM-2552 帳號編輯頁角色區塊改唯讀摘要+「管理指派」、舊角色路由導向新頁、選單改「存取管理 › 角色/指派」、介面「能力點」全改「權限」;BE 修存帳號會清掉指派起訖的陷阱 帳號頁瘦身、路由、選單 migration、i18n、jedi-iam 修正 設好到期日後存一次帳號,到期日仍在;舊網址導到新頁;畫面上無「能力點」 T-5.2~5.4 sonnet
T-5.6 CM-2541 (不做,D12) 部門隔離設定頁 — — — —

A.6 頁內按鈕照權限隱藏 — 依賴:A.3、CM-2556(補權限)

2026-10-07 決策者手測後新增。後端守門與側欄遮蔽做完後,頁內寫入類按鈕大多不看權限(117 支有按鈕的頁只有 17 支判斷、用到 19 項),症狀是「按鈕在、按下去 403」。裁示:隱藏不停用(授權唯讀才用停用+說明);專案內頁雙層——公司層權限決定按鈕出不出現、專案成員角色維持「輪到誰做」,兩者都過才顯示,不把成員角色換成公司層權限(那是 FR-B);前端不看 enforce_mode,只記錄的權限照樣隱藏,否則觀察期看不出誰缺權限。架構(決策者 2026-10-07 拍板,不逐顆補):三層——路由層(每頁一顆進入權限登記在後端 route_capabilities,前端守衛讀後端回的可進名單)、元素層(v-can 指令唯一寫法)、資料層(專案角色/狀態以 v-if 疊加);真相唯一鏈 route_capabilities → /users/me/capabilities → userStore → 守衛與指令,前端不寫死任何「哪頁要哪顆」。被排除:逐顆補 v-if(無機制擋新裸頁)、前端路由 meta.capability(第二套真相)、內頁跟側欄頁同門檻(新增頁有 read 就進得去)。取捨全文見 docs/analysis/2026-10-07-fr132-a6-frontend-three-layer-authz.md。

架構

層 擋什麼 定義在哪 前端怎麼寫 守衛
路由 直打網址進頁 ui_routes+route_capabilities,每頁恰一顆 ALL,取主要動作(新增 create、編輯 update、檢視 read;共用表單另綁 ANY update);內頁 is_menu=false 不寫;全域守衛讀 userStore.routes,不在名單導 /403 BE test/test_fr132_route_entry_registered.py
元素 看得到按了才 403 權限宣告(套件 CAPABILITIES、core/host_capabilities.py),產生器出 FE src/config/capabilities.js v-can="CAP.X"(陣列=任一、{all}=全部),編譯期改寫成 v-if FE src/__tests__/v-can-guards.spec.js
資料 不輪到你、狀態不對 專案成員角色、單據狀態 v-if 與 v-can 並寫,AND 合併 —

真相唯一:後端表 → GET /users/me/capabilities(capabilities、routes、route_gates)→ userStore → 路由守衛與 v-can。routes 與側欄、角色「可見頁面」預覽走同一條可見判定,三者不會不一致。端點守門不通過拋 CapabilityRequiredError,403 帶 data.required/mode,前端錯誤提示與 /403 頁據此說缺哪顆。

被排除的做法:

  • 逐顆補 v-if:沒有機制擋新頁不登記,九種判斷寫法繼續並存,守衛掃不出漏網。
  • 前端路由 meta.capability 寫死權限名:第二套真相,與後端漂移時沒人察覺,角色「可見頁面」會與實際不符。
  • 內頁跟側欄頁同門檻:有檢視權限就進得了新增頁,送出才 403,只是把問題從按鈕搬到頁面。

規範落點:FE CLAUDE.md「權限守門三層」、BE docs/claude/frontend-overview.md §5.4(含新頁登記五步驟)、.claude/rules/security/endpoints.md「前端對應」、FE .claude/rules/security/frontend.md、reviewer 判準 docs/claude/secure-coding-rules.md「前端」。

編號 卡號 做什麼(白話) 產出 驗收條件 依賴 建議 model
子母 CM-2559 決策與拆棒 — — — —
T-6.1 CM-2560 單一來源 store、hasCap 改讀它、AI 小幫手浮鈕接判斷 BE 輕量端點+FE store blspan 無小幫手鈕、blsadmin 有;既有 17 頁行為不變 A.3 opus
T-6.2 CM-2561 全站寫入類按鈕→API→權限盤點,專案內頁標雙層 inventory/a6-button-capabilities.md 186 支檔逐檔;附錄 A 查不到的列出 無 opus
T-6.M1 CM-2564 路由層:內頁全登記進入權限(主要動作那顆)、後端回可進路由名單、前端全域守衛;含 141 條路由盤點 migration+端點欄位+守衛 直打內頁網址被導走;側欄/角色樹/直入三者一致 T-6.1 opus
T-6.M2 CM-2565 元素層:v-can 指令唯一寫法+三支守衛(寫入鈕必掛、禁新增散寫、名稱合法) 指令+vitest 守衛 拔掉一顆 v-can 守衛紅 T-6.1 opus
T-6.M2b CM-2569 權限矩陣產生器:掃 v-can 掛點+後端表 → permission-matrix.json,守衛三向核對 JSON+守衛 重跑 diff 空;刪一列紅 M2a opus
T-6.M2c CM-2570 GET /permission-matrix、角色設定頁樹改讀矩陣、第三層列涵蓋功能點 端點+FE 樹第三層看得到功能點;存法不變 M2b、CM-2563 opus
T-6.M3 CM-2566 規範落地:FE CLAUDE.md 三條硬規則、資安規則前端對應、frontend-overview 三層圖與五步驟、reviewer 判準 文件 新人照五步驟開一頁守衛由紅轉綠 M1、M2 定案 opus
— CM-2567 死碼清理(盤點查出 14 顆按鈕在無引用檔) 刪檔 build+vitest 過 無 sonnet
T-6.3 CM-2574 grc 專案內頁 38 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 opus
T-6.4 CM-2575 grc 任務與匯入 50 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 opus
T-6.5 CM-2576 oscal 專案 SSP 與共用 panel 55 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 opus
T-6.6 CM-2577 oscal 資源庫、合規框架+flow 47 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 opus
T-6.7a CM-2578 detection+evidence 39 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 sonnet
T-6.7b CM-2579 survey 39 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 sonnet
T-6.8a CM-2580 system+platform+ai 39 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 sonnet
T-6.8b CM-2581 iam+asset+workspace 35 顆:照盤點表掛 v-can,順手清散寫 守衛基線降、矩陣 JSON 長 三向守衛與 v-can 守衛全綠 M1、M2a、M2b、CM-2556 sonnet
T-6.Z 盤點後補開 守衛:打寫入 API 的呼叫點必有判斷或登記豁免 測試 突變:拔掉一個 v-if 要紅 T-6.3~ 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