FR-155 · Project Role → RBAC — 需求討論稿 · 2026-10-21 · 待決策者裁 D1–D10
角色管理分「系統角色」「專案角色」兩個 tab,專案參與者改讀專案角色表
專案參與者今天是四個寫死的英文字(manager/reviewer/auditor/viewer),散在三個 repo 約一百多處,連流程圖的 XML 裡都有;系統角色卻是管理員自己建、自己勾能力點,兩邊沒有關係。本案把專案角色也變成「可建、可勾」:角色管理頁分系統/專案兩個 tab,各有各的能力點;專案參與者仍每個專案挑人,但下拉從專案角色表讀;程式裡所有「他是不是 manager」改成「他在這個專案的角色有沒有這個能力點」。同一人 A 專案負責人、B 專案瀏覽者照樣做得到。
| 層 | 現況 | 寫死在哪 |
|---|---|---|
| 系統角色 | roles 表+role_capability_patterns(萬用字元)+user_roles(租戶/部門範圍、起訖日)。管理員在「存取管理」頁自己建角色、勾能力點 |
無,FR-132 已做成資料驅動 |
| 專案角色 | ParticipantRole 四值 StrEnum(manager/reviewer/auditor/viewer),存五張參與者表的 role 文字欄 |
BE 主專案 28 支檔、6 支 jedi 套件、FE 約 30 支檔 |
| 流程管理 | BPMN UserTask 的 main_role 擴充屬性寫在 XML 裡;stage_objects.default_main_roles 當預設 |
設計器範本 58 處、進行中實例快照約 4,400 處(DEV)、FE 流程編輯器選項寫死四值 |
專案角色寫死的樣態分五類,修法各不同:
| 樣態 | 例子 | 數量(BE 主專案) |
|---|---|---|
| 守門參數 | allowed_roles=("auditor", "manager")、ParticipantRole.MANAGER |
約 70 處呼叫(大多已走 common/authz/project.py 的四支 canonical 函式) |
| 原生 SQL 讀模型 | pp.role IN ('auditor','manager') |
8 支 infra/readmodel/ 查詢(我的稽核待辦、改善計畫儀表板、結案報告清單、到期提醒、偵測通知…) |
| 通知收件人 | stage_entered_recipients.py 自訂 MANAGER = "manager";批次完成通知、偵測通知各寫一遍 |
5 處 |
| 序列化與驗證 | API serializer 的 OneOf([...])、Excel 匯入驗證、套件 serializer |
4 份清單各自維護 |
| 業務語意 | 建立者自動 manager、manager 可推進任何階段、auditor 才有 audit_scope(內稽/外稽) |
散在 service |
另外 enum 有兩份相同複本(主專案 common/enum/participant_enum.py 與 jedi_task_platform),FE 的角色標籤與顏色對照是第三份(ParticipantPicker.vue、ProjectAuditorOverview.vue、FlowTemplateEditorView.vue 各一份)。
專案角色的四個值帶程式語意:流程引擎靠 auditor 決定誰能推進稽核階段,通知靠 reviewer 決定寄給誰。純粹把值搬進字典表,解決的是標籤與排序,解決不了「兩套角色」——管理員多加一個「資深稽核員」,程式不知道他該收哪種通知、能推哪個階段。
專案參與人員的設定還是要有,只是不給角色了,權限就是交給 RBAC 那邊的能力點處理。問完 PM,同一個人在不同專案要能有不同身分,所以角色管理的介面分兩個 tab,一個管系統、一個管專案,專案有專案的能力點設置,系統有系統的能力點設置。 — 決策者(2026-10-21)
| # | 做什麼 | 白話 |
|---|---|---|
| ① | 角色分兩種 | 角色管理頁分「系統角色」「專案角色」兩個 tab;專案角色勾的是「進了專案之後能做什麼」的能力點(管參與者、審核、稽核判定、推進階段、填答、看報告) |
| ② | 參與者指角色、不指字串 | 專案參與者仍每個專案挑人,但下拉從專案角色表讀;出貨內建四個範例(負責人/審核者/稽核員/瀏覽者)對應今天四個值,升級時舊資料自動對過去 |
| ③ | 判定改查能力點 | 守門、通知收件人、流程推進、待辦清單全部改成「他在這個專案的角色展開後有沒有這個能力點」,不再比對 'manager' |
一句話:先把散掉的四個字收成一份並定出專案能力點清單(B.1),再改資料表與升級腳本(B.2),改判定與通知(B.3),最後畫面(B.4)與流程管理(B.5)。看得見的畫面排在 B.4 不是因為它不重要,是它要等 B.2 的表才有東西可讀;雛形先給你看,排序以你何時看得到畫面為準。
| 階段 | 做什麼(白話) | 產出 | 完成怎麼判定(決策者可親手檢查) | 依賴 |
|---|---|---|---|---|
| B.1 收斂與清單 | 四個字只留一份定義,其他地方都去讀它;定出專案能力點清單(每個「專案內按鈕」對到一個能力點);對照表「今天四個角色各自有哪些能力點」 | 能力點對照表、enum 單一來源、守衛測試擋字面值 | 跑 pytest test/test_project_role_literal_guard.py 綠;打開對照表每個專案內的動作都有一列、四個範例角色每個都看得出勾了什麼 |
D1、D2 |
| B.2 資料地基 | roles 加種類欄(系統/專案);專案能力點標 project 範圍;五張參與者表 role 改指角色;升級腳本替每個租戶建四個範例專案角色、把舊資料對過去 |
migration、seed、拿真實舊庫跑過的升級排練紀錄 | 拿一份客戶舊庫升級後,開任一專案的參與者頁,每個人的角色名跟升級前一樣;用舊帳號登入能按的按鈕跟升級前一模一樣 | B.1、D3、D4 |
| B.3 判定與通知 | 守門四支函式改查專案能力點;8 支 SQL 讀模型、5 處通知改同一個查法;建立者自動指派改讀「預設負責人角色」 | 判定層新版、讀模型改寫 | 建一個「只能看、不能審核」的自訂專案角色指派給某人:他進專案看得到,按審核回 403,批次完成通知不會寄給他;把審核能力點勾回去,立刻能按、會收信 | B.2、D5、D6 |
| B.4 畫面 | 角色頁兩個 tab;專案角色詳情頁的能力點矩陣只列專案能力點;參與者下拉讀表;專案內按鈕改讀「我在這個專案的能力點」 | FE 四處改動 | 對照雛形三頁;在專案角色 tab 新增「資深稽核員」勾三個能力點,到任一專案參與者下拉看得到它,指派後該人進專案只看得到那三個動作的按鈕 | B.3、D7 |
| B.5 流程管理 | 流程圖 UserTask 的「主要角色」改成選能力點;stage_objects.default_main_roles 改存能力點;舊 XML 讀取時換算 |
流程編輯器改動、換算層 | 開流程編輯器點一個任務節點,「主要角色」下拉列的是能力點不是四個字;進行中的舊專案推進階段行為不變 | B.3、D8 |
B.1→B.2→B.3 必須照順序;B.4 與 B.5 可以平行。
✅ 決策者 2026-10-21 已拍板
D1 專案能力點的命名與粒度
今天系統能力點是三段式 <模組>.<資源>.<動詞>(例 grc.project.update),FR-132 已把 grc.* 38 個做出來,但那是「租戶層能不能碰這類東西」。專案內的動作要另一組名字,不能跟 grc.* 混用,否則「有 grc.audit.approve 的系統角色」會被誤判成「每個專案都能審核」。
選項:
proj.*:例 proj.participant.manage、proj.review.approve、proj.audit.judge、proj.stage.advance、proj.survey.answer、proj.report.read。與 grc.* 一眼分得開,能力點表加 assignable_scopes=['project'] 就是專案能力點。grc.* 但 assignable_scopes 標 project:名字相同、靠範圍欄分,畫面與 grep 都容易混。建議 (a)。粒度照今天四個角色實際差異切,初版約 12~15 個,清單見第 5 節草案;B.1 盤點後定稿。
D2 出貨範例專案角色與對應
升級時舊資料的四個字要對到角色,所以每個租戶至少要有四個專案角色存在。
選項:
建議 (a),與 FR-132 D6 的精神一致(系統管理員鎖、範例可改)。
D3 參與者表怎麼改
五張參與者表(project_participants、project_group_participants、project_control_participants、control_group_participants、process_participants)的 role 都是 varchar 文字欄。
選項:
project_role_id FK 指 roles.id,舊 role 欄並存一版後退役:升級腳本把 role='manager' 對到該租戶的「負責人」角色 id;舊欄留著,回滾不必動資料。與 FR-132 D2 同手法。role 欄語意為角色 uid:回滾要動資料。建議 (a)。control_group_participants 與 project_group_participants 同形,B.1 順便確認是否其一已無人用。
D4 系統管理員進專案算什麼
系統角色 *.*.* 是租戶內全權,但專案能力點是另一組名字(D1 a),*.*.* 展開後會不會包含 proj.*?
選項:
建議 (a)。判定時 proj.* 只從「這個專案的參與者指派」展開,不從 user_roles 展開;程式上就是兩個入口各查各的。
D5 群組層、控制項層的回退規則
今天 get_user_role 由下往上找:控制項層有指派用它,沒有找群組層,再沒有用專案層,回傳第一個找到的角色。
選項:
建議 (a),行為與今天一致,風險最低;(b) 會讓「控制項層降級成只能看」做不到。
D6 稽核範圍(內稽/外稽)跟著誰
project_participants.audit_scope(both/internal/external)今天只對 auditor 有意義:通知與可推進階段會比對稽核性質。
選項:
建議 (a)。
D7 專案內按鈕的前端判定
今天 FE 專案頁用 p.role === 'manager' 判按鈕(ProjectPlanningView.vue 9 處、ProjectAuditorOverview.vue 6 處…)。
選項:
my_capabilities),FE 新增 v-can-project="PROJ.X" 指令:與 FR-132 A.6 的 v-can 同形,權限名由 BE 產常數檔。建議 (a)。回退規則在 BE 算好,FE 不重算。
D8 流程圖 main_role 怎麼翻
BPMN UserTask 的 main_role 今天存角色名(auditor),進行中實例有約 4,400 份 XML 快照。客戶改了角色名字、或加了新角色,角色名不能再當程式依據。
選項:
main_role 改存能力點名(例 proj.audit.judge),舊值讀取時換算:_resolve_main_roles 遇到舊四值就對到對應能力點;設計器範本與 stage_objects.default_main_roles 用 migration 改成能力點;實例 XML 不碰。建議 (a)。判定變成「使用者在這個專案的能力點是否包含 UserTask 要求的能力點」;manager 全域覆寫改成「有 proj.stage.advance-any 能力點」。
D9 jedi 套件的改法
守門 canonical 在主專案 common/authz/project.py,6 支套件透過 adapter 轉發,但套件內 serializer 與 enum 也寫死四值(jedi_task_platform、jedi_compliance_audit、jedi_survey、jedi_evidence_classification、jedi_detection、jedi_flow_engine)。
選項:
建議 (a)。套件異動照 CLAUDE.md 規範先列清單等你點頭,B.1 盤點後給。
D10 分版
整案橫跨三 repo、六支套件、五張表、約 4,400 份 XML。
選項:
main_role 第一版只做讀取換算不改編輯器;第二版 B.5 編輯器改選能力點。建議 (a)。第一版完成客戶就能自訂專案角色;流程編輯器改選項是錦上添花。
三頁雛形在 prototype/index.html,純前端假資料,用現有設計系統刻。
| 頁 | 入口 | 新增/改變 |
|---|---|---|
| 1 角色管理(兩個 tab) | 系統設定 / 存取管理 / 角色 | 頁頂多「系統角色|專案角色」兩個 tab;專案角色 tab 列四個範例角色+自訂角色,欄位:名稱、說明、能力點數、使用中專案數、預設負責人標記 |
| 2 專案角色詳情 | 點任一專案角色 | 與系統角色詳情同形(基本資料卡、能力點矩陣、儲存列),但矩陣只列 proj.*、沒有「可見選單預覽」、多一個「預設負責人」開關 |
| 3 專案參與者 | 專案 / 設定 / 參與者 | 角色下拉從專案角色表讀,含自訂角色;稽核員才顯示稽核範圍欄;下拉旁一行「管理專案角色」連去頁 1 |
| 能力點 | 白話 | 負責人 | 審核者 | 稽核員 | 瀏覽者 |
|---|---|---|---|---|---|
proj.project.read |
看專案 | ✓ | ✓ | ✓ | ✓ |
proj.project.update |
改專案設定、刪專案 | ✓ | |||
proj.participant.manage |
增刪參與者、指派任務 | ✓ | |||
proj.job.execute |
執行任務、上傳佐證、填答 | ✓ | |||
proj.job.manage |
增刪改任務、批次完成不限指派 | ✓ | |||
proj.review.approve |
標記已審閱、退回 | ✓ | ✓ | ||
proj.audit.judge |
稽核判定、AP 編輯、AO 結果 | ✓ | ✓ | ||
proj.stage.advance |
推進自己負責的階段 | ✓ | ✓ | ✓ | |
proj.stage.advance-any |
推進任何階段、強制推進 | ✓ | |||
proj.poam.manage |
改善計畫建立與結案 | ✓ | |||
proj.report.read |
看結案報告、匯出 | ✓ | ✓ | ||
proj.report.manage |
建立與發布結案報告 | ✓ | |||
proj.evidence.manage |
證據分類批次操作 | ✓ | |||
proj.ssp.update |
SSP 各區塊寫入 | ✓ |
通知收件人規則跟著能力點:「請審閱」寄給有 proj.review.approve 的人;「稽核待辦」列有 proj.audit.judge 的人;階段進站通知寄給有該站要求能力點的人,加上有 proj.stage.advance-any 的人。
%%{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 sys[系統層(今天就有)]
U[users] --> UR[user_roles<br/>租戶/部門範圍]
UR --> R[roles<br/>+ kind: system / project<br/>+ is_default_owner]
R --> RP[role_capability_patterns]
RP --> C[capabilities<br/>grc.* assignable_scopes=tenant,org_unit<br/>proj.* assignable_scopes=project]
end
subgraph proj[專案層(本案改)]
P[projects] --> PP[project_participants<br/>+ project_role_id → roles<br/>audit_scope 留著]
PP -. 展開 .-> RP
PG[project_group_participants<br/>+ project_role_id] --> R
PC[project_control_participants<br/>+ project_role_id] --> R
end
PP --> R
| 表 | 動作 |
|---|---|
roles |
加 kind(system/project,預設 system)、is_default_owner(每租戶恰一個專案角色為 true) |
capabilities |
新增 proj.* 列,assignable_scopes=['project']、module='proj' |
| 五張參與者表 | 加 project_role_id FK → roles.id;role 欄並存一版 |
stage_objects |
default_main_roles 值改能力點名 |
flow_templates.bpmn_xml |
migration 把 main_role 四值改能力點名;workflow_templates.xml(實例快照)不碰,讀取換算 |
%%{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 FE as 前端
participant API as 後端 route
participant S as app service
participant G as 專案能力點守門
participant DB as 資料庫
FE->>API: POST /review/mark(帶 control_uid)
API->>S: mark_reviewed(control_uid)
S->>DB: resolve 控制項 → project_id, group_id
S->>G: assert_project_capability("proj.review.approve", project_id, group_id, control_id)
G->>DB: 由下往上找第一筆參與者指派(控制項→群組→專案)
DB-->>G: project_role_id
G->>DB: 展開該角色的 pattern(Redis 快取)
DB-->>G: {proj.project.read, proj.review.approve, ...}
G-->>S: 通過/ForbiddenError/非成員 NotFound
S-->>FE: 200 或 403 或 404
守門入口維持 common/authz/project.py,四支函式簽名改收能力點名:assert_project_capability(cap, ...)(回退版)、assert_project_capability_exact(cap, ...)(精確版)、assert_project_participant 不變(任一參與者)。舊四支留 deprecated shim 一版,內部把 allowed_roles 對到能力點。
| 元件 | 現況 | 本案動作 | 階段 |
|---|---|---|---|
common/enum/participant_enum.py+jedi_task_platform 同名 |
兩份相同 enum | 只留套件那份,主專案 re-export;守衛測試擋 app/infra/api 出現 'manager' 等字面值 |
B.1 |
common/authz/project.py 四支守門 |
比對 allowed_roles tuple |
改收能力點名;舊簽名留 shim 一版 | B.3 |
participant_role_service.get_user_role(jedi_task_platform) |
回角色字串 | 回 project_role_id,新增 get_user_project_capabilities 展開 |
B.3 |
8 支 infra/readmodel/ SQL(pp.role IN (...)) |
字面值比對 | 改 join 展開表或先查「有此能力點的角色 id 集合」再 IN |
B.3 |
stage_entered_recipients.py、job_batch_complete_query.py、detection_job_notify_query.py |
各自比角色 | 統一走「有能力點 X 的參與者」查法 | B.3 |
project_start_app_service.py(建立者自動 manager) |
寫 'manager' |
寫該租戶 is_default_owner 角色 |
B.3 |
api/project/serializers/project.py、excel_parser/validators.py、套件 serializer |
OneOf 四值 |
改驗角色 uid 存在於該租戶專案角色 | B.3 |
stage_advance_service._resolve_main_roles |
讀 XML main_role 四值 |
讀取換算:四值→能力點;判定改能力點 | B.3(換算)/B.5(編輯器) |
stage_objects.default_main_roles |
角色名 | migration 改能力點名 | B.2 |
FE views/access/RoleList.vue、RoleDetail.vue |
單一列表 | 兩個 tab;專案角色詳情矩陣只列 proj.* |
B.4 |
FE ParticipantPicker.vue、ProjectParticipantsPanel.vue |
四值寫死 | 下拉讀 GET /project-roles(租戶的專案角色) |
B.4 |
FE 專案頁約 30 處 p.role === 'manager' |
字面值 | v-can-project+專案 API 回 my_capabilities |
B.4 |
FE FlowTemplateEditorView.vue roleOptions |
四值寫死 | 下拉讀 proj.* 能力點 |
B.5 |
| 6 支 jedi 套件呼叫端 | allowed_roles=("manager",) |
改能力點名參數 | B.3(走 path dependency) |
⚠️ 三個最可能出事的點
role = 'manager' 變成 join 展開表。做法是先查「這個租戶哪些專案角色有能力點 X」(角色數很少,可快取),再 project_role_id IN (...),查詢形狀不變。明確不做:專案角色的部門範圍、起訖日(參與者指派本來就是專案範圍、無時效);拒絕規則;把專案角色匯出 JSON(第二版再看);process_participants(子流程參與者)若 B.1 盤點確認無人讀 role 欄就不改。