FR-132 · Access Management — 需求討論稿 · 2026-10-06 · 決策已定案
誰 × 角色 × 範圍 × 時效
現在的權限系統有角色、有能力點(系統裡「可以做某件事」的最小單位)、也有指派表,但部門範圍存在資料表裡卻沒進判定、指派起訖日判定有算但畫面設不了、33 支 API 只驗登入不驗能力點、能力點命名有三套寫法,前端也只有一張逐筆勾選的角色表。本案參考 Azure 的「誰 × 角色 × 範圍 × 時效」模型,把公司層這一套補完:三段式命名+萬用字元(wildcard)角色、部門範圍繼承、指派起訖日、守門補齊、一個預設關閉的部門資料隔離開關,以及一個新的「存取管理」模組。專案內的成員角色合併另開 FR-B,不在本案範圍。
Guidant AI 的權限今天分兩層,各管各的:
project.read),再把角色指派給使用者。API 用 require_capability("project.read") 這類守門擋人。這層由 jedi-iam 套件(帳號與權限套件)判定。另有一層是資料隔離:資料庫用 RLS(Row-Level Security,資料庫列層級隔離)擋住「別的租戶的資料」。這層只到租戶,不到部門——見 4.2。
公司層這一套有骨架,但三個地方沒接上:
user_roles 表有 org_unit_id(部門)與 scope 欄(jedi-iam infra/models/user_role.py:20-84),判定時卻只比租戶(user_role_repo_impl.py:81-101)——前端存部門指派時同時帶租戶 id,所以指派到某部門的角色,實際上在整個租戶都有效。指派的起訖日(starts_at/ends_at)判定有算,但前端只開了使用者帳號層的生效/到期日,每筆指派設不了。api/ 下 61 支要登入的 route 檔裡,33 支沒掛任何能力點(盤點 ① 第 3 節)。Azure 把一次授權拆成四個問題:誰(使用者)× 什麼角色(一組動作,可用 * 萬用字元)× 在哪個範圍(訂用帳戶/資源群組/單一資源,上層涵蓋下層)× 到什麼時候(指派可設起訖)。角色定義可以匯出成 JSON 帶走,「檢查存取」頁可以列出某人實際拿到哪些權限、從哪個指派來。
| # | 補什麼 | 白話 |
|---|---|---|
| ① | 角色定義 | 能力點改成三段式名字、角色可以寫 grc.project.* 這類萬用字元、角色可匯出匯入 JSON |
| ② | 指派判定 | 部門範圍真的進能力點判定、上層部門的指派涵蓋子部門、每筆指派可設起訖日;另做「部門資料隔離」機制,租戶自己決定要不要開(預設關,D12) |
| ③ | 守門與畫面 | 33 支漏網的 API 補守門;前端做一個「存取管理」模組,能看清楚「誰有什麼、為什麼有」 |
一句話:先把能力點清單理乾淨定規則(A.1),再改資料表(A.2)與判定邏輯(A.3),最後同時補守門(A.4)和做新畫面(A.5)。
| 階段 | 做什麼(白話) | 產出 | 完成怎麼判定(決策者可親手檢查) | 依賴 |
|---|---|---|---|---|
| A.1 盤點與規則 | 把每支 API(含外部套件自帶的 route)對到一個三段式能力點,定出模組清單、整棵能力點樹與出貨角色的內容 | 能力點對照表(route × method → 新名 → 舊名 → UI 頁)、模組定稿表、出貨角色 JSON 草稿、D12 要改 policy 的表清單 | 打開對照表:每一支要登入的 route 都有一列、每列都有新名字;出貨角色 JSON 裡看得到 *.*.read 這類寫法 |
D9 |
| A.2 資料地基 | 能力點改名、補模組/顯示名/排序;角色改存萬用字元;部門路徑回填;D12 開關欄 | migration、JSON seed、拿真實舊庫跑過的升級排練紀錄 | 拿一份客戶舊庫升級後,用舊帳號登入,選單與能按的按鈕和升級前一模一樣 | A.1、D2、D10 |
| A.3 判定層 | jedi-iam 學會展開萬用字元、吃部門範圍、加快取;選單、明細、is_admin 改吃同一份結果;D12 的 RLS 機制落地但開關預設關 |
jedi-iam 新版、選單與判定同源、D12 policy | 給某人一筆「A 部門、明天到期」的指派:部門隔離開關關著時,他在整個租戶都能用那些能力點(與今天相同);開關打開後,他只能對掛在 A 或 A 子部門的資料用那些能力點,B 部門的專案看不到也改不了;把到期日改成昨天,重新登入就不能用 | A.2、D3、D4、D8、D12 |
| A.4 守門補齊 | 33 支漏網 route 掛上能力點;第一版只記錄不擋,下一版才真擋 | route 掛點、「本來會被擋」的 log 與統計 | 用一般帳號點完全部頁面:第一版畫面完全沒變,但 log 裡看得到「若真擋會擋到誰」;下一版同一個動作會被擋 | A.3、D5 |
| A.5 存取管理前端 | 新模組四頁:角色、指派、檢查存取、使用者;部門隔離開關與開啟前預覽 | 四頁+取代舊兩頁 | 在「檢查存取」選一個人,看得到他每個能力點從哪個角色、哪個萬用字元、哪個部門來、何時到期 | A.3、D1、D6 |
A.1→A.2→A.3 必須照順序;A.4 和 A.5 可以平行做。
✅ 要做的事
<模組>.<資源>.<動詞>,例如 grc.project.approve、oscal.ssp.export、system.ldap-config.update。資源名一律用連字號。前兩段(模組、資源)就是角色矩陣的分組,不另建群組表。模組怎麼定見 5.4;capabilities 補的欄見第 6 節。grc.project.*、*.*.read;一個完整名字本身也是一個 pattern。判定時把 pattern 展開成具體能力點再比對,展開結果放快取(鍵=租戶+能力點清單版本)。既有資料 migration 時把每筆能力點原樣轉成「完整名 pattern」,行為零變化;is_admin 角色改成 *.*.*。name / description / is_admin / capabilities[pattern];出貨角色從 SQL seed 改成 JSON seed;匯入時對不上的 pattern 報錯並列出,不靜默跳過。/auth/role-manage 與 /auth/user-manage(見第 10 節)。path,例如 /10/12/,見 jedi-iam infra/models/org_unit.py:32)、user_roles 的部門範圍進判定(起訖已在判定裡)。*.*.* 自動進系統管理員角色;一般角色由客戶自己勾。這是行為變更,寫進 release note。✅ D1–D12 定案一覽(2026-10-06)
| # | 題目 | 定案 |
|---|---|---|
| D1 | 模組命名 | 一個「存取管理」模組,底下四頁 |
| D2 | pattern 怎麼存 | 新表 role_capability_patterns,舊表 role_capabilities 並存一版後退役 |
| D3 | 快取怎麼失效 | 能力點清單版本號+角色改動時主動清該租戶快取 |
| D4 | 部門繼承方向 | 上層部門的指派涵蓋所有子部門 |
| D5 | 只記錄觀察期 | 一個 release;DEV+STG 跑滿一週、無「非管理員會被擋」紀錄才真擋 |
| D6 | 出貨角色 | 系統管理員 *.*.* 內建不可刪不可改、每租戶必有;稽核主管/稽核員/唯讀是出貨範例,可改可刪可複製 |
| D7 | is_admin 旗標 |
保留;判斷管理員只看旗標 |
| D8 | 選單同源 | 選單可見性、選單明細、is_admin claim、判定四者吃同一份展開結果,併 A.3 |
| D9 | A.1 切棒 | 按模組分棒平行,首腦統一裁命名 |
| D10 | license 對應 | 新增 license_module 欄存舊 resource_type |
| D11 | 舊名過渡 | 判定一版內同時接受新舊名,守衛測試掃乾淨才移除 |
| D12 | 部門資料隔離 | 做機制+租戶開關,預設關 |
每項的理由見第 12 節。
✅ 明確不做(與理由)
| 不做 | 理由 |
|---|---|
| 拒絕規則(deny/notActions) | Azure 的 Deny Assignment 一般客戶不能自己建,只有系統內部用;規則一旦有「允許+拒絕」,管理員就要推算兩者疊起來的結果,出錯率遠高於好處 |
| 資料層動作(dataActions) | Azure 用來區分「管理資源」與「讀資源裡的資料」。本系統的資料存取由專案成員角色、RLS 與 D12 的部門隔離管,再加一層只會重疊 |
| 角色可指派範圍限制(assignableScopes) | 只有一個租戶內自訂角色時沒有實質需求;capabilities 新增的「可指派範圍層」已涵蓋「這個能力點只能在哪一層給」 |
| IP/時段等條件 | Azure 的 Conditional Access 掛在登入層,不是授權層。本系統登入層已有安全政策頁 |
| 政策語言(policy) | 為了「表達力」引入一門語言,換來的是沒人看得懂角色到底給了什麼 |
| 資源實例級統一授權表 | 單一專案、單一 SSP 的授權今天由成員角色管,統一進一張表是 FR-B 之後才考慮的事 |
部門隔離改到全部 43 張帶 org_unit_id 的表 |
大多數是工作紀錄、子表或設定表,隔離它們沒有業務意義,只會增加 policy 維護面;只挑客戶會說「這是 A 部門的」那幾張(第 8 節) |
事實全部來自本資料夾三份盤點:盤點 ①(BE+jedi-iam)、盤點 ②(專案角色與流程)、盤點 ③(FE),資料隔離與部門指派的數字另查 DEV DB。jedi-iam 路徑以 jedi-iam/jedi_iam/ 為根,FE 路徑以 compliance-manager-fe/src/ 為根。
| 元件 | 現況 | 座標 |
|---|---|---|
| 指派表已有部門與起訖欄 | user_roles 有 org_unit_id、scope、starts_at、ends_at(timestamptz),以及「租戶或部門至少一個」的檢查 |
infra/models/user_role.py:20-84、:26-44 |
| 有效指派的共用條件 | active_user_role_conditions 已判起訖、角色啟用、未刪除、租戶 |
infra/repository/active_role_conditions.py:19-39 |
| 部門有路徑欄 | org_units.path(例 /10/12/)+索引 (tenant_id, path),前綴比對就能找子部門;DEV 目前沒有空值 |
infra/models/org_unit.py:17,32 |
| 單一請求內的快取 | CapabilityGuard 把能力點清單存在 flask.g |
authz/capability.py:48,99-106 |
| super_admin 一律放行 | viewer_has_capability 與 require_capability 開頭就判 |
authz/capability.py:115,135 |
| RLS 注入 session 變數的做法 | 每次開 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_org_allowed_for_session(org_unit_id):資料列的部門 path 是否落在 session 部門清單之下 |
DEV DB 函式,現只用在 INSERT 檢查 |
| 前端指派畫面已能選部門 | 指派對話框三個下拉:租戶、部門、角色 | FE views/user-manage/UserMTRBACForm.vue:589-650 |
| 前端矩陣範本 | RoleForm 內的 TreeTable 三態勾選矩陣 | FE views/role-manage/RoleForm.vue:456-516、CSS :531-605 |
| 新能力點補配舊角色的 migration 範例 | 有現成寫法 | scripts/sql/2026-09-25-fr114-module-frame-read-capability-backfill.sql |
| seed 用名稱反查 id | 出貨基線不寫死 id | scripts/init/README.md:57-66 |
🔴 部門指派被當成全租戶指派
get_active_capability_names 只比 tenant_id,完全沒用 org_unit_id 與 scope,也沒有子部門繼承(infra/repository/user_role_repo_impl.py:81-101)。前端存部門指派時同時帶租戶 id(FE UserMTRBACForm.vue:216-223),所以判定只比租戶的結果是:指到某部門的角色,在整個租戶都有效。
DEV 現有 39 筆部門指派(scope = 'org',另有 141 筆租戶指派),全部帶租戶 id,對應 38 位使用者;其中 6 筆是管理員角色。管理員以為「只給 A 部門」,系統給的是整個租戶,而且沒有任何錯誤訊息。本案定案(7.1,方式乙):部門範圍套在資料上,D12 開關關著時部門指派仍等同全租戶有效、行為與今天一致;打開開關後才真正收窄,開啟前的預覽清單(8.3)會列出誰會少看到什麼。
🔴 資料隔離只有租戶一層,org_unit_id 只是標記
DEV 全部 RLS 讀取 policy 只比 app_tenant_allowed_for_session(tenant_id)。43 張表有 org_unit_id 欄,沒有一條 SELECT/UPDATE/DELETE policy 看它,Python 層也沒有按部門過濾。用到部門的 policy 只有兩種:
bulletins、devices、surveys、survey_folders、workflow_templates——新增時部門必須是自己所屬部門之下。user_org_units、project_org_unit_mapping 自身的 policy。所以 org_unit_id 今天是三種標記:歸屬紀錄(這筆是誰的)、UI 篩選器(列表可依部門篩)、公告投遞(bulletin_org_units)。同租戶的人看得到所有部門的資料。要不要讓部門也擋資料,由 D12 決定(第 8 節)。
🔴 資安項:兩個讀取端不看「有效指派」
user.roles → role.capabilities,不過濾效期、停用、租戶(app/service/user_service.py:522-529, 547)。is_admin claim:any(role.is_admin == 1 for role in user.roles),同樣不過濾(middleware/jwt_mw.py:79)。前端路由守衛 requiresAdmin 吃的就是這個值(FE config/router/index.js:1057-1066)。後果:一個已到期或已停用的管理員角色,仍可能讓前端顯示管理頁、選單明細顯示他「有」某能力點。API 層的 require_capability 有走有效條件、不受影響,所以這是「看得到不該看到的入口」而不是「做得到不該做的事」。併 A.3 一起修(D8)。
命名三套寫法(DEV 共 123 個能力點、37 種 resource_type,其中 16 個是平台層):
| resource 寫法 | resource_type 種數 | 能力點筆數 | 例 |
|---|---|---|---|
| 底線 | 4 | 13 | cloud_integration、notify_config、system_config、flow_template |
| 連字號 | 19 | 56 | ldap-config、smtp-config |
| 單字無分隔 | 14 | 54 | project、feedback |
另有 9 個孤兒能力點(沒掛任何 route):cloud_integration.update、feedback.export、system_config.create/delete/update、workflow.create/delete/read/update;system_config 缺 read,而 system_config_route 剛好沒有任何守門(盤點 ① 第 4 節)。
33 支只驗登入的 route(盤點 ① 第 3 節 J 組):
| 模組 | 支數 | 備註 |
|---|---|---|
| oscal(SSP) | 13 | ssp_components、ssp_control_implementation、ssp_export 等,service 層有成員角色守門 |
| oscal(framework) | 3 | framework_parse_job、framework_version_edit、oscal_framework_route |
| flow_engine | 5 | stage_advance、stage_rollback 等,service 層有成員角色守門 |
| grc(flow_control) | 3 | assessment_plan、job_force_start、job_route |
| grc(project) | 3 | ap_docx_import、ar_import、audit_round |
| grc(project_summary_report) | 2 | 報告與報告歷史 |
| system | 2 | system_config_route、setup_route(設定精靈,可能是刻意不設守門) |
| 其他 | 2 | ai_quota_route、diagnostic_bundle_route |
SSP、flow_engine、grc 這幾組多數在 service 層已有專案成員角色守門(盤點 ② 第 2 節),「沒掛能力點」不等於「誰登入都能用」——缺的是租戶 RBAC 這一層:公司沒辦法說「稽核部門的人不准碰 SSP 匯出」。這 61 支只算 BE api/;jedi-iam 的 /roles、jedi-survey 等外部套件自帶的 route 不在其中,A.1 要一併列入。
其他缺口:
effective_date/expire_date(FE UserMTRBACForm.vue:489,501),每筆角色指派本身沒有;資料表有 starts_at/ends_at、判定也會看,只是畫面沒開。requiresAdmin 與 requiresPlatformAdmin(config/router/index.js:1057-1100),註解明寫不依選單擋直接輸入網址(:464、:873)。api/routing.py:107-111)。Administrator(id=2、is_admin = 1,scripts/init/04-seed-core.sql:62),從 :272 起配全部 123 個能力點;新租戶另建 System Manager(app/service/tenant_provisioning_service.py:49,149)。DEV 9 個租戶各至少有一個 is_admin 角色,其中租戶 102 有兩個;轉換規則見第 6 節。<模組>.<資源>.<動詞>,三段都小寫,資源名內部用連字號:
grc.project.read、grc.project.approve、grc.audit-round.closeoscal.ssp.export、oscal.framework-version.updatesystem.ldap-config.update、system.security-policy.read前兩段就是矩陣的兩層分組,所以「群組」不是另一張表,而是名字的前綴。新增一個能力點,矩陣自動長出它該在的位置。
| 規則 | 說明 | 例 |
|---|---|---|
| 每個資源固定 read/create/update/delete | 四個基本動詞一律有,不視情況省略 | grc.project.read … grc.project.delete |
| 獨立動詞只給「給了 update 卻不想一起給」的 | 只有四類:狀態轉換(approve/close/publish)、對外送出(export/send)、指派(assign)、匯入(import) | grc.project.approve、oscal.ssp.export、grc.project.assign |
| 一個資源一個 read | 不拆「列表」與「明細」 | oscal.ssp.read 同時管清單與內容 |
| 跨資源動作歸「被產出的資源」 | 從 SSP 產生 AP,算 AP 的 create | grc.assessment-plan.create |
| 純查詢輔助端點不開能力點 | 下拉選單、menu 類端點跟著主資源的 read | 專案下拉跟 grc.project.read |
%%{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 LR
ROOT["全部 *.*.*"] --> GRC["grc 專案稽核"]
ROOT --> OSCAL["oscal 合規框架"]
ROOT --> SYS["system 系統設定"]
GRC --> P["project 專案<br/>read create update delete<br/>approve assign export"]
GRC --> AR["audit-round 稽核輪次<br/>read create update delete close"]
OSCAL --> SSP["ssp 系統安全計畫<br/>read create update delete<br/>import export"]
SYS --> LDAP["ldap-config LDAP 設定<br/>read create update delete"]
矩陣上,勾「grc」這一列=存 grc.*.*;勾「grc / project」的 read 欄=存 grc.project.read;勾整欄 read=存 *.*.read。畫面上看到的永遠是展開後的結果,存起來的是最短的 pattern。
模組不是 UI 選單。 選單會隨改版重排,能力點名卻是對外契約——授權照、角色 JSON、客戶已勾好的角色都綁著它,改一次就是一次全客戶升級。兩者各自獨立:矩陣用模組分組、顯示 display_name;選單照自己的結構走,透過 route_capabilities 對到能力點。
判準:客戶會不會把這群功能當一組來授權。 不照程式住在哪個套件分——外部套件(jedi-*)只是實作位置,一個模組可以橫跨主專案與數個套件,一個套件也可以分屬兩個模組。
硬規則:模組邊界與授權照的加值包邊界對齊。 授權照把功能分成基礎包與四個加值包(docs/features/FR-062-2608-license-management/design.md 第 6 節):
| 加值包 | 內含 resource_type |
|---|---|
| 問卷包 | survey |
| 檢測工具包(三合一不拆賣) | remote-agent-manage、detection-profile、plugin |
| 雲端整合包 | cloud_integration |
| AI 儀表板包 | ai-dashboard |
另有不受授權照管制的基礎設施七項 INFRASTRUCTURE_MODULES(smtp-config、ldap-config、log-forwarding、security-policy、ai-call-log、ai-quota、scan-zone,common/authz/license.py:107-121)。一個加值包必須整包落在單一模組內,不可橫跨兩個模組——否則「買了檢測工具包」在矩陣上會散成兩區,客戶勾一半就以為勾完了。模組可以比加值包大(一個模組含一個加值包加其他基礎功能),不可以把一個加值包切開。
現況:沒有正式的模組盤點。手上有的是三份半成品——DEV 的 37 個 resource_type(=三段名的第二段,沿用不重定義)、授權照的基礎包/加值包分層、core/plugins/ 下約 30 支 plugin 註冊檔。
草案(A.1 的起點,不是定稿):
| 模組 | 資源(現有 resource_type) | 住在哪 |
|---|---|---|
grc 專案稽核 |
project、audit、workflow、project-summary-report、information-system | api/project、flow_control、jedi-compliance-audit、jedi-task-platform |
oscal 合規框架 |
compliance-framework、module-frame | api/oscal、api/module_frame |
flow 流程設計 |
flow_template | api/flow_engine、jedi-flow-engine |
detection 檢測 |
device、remote-agent-manage、detection-profile、plugin、scan-zone | jedi-detection、jedi-remote-agent |
evidence 證據 |
cloud_integration、storage-config | jedi-evidence-classification、api/cloud_integration |
survey 問卷 |
survey | jedi-survey |
system 系統設定 |
smtp-config、ldap-config、notify_config、security-policy、log-forwarding、system_config、system-menu、issue-integrate-config、license | api/system_config 等 |
iam 身分與存取 |
user、role、department、tenant | jedi-iam |
platform 平台(原廠) |
ai-dashboard、ai-call-log、ai-quota、bulletin、bulletin-list、feedback、feedback-view、log | is_platform 與平台層 |
盤點時預期會有爭議、要回來請決策者裁的歸屬:
information-system 歸 grc 還是 oscal(它是 SSP 受評範圍的支撐資料,也是專案的設定項)storage-config 歸 evidence 還是 system(存的是證據檔,但屬機房設定性質)ai-quota 歸 platform 還是 systemai-dashboard 是賣給租戶的加值包,放在「平台(原廠)」模組名稱上會誤導;要不要獨立成一個模組A.1 每棒產出表欄位固定:route × method → 新三段名 → 現有能力點 → UI 頁。外部套件自帶的 route 一併列入(jedi-iam 的 /roles、jedi-survey 的端點等)——第 4.2 節的 61 支只算 BE api/,只照那份盤會漏掉整個 iam 與 survey 模組。
| 表 | 動作 | 內容 | 對應決策 |
|---|---|---|---|
capabilities |
改資料 | 123 筆 name 改三段式;resource_type 改成新資源名(三段名的第二段);action 是動詞 |
A.1 |
capabilities |
加欄 | module(三段名第一段,與 name 一致由檢查約束保證)、display_name(i18n key)、sort、assignable_scopes(tenant/org_unit/project,project 為 FR-B 預留)、license_module(存舊 resource_type) |
D10 |
capabilities |
加欄(過渡) | 舊名欄,判定一版內同時接受新舊名 | D11 |
capabilities |
補資料 | 補 system.system-config.read;9 個孤兒依 A.1 結論刪或掛 |
A.1 |
role_capability_patterns |
新表 | role_id、pattern |
D2 |
role_capabilities |
保留一版後退役 | 並存期間舊版程式仍可讀 | D2 |
roles |
加欄 | is_system(內建,不可刪不可改)、is_template(出貨範例,可改可刪可複製) |
D6 |
user_roles |
不改結構 | 既有 org_unit_id、scope 開始進判定;starts_at、ends_at 前端補欄位 |
D4 |
| 快取版本 | 新 | capability_catalog_version 整數,新增或改名能力點時加一;放新表或 system_configs 一列 |
D3 |
| 租戶設定 | 加欄 | org_unit_isolation 開關,預設關;放 tenants 加欄或 system_configs 一列,A.2 依 RLS 讀取成本定 |
D12 |
| RLS | 改 policy | D12 挑選的業務表,SELECT/UPDATE/DELETE policy 加部門條件;session 加 app.allowed_org_unit_paths |
D12 |
org_units |
回填 | path 掃一次,保證每列有值且格式一致(/id/…/) |
D12 前置 |
| seed | 改 | scripts/init/04-seed-core.sql 的 123 列 role_capabilities 變成一列 *.*.*(系統管理員)+三個範例角色;出貨角色改 JSON seed |
D6 |
升級相容:全部是加欄、加表、改資料,沒有刪欄、沒有改型別。帶舊資料升級不會因 schema 失敗;會出事的只會是「資料改得不對」,所以靠下面的自檢擋。
role_capabilities 轉成一筆「完整名 pattern」,角色實際能做的事一個都不能多、不能少。migration 結尾自檢:轉換前後每個角色展開出來的能力點集合逐角色比對一致,有任何一個角色不一致就整支 rollback。*.*.* 展開的範圍:非 root 租戶展開時扣掉 is_platform 能力點與不販售清單 TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES(core/plugins/identity.py:89-95),與今天新租戶 System Manager 拿到的集合一致(DEV:123 − 16 個平台層 − 4 個 workflow = 103)。授權照另由軸⑥判,不在展開時扣。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)。resource_type:授權照裡的模組鍵就是能力點的 resource_type(common/authz/license.py:79-80、:283-297),照已簽出在客戶手上不能重簽。改名後 license 改讀 license_module(D10)。tenant_provisioning_service.py 建角色的邏輯改成建系統管理員 *.*.* 加三個範例;scripts/init/migrate-capability-grants.sql 模板改寫 pattern。scripts/init/02-schema.sql 與 seed 要同步,否則新裝客戶缺這幾支 migration 且不報錯。所有 migration 落地後回報「出貨基線待重產」,重產屬決策者裁示。✅ 定案:哪個 is_admin 角色變成內建系統管理員
規則:租戶內「能力點=該租戶全集」的 is_admin 角色,轉成內建系統管理員 *.*.*(設 is_system);同一租戶有多個時取最早建的那個。其餘 is_admin 角色保留旗標,能力點原樣轉成完整名 pattern,不升級成 *.*.*。這樣第 1 點的零變化成立,也不會替客戶刪角色;升級排練紀錄列出每租戶選到誰、哪些沒轉。
各環境實查:DEV 租戶 102 原本有一個 is_admin 角色只配 60 個能力點,已補滿,該租戶兩個全集角色依上條取最早建者。STG 與 190:is_admin 角色皆每租戶一個且配滿(STG 的 121/101 個是版本落後少兩個能力點,升級會補)。
判定還是在 jedi-iam 的 CapabilityGuard,對外介面不變(require_capability("grc.project.read") 照用),裡面換三件事:
active_user_role_conditions(起訖、啟用、未刪除、租戶),加上部門條件(7.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'}}}%%
sequenceDiagram
participant R as API 請求
participant G as CapabilityGuard
participant DB as 資料庫
participant C as 展開快取
R->>G: require_capability("grc.project.read")
G->>G: super_admin?是就直接放行
G->>DB: 查有效指派(起訖、啟用、租戶;部門指派連同其部門 path 一起取回)
DB-->>G: 角色清單
G->>C: 取這些角色的展開結果(鍵:租戶+能力點清單版本+角色)
alt 快取有
C-->>G: 具體能力點集合
else 快取沒有
G->>DB: 讀角色 pattern 與能力點清單
G->>G: 展開 pattern(grc.project.* → read/create/…)
G->>C: 存入
end
G->>G: 比對;同一請求內再存 flask.g
G-->>R: 放行,或回 403
角色不分部門,指派才分部門。 角色定義(一組 pattern)全租戶共用;user_roles.org_unit_id 是這筆指派的範圍。既有欄位設計正確,缺的是判定去讀它,以及資料有可比的部門歸屬。
先分清兩個不同的「部門」:
| 名詞 | 存在哪 | 意思 | 參不參與本案判定 |
|---|---|---|---|
| 所屬部門 | user_org_units |
這個人隸屬哪些部門;登入時依租戶過濾後放進 user context(jedi-iam middleware/context.py:119-129) |
不參與 |
| 指派部門 | user_roles.org_unit_id |
這筆角色指派生效的範圍 | 參與 |
判法:一筆掛在部門 X 的指派,在使用者碰的資料列 org_unit_id 落在 X 子樹內時啟用(資料列部門的 path 以 X 的 path 開頭)。使用者本人隸屬哪個部門不影響結果——一個人可以隸屬稽核部,卻被指派去管財務部的專案。這就是 Azure「上層涵蓋下層」的原意:比的是被操作的對象落不落在指派範圍內。
org_unit_id 為空):在該租戶內一律啟用,與資料部門無關。org_unit_id 為空的資料列不受部門限制。落到程式上分兩層:
| 層 | 問的問題 | 判法 |
|---|---|---|
route 層 require_capability(cap) |
這個人能不能做這類動作 | 任一有效指派(租戶或部門)給了這個能力點就放行——對外介面不變 |
| 資料可見性(RLS) | 看得到哪幾筆 | D12 開時,資料列部門要落在「有效部門指派的部門 path」子樹內,或此人有有效租戶指派(第 8 節) |
單筆寫入 require_capability_on(cap, org_unit_id) |
對這一筆能不能做 | D12 開時,要求「給了這個能力點的指派」本身的範圍涵蓋該筆資料的部門;D12 關時等同 require_capability |
第三層存在的原因:一個人在 A 部門是編輯、在 B 部門是唯讀,RLS 讓他看得到 A、B 兩邊,route 層也會因為 A 那筆指派放行 update——只有比對「哪一筆指派給的」才擋得住他改 B 的資料。第三層只需掛在 D12 挑選的業務表的寫入路徑(第 8.2 節),由 A.3 提供、守門補齊(A.4)一併掛上。
今天選單可見性(infra/repository/ui_route_repo_impl.py:70-141)、判定(user_role_repo_impl.py:73-101)、選單明細(user_service.py:522-529)、JWT is_admin claim(middleware/jwt_mw.py:79)是四條各自的查法,後兩條不看有效指派。pattern 與部門範圍上線後若四條各自實作,必然有一條漏改,症狀是「選單看得到、點進去 403」或反過來,且不報錯。四條全部改吃同一個「有效指派+展開後能力點集合」(D8)。
D12 定案:做機制,租戶自己開,預設關。 關著時行為與今天完全一樣;需要「A 部門看不到 B 部門的專案」的客戶,在存取管理裡打開。
%%{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{"租戶允許?<br/>app_tenant_allowed_for_session"}
T -- 否 --> NO["看不到"]
T -- 是 --> S{"本租戶開了部門隔離?"}
S -- 否 --> YES["看得到(與今天相同)"]
S -- 是 --> N{"該列 org_unit_id 為空?"}
N -- 是 --> YES
N -- 否 --> P{"該列部門 path 落在<br/>允許的部門清單子樹內?"}
P -- 是 --> YES
P -- 否 --> NO
app.allowed_org_unit_paths,照 jedi-common session/database/db.py:205-209 注入 app.allowed_tenant_paths 的做法。值=使用者有效部門指派的部門 path 清單;有任何有效租戶指派、是 super_admin 或系統管理員時,等同全部門可見。app.allowed_org_paths 不是同一件事:那個是所屬部門(context.py:129),今天只給 INSERT 檢查用;新的是指派部門。名字刻意不同,A.3 落地時兩個變數旁都要寫明差別,否則下一個人會拿錯的那個。org_unit_id 為空 或 其 path 在允許清單子樹內 或 本租戶未開隔離 或 app.can_read_all_orgs 為真)」。can_read_all_orgs 是背景流程已在用的系統放行旗標(app/flow_engine/service/workflow_execution_service.py:581,706),policy 必須尊重它,否則背景工作會在開隔離的租戶裡讀不到資料。只挑客戶會說「這是 A 部門的」業務表,初估 8~12 張,A.1 盤點定案:
| 候選表 | 說明 |
|---|---|
compliance.projects |
稽核專案 |
public.devices |
受評設備 |
compliance.evidence_batches |
證據批次 |
compliance.detection_executions |
檢測執行紀錄 |
survey.surveys |
問卷 |
public.bulletins |
公告——已有 bulletin_org_units 的投遞規則,A.1 要判斷再加一層隔離會不會和投遞打架 |
compliance.information_systems |
資訊系統 |
compliance.module_frames |
控制項框架範本 |
不挑的:工作紀錄、解析任務、子表(如 evidence_batch_files)、設定表。子表靠父表入口間接隔離,A.1 要確認沒有直接列出子表的 API 繞過父表。
預覽是這個功能的安全網:部門指派下錯一層,開了之後主管看不到自己部門的專案,而 RLS 擋掉的資料畫面上只是少了幾筆,不會報錯。
org_units.path migration 掃一次回填,保證每列有值且格式一致。DEV 目前沒有空值,但客戶舊庫未驗過;path 有誤,子樹比對就會靜默算錯。org_unit_id 的表全改 policy——只挑業務表。第 4.2 節那 33 支 route,由 A.1 盤點逐支定出要掛的能力點。setup_route(設定精靈)在安裝當下還沒有任何帳號,A.1 要確認是否刻意不設守門,是的話登記成豁免清單而非漏網。
| 階段 | 行為 | 客戶感受 |
|---|---|---|
| 第一版:只記錄 | 判定照跑,結果是「不放行」時寫 log 但仍放行 | 無感 |
| 下一版:真擋 | 不放行就回 403 | 沒被授權的一般角色會開始被擋 |
新掛上去的能力點會自動進 *.*.*(系統管理員角色),管理員不受影響;一般角色要客戶自己勾。這條要寫進兩版的 release note:第一版預告「下一版會開始擋,請到存取管理頁補勾」,並附「只記錄」期間的統計讓客戶知道會擋到誰。觀察期長度與放行判準見 D5。
一個模組(D1)、四頁,取代今天的 /auth/role-manage、/auth/role-form、/auth/user-manage、/auth/user-form-mtrbac(FE config/router/index.js:93-122)。
%%{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 FE["存取管理模組(前端)"]
ROLE["角色<br/>矩陣、pattern、JSON 匯出入"]
ASSIGN["指派<br/>誰 × 角色 × 範圍 × 起訖"]
CHECK["檢查存取<br/>有效能力點與來源"]
USER["使用者<br/>帳號本身"]
end
subgraph BE["後端"]
CAP["能力點清單<br/>(含顯示名、排序)"]
RP["角色 pattern"]
UR["user_roles 指派"]
EXP["展開服務<br/>(與判定同源)"]
end
ROLE --> CAP
ROLE --> RP
ASSIGN --> UR
CHECK --> EXP
EXP --> RP
EXP --> UR
USER -.->|從使用者跳到指派| ASSIGN
RoleForm.vue:456-516),抽成可重用元件。grc.*.* 與一個個勾的差別。*.*.*,is_system):內建、不可刪、不可改內容、每租戶必有一個;畫面上整列鎖住,只能看、只能複製。is_template):出貨範例,客戶可以直接改、刪、複製。刪了或改壞了,用角色 JSON 匯入出貨預設那份即可還原(第 11 節)。範例內容在 A.1 定出能力點樹後才填。RoleForm.vue:236-240、:41-54、:171-220)。選一個人,列出他目前的有效能力點,每一列寫出:來自哪個角色、哪條 pattern、在哪個範圍、何時到期。另外標出三種「不必看矩陣」的豁免:帳號層 super_admin、角色 is_admin、平台管理員。這頁吃的就是判定層的展開服務,所以畫面上看到的=API 實際放行的。本租戶開了部門隔離時,另列一區「他看得到哪些部門的資料」,同樣來自判定層的有效部門指派。
租戶層一個開關(D12,第 8 節),放在存取管理模組內。按下開啟先出預覽清單(每人每張表少看到幾筆、誰會整張表空掉),確認後才生效並記稽核事件。只有系統管理員能動。
只管帳號本身(基本資料、啟用停用、帳號層生效到期、匯入)。今天表單裡的角色指派欄(FE UserMTRBACForm.vue:548-578)移到「指派」頁,這頁只留一個連結跳過去。
{
"name": "稽核員",
"description": "執行稽核、可讀全部專案",
"is_admin": 0,
"capabilities": ["grc.*.read", "grc.audit-round.update", "oscal.ssp.read"]
}app/auth/service/role_app_service.py:54-91)。D1 ✅ 定案:一個「存取管理」模組,底下四頁
定案:角色、指派、檢查存取、使用者四頁收在同一個「存取管理」模組,取代「角色管理」「使用者管理」兩個頁名。
理由:四頁在概念上是一組,分散在兩頁名下,「檢查存取」與「指派」找不到家;名稱也和 Azure 的「存取控制」對得上,客戶 IT 人員一看就懂。被排除的選項是保留兩個舊頁名各自加功能。
D2 ✅ 定案:新表 role_capability_patterns,舊表並存一版
定案:pattern 存新表 role_capability_patterns(role_id, pattern),role_capabilities 保留一版、下一版退役。
理由:新表並存期間,升級出問題可以退回舊版程式繼續讀舊表,回滾不需要動資料;舊表下一版再退役,那時已有一版的實際運行可以確認新表正確。被排除的選項是 role_capabilities 直接改欄,回滾就得動資料。
D3 ✅ 定案:版本號+主動清除
定案:快取鍵含能力點清單版本號(新增或改名能力點時加一),角色改動時主動清掉該租戶的快取。
理由:權限的錯誤方向是「改了還有效」——管理員拔掉某人權限,若靠時間到期,到期前他還能操作,這在稽核上說不過去。代價只是角色寫入時多清一次快取。多台 api/worker 行程時清除要能通知所有行程(或快取放 Redis),見第 14 節。
D4 ✅ 定案:上層部門的指派涵蓋所有子部門
定案:部門指派對掛在指派部門本身與其全部子部門的資料有效(7.1,範圍套在資料上,使用者所屬部門不參與);D12 關著時部門指派等同全租戶有效。
理由:這是一般人對「部門」的直覺(總公司主管管得到子部門),也是 Azure 的做法;要嚴格限定時,直接把指派下到子部門即可。反過來若嚴格只在該部門本身有效,總公司主管要在每個子部門各指派一次,部門一多就失控。
D5 ✅ 定案:只記錄觀察一個 release
定案:守門補齊先出「只記錄不擋」版,觀察一個 release。放行判準:DEV+STG 跑滿一週,log 裡沒有「非管理員會被擋」的紀錄;若有,逐筆判斷是該補勾還是能力點掛錯,處理完再進下一版。
D6 ✅ 定案:一個內建系統管理員+三個出貨範例
定案:
*.*.*):內建、不可刪、不可改內容、每個租戶必有一個。*.*.read):出貨範例,客戶可改、可刪、可複製;刪了或改壞了,用角色 JSON 匯入出貨預設那份還原。理由:系統管理員是「至少有一個人能進來修權限」的保證,客戶刪掉或改掉它,整個租戶可能沒人能再開存取管理,所以鎖死。其餘三個只是起點,各家稽核編制不同,硬鎖只會逼客戶複製出一堆近似角色。稽核主管與稽核員的 pattern 等 A.1 把能力點樹定出來再填,現在定只會被改。
D7 ✅ 定案:保留 is_admin 旗標,判斷管理員只看旗標
定案:is_admin 保留,系統管理員角色的 pattern 一律是 *.*.*;判斷「誰是管理員」只看旗標,不看是否持有 *.*.*。
理由:JWT 的 is_admin claim(middleware/jwt_mw.py:79)、前端 requiresAdmin 路由守衛、新租戶建角色(tenant_provisioning_service.py:149)、升級腳本都讀這個旗標;而用「持有某能力點」反推管理員身分已經實際踩過坑(升級腳本把非管理員塞滿權限)。
D8 ✅ 定案:選單、明細、is_admin、判定四者同源,併 A.3
定案:選單可見性、選單明細、JWT is_admin claim 都改吃判定層同一份「有效指派+展開後能力點」(7.2)。
理由:這同時修掉第 4.2 節的兩個資安漏洞;而且 pattern 上線後若各自展開,必然漏改一條,症狀是「選單看得到、點進去 403」或反過來,且不報錯。
D9 ✅ 定案:A.1 按模組分棒平行
定案:按 5.4 的模組草案分棒平行盤點(BE route 與外部套件 route 都算),各出一張「route × method → 新三段名 → 現有能力點 → UI 頁」表,首腦合併成一棵樹,並統一裁「資源怎麼命名、動詞歸哪、模組邊界爭議」。
理由:61 支 BE route 檔加外部套件端點加 123 個能力點,一棒全掃 context 會爆;分棒各自小、可平行。命名由首腦統一裁,避免每棒各出一套慣例。
D10 ✅ 定案:新增 license_module 欄存舊 resource_type
定案:capabilities 加 license_module 欄,migration 時存入舊 resource_type 原值;license 判定(common/authz/license.py:79-80、:283-297)、寫入端 license 守門(app/auth/service/role_app_service.py:54-91)、基礎設施豁免清單 INFRASTRUCTURE_MODULES(common/authz/license.py:107-121)改讀 license_module;resource_type 改成新的資源段。
理由:授權照已簽出在客戶手上不能重簽,「這個能力點屬於照上的哪個模組」必須保持原值。resource_type 今後會被讀成「三段式的中間段」,繼續拿它當 license 鍵,下一個人改資源名時一定會順手改到它,症狀是整個模組在已出貨客戶那邊被判成「沒買」。獨立一欄、名字寫明是 license 用,才擋得住。
D11 ✅ 定案:一版內新舊名並存
定案:能力點表加一欄舊名,判定一版內同時接受新舊名;加一支守衛測試掃「程式裡還有沒有舊名」,掃乾淨下一版才移除舊名欄。
理由:BE 約 116 處、jedi 套件 11 支檔、FE 約 14 支檔寫死舊名;jedi 套件要發版、FE 和 BE 要同版上線,一次改完任何一處漏改就是靜默 403。
D12 ✅ 定案:部門資料隔離做機制+租戶開關,預設關
定案:RLS 加部門條件、session 加 app.allowed_org_unit_paths、租戶層 org_unit_isolation 開關預設關;只改挑選的業務表;開啟前先出預覽清單。細節見第 8 節。
理由:今天資料隔離只到租戶(第 4.2 節),有客戶需要「部門之間互不相見」,但大多數客戶的部門只是組織標記,硬開會讓主管突然看不到東西。做成開關、預設關,升級對所有人無感,需要的客戶自己開;預覽清單讓「開了之後誰會少看到什麼」在開之前就看得到,因為 RLS 擋掉的資料不會報錯。被排除的選項:不做(需要的客戶無解)、全面開啟(升級即改行為)、改到 43 張全部(維護面過大且多數無業務意義)。
%%{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 LR
subgraph V1["版本 1"]
A1["A.1 盤點與規則"] --> A2["A.2 資料地基"] --> A3["A.3 判定層+D12 機制(開關關)"]
end
subgraph V2["版本 2"]
A4L["A.4 守門(只記錄)"]
A5["A.5 存取管理前端+隔離開關"]
end
subgraph V3["版本 3"]
A4B["A.4 守門(真擋)"]
end
subgraph VB["時間待定"]
FRB["FR-B 專案層合併"]
end
A3 --> A4L
A3 --> A5
A4L --> A4B
A1 -.->|能力點樹決定 FR-B 範圍| FRB
| 版本 | 內容 | 客戶感受 |
|---|---|---|
| 版本 1 | A.1~A.3 | 權限行為與資料可見性都不變(D12 開關預設關,既有部門指派仍等同全租戶有效) |
| 版本 2 | A.4 只記錄版+A.5 新前端 | 換一套權限畫面;可以自己開部門隔離;release note 預告下一版開始擋 |
| 版本 3 | A.4 真擋 | 沒被授權的一般角色開始被擋 |
| 時間待定 | FR-B | 專案層合併;開始時間由決策者與 PM 討論後定 |
⚠️ 升級相容:要拿真實舊庫排練,不是乾淨新裝
A.2 改能力點名字、搬角色資料,新裝環境永遠會過;會出事的是「帶著舊資料升級」。每版必須可直接升級不影響舊資料,所以 A.2 的驗收要拿一份隔一版以上的真實客戶舊庫跑 migrate,比對升級前後每個角色展開出來的能力點集合完全相同(逐角色 diff,不是抽樣)。
⚠️ 部門指派只在 D12 開關打開後才真正收窄
方式乙下,開關關著時部門指派等同全租戶有效,升級不改任何人的權限。D12 開關打開後部門指派才真正收窄,開啟前的預覽清單(8.3)會列出每個人會少看到什麼。
⚠️ test repo 連動:190 E2E 環境 82 筆指派全掛部門
190 E2E 環境的 82 筆角色指派全部掛在部門上。開關預設關,這些指派照舊等同全租戶有效,既有回歸測試不壞;但 D12 開啟後的行為(看不到別部門資料、改不了別部門資料、預覽清單)要另寫測試案,不能拿既有案子打開開關硬跑。
⚠️ 守門行為變更:版本 3 會擋到人
客戶的一般角色是客戶自己勾的,我們不知道他們勾了什麼。版本 2 只記錄期間的統計要隨 release note 交給客戶,否則版本 3 一上線就會接到「某某人突然不能用」的電話。
⚠️ 部門隔離開啟後,少看到的資料不會報錯
RLS 擋掉的列只是不出現在列表裡,沒有錯誤訊息。部門指派下錯一層、或 org_units.path 有誤,主管就會「以為部門沒有專案」。開啟前的預覽清單(8.3)與 path 回填(8.4)是兩道必要防線,不可省。背景流程要尊重 app.can_read_all_orgs,否則開了隔離的租戶裡排程工作會讀不到資料。
其他:
本節只寫範圍與風險,不展開設計。FR-B 另開時間由決策者與 PM 討論後定;FR-132 只替它預留一件事:
capabilities.assignable_scopes的值域含project。
要做什麼:把專案成員角色(manager/reviewer/auditor/viewer)收進同一套「誰 × 角色 × 範圍 × 時效」,範圍多一層 project;專案內的按鈕改由能力點判定,成員角色變成「一組 pattern 的預設角色」。
為什麼至少要等 A.1:專案內每個按鈕要對到哪個能力點,取決於 A.1 定出來的能力點樹;樹沒定,FR-B 的對照表無從寫起。
已知的難處(盤點 ②):
| 難處 | 規模 | 座標 |
|---|---|---|
| 硬寫的角色判斷 | 約 70 處,橫跨主專案與 6 個 jedi 套件(task_platform、compliance_audit、survey、evidence_classification、detection、flow_engine) | 盤點 ② 第 2 節 |
| 五張參與者表 | 專案、控制項群組、專案群組、控制項、流程各一張,role 欄同形 | 盤點 ② 第 1 節 |
| 回退規則 | control → group → project,下層蓋過上層;另有只認專案層的精確判法,兩者不可互相冒充 | participant_role_service.py:34-85、common/authz/project.py:28-90 |
| 流程綁角色寫在 XML 裡 | BPMN UserTask 的 main_role 屬性;stage_objects.default_main_roles 是後備 |
盤點 ② 第 3 節 |
| 進行中的流程是整份 XML 快照 | DEV 有 1150 筆 PROCESSING 實例,各有一份獨立的範本 XML;只改設計器範本不影響已建立的實例 | 盤點 ② 第 5 節 |
最後一條決定了 FR-B 的關鍵取捨:翻譯 main_role 必須連實例那份 XML 一起改,或在 _resolve_main_roles(app/flow_engine/service/stage_advance_service.py:584-591)做讀取時轉換。
common/authz/__init__.pydocs/analysis/2026-07-07-unified-auth-guard-design.mddocs/system-design/permission/Permission-System-權限與選單系統設計說明書.mdcommon/authz/license.pydocs/features/FR-062-2608-license-management/design.md 第 6 節core/plugins/identity.py:89-95