FR-155 · Project Role → RBAC — 需求討論稿 · 2026-10-21 · 待決策者裁 D1–D10

專案角色併入 RBAC

角色管理分「系統角色」「專案角色」兩個 tab,專案參與者改讀專案角色表

專案參與者今天是四個寫死的英文字(manager/reviewer/auditor/viewer),散在三個 repo 約一百多處,連流程圖的 XML 裡都有;系統角色卻是管理員自己建、自己勾能力點,兩邊沒有關係。本案把專案角色也變成「可建、可勾」:角色管理頁分系統/專案兩個 tab,各有各的能力點;專案參與者仍每個專案挑人,但下拉從專案角色表讀;程式裡所有「他是不是 manager」改成「他在這個專案的角色有沒有這個能力點」。同一人 A 專案負責人、B 專案瀏覽者照樣做得到。

已拍板:角色頁分兩個 tab、專案角色有自己的能力點、參與者仍每專案指派 D1–D10 待裁 雛形:prototype/index.html(三頁可點) 跨 BE/FE/6 支 jedi 套件、五張參與者表、流程圖 XML
§1

需求背景與目標

現況:兩套角色,其實是三個地方在寫死

層 現況 寫死在哪
系統角色 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 各一份)。

為什麼不能只把四個值搬進 system_menus

專案角色的四個值帶程式語意:流程引擎靠 auditor 決定誰能推進稽核階段,通知靠 reviewer 決定寄給誰。純粹把值搬進字典表,解決的是標籤與排序,解決不了「兩套角色」——管理員多加一個「資深稽核員」,程式不知道他該收哪種通知、能推哪個階段。

決策者已拍板的方向(2026-10-21 對話)

專案參與人員的設定還是要有,只是不給角色了,權限就是交給 RBAC 那邊的能力點處理。問完 PM,同一個人在不同專案要能有不同身分,所以角色管理的介面分兩個 tab,一個管系統、一個管專案,專案有專案的能力點設置,系統有系統的能力點設置。 — 決策者(2026-10-21)

本案要做的三件事

# 做什麼 白話
① 角色分兩種 角色管理頁分「系統角色」「專案角色」兩個 tab;專案角色勾的是「進了專案之後能做什麼」的能力點(管參與者、審核、稽核判定、推進階段、填答、看報告)
② 參與者指角色、不指字串 專案參與者仍每個專案挑人,但下拉從專案角色表讀;出貨內建四個範例(負責人/審核者/稽核員/瀏覽者)對應今天四個值,升級時舊資料自動對過去
③ 判定改查能力點 守門、通知收件人、流程推進、待辦清單全部改成「他在這個專案的角色展開後有沒有這個能力點」,不再比對 'manager'
§2

分工概述(30 秒版)

一句話:先把散掉的四個字收成一份並定出專案能力點清單(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 可以平行。

§3

已定案事項

✅ 決策者 2026-10-21 已拍板

  • 角色管理頁分兩個 tab:系統角色(今天那一套,不動)、專案角色(新)。
  • 專案角色有自己的能力點:管的是「進了專案之後能做什麼」。
  • 專案參與者設定保留:還是每個專案挑人,下拉從專案角色表讀;同一人不同專案可以不同角色(PM 確認需要)。
  • 判定改查能力點:所有「他是不是 manager」改成「他在這個專案的角色有沒有這個能力點」。
§4

待決策 D1–D10

D1 專案能力點的命名與粒度

今天系統能力點是三段式 <模組>.<資源>.<動詞>(例 grc.project.update),FR-132 已把 grc.* 38 個做出來,但那是「租戶層能不能碰這類東西」。專案內的動作要另一組名字,不能跟 grc.* 混用,否則「有 grc.audit.approve 的系統角色」會被誤判成「每個專案都能審核」。

選項:

  • (a) 新模組前綴 proj.*:例 proj.participant.manage、proj.review.approve、proj.audit.judge、proj.stage.advance、proj.survey.answer、proj.report.read。與 grc.* 一眼分得開,能力點表加 assignable_scopes=['project'] 就是專案能力點。
  • (b) 沿用 grc.* 但 assignable_scopes 標 project:名字相同、靠範圍欄分,畫面與 grep 都容易混。

建議 (a)。粒度照今天四個角色實際差異切,初版約 12~15 個,清單見第 5 節草案;B.1 盤點後定稿。

D2 出貨範例專案角色與對應

升級時舊資料的四個字要對到角色,所以每個租戶至少要有四個專案角色存在。

選項:

  • (a) 四個範例角色可改可刪,但有「預設負責人」旗標:負責人/審核者/稽核員/瀏覽者各一,客戶可改名、可改能力點、可加新角色;其中一個標為「預設負責人」(建專案時建立者自動指派、manager 全域覆寫用),不可刪。
  • (b) 四個全鎖不可改:客戶要自訂只能複製。

建議 (a),與 FR-132 D6 的精神一致(系統管理員鎖、範例可改)。

D3 參與者表怎麼改

五張參與者表(project_participants、project_group_participants、project_control_participants、control_group_participants、process_participants)的 role 都是 varchar 文字欄。

選項:

  • (a) 新增 project_role_id FK 指 roles.id,舊 role 欄並存一版後退役:升級腳本把 role='manager' 對到該租戶的「負責人」角色 id;舊欄留著,回滾不必動資料。與 FR-132 D2 同手法。
  • (b) 直接改 role 欄語意為角色 uid:回滾要動資料。

建議 (a)。control_group_participants 與 project_group_participants 同形,B.1 順便確認是否其一已無人用。

D4 系統管理員進專案算什麼

系統角色 *.*.* 是租戶內全權,但專案能力點是另一組名字(D1 a),*.*.* 展開後會不會包含 proj.*?

選項:

  • (a) 不包含:系統管理員要在專案裡做事,一樣要被加成參與者、指派專案角色。不是參與者就看不到專案(與今天相同)。通知不會因為他是管理員就每封都收。
  • (b) 包含:管理員進任何專案都是全權,每種通知都收,稽核員清單會列他。

建議 (a)。判定時 proj.* 只從「這個專案的參與者指派」展開,不從 user_roles 展開;程式上就是兩個入口各查各的。

D5 群組層、控制項層的回退規則

今天 get_user_role 由下往上找:控制項層有指派用它,沒有找群組層,再沒有用專案層,回傳第一個找到的角色。

選項:

  • (a) 規則不變,比對對象換成能力點:由下往上找到第一筆指派,展開它的角色,看有沒有要求的能力點。
  • (b) 改成聯集:三層指派的角色全部展開取聯集。

建議 (a),行為與今天一致,風險最低;(b) 會讓「控制項層降級成只能看」做不到。

D6 稽核範圍(內稽/外稽)跟著誰

project_participants.audit_scope(both/internal/external)今天只對 auditor 有意義:通知與可推進階段會比對稽核性質。

選項:

  • (a) 欄位留在參與者列,判定時只對「有稽核判定能力點」的參與者生效:不綁角色名,綁能力點。
  • (b) 搬到角色上:角色定義「只做內稽」,同一人兩個專案要不同範圍就要兩個角色。

建議 (a)。

D7 專案內按鈕的前端判定

今天 FE 專案頁用 p.role === 'manager' 判按鈕(ProjectPlanningView.vue 9 處、ProjectAuditorOverview.vue 6 處…)。

選項:

  • (a) 專案詳情 API 回「我在這個專案的能力點清單」(my_capabilities),FE 新增 v-can-project="PROJ.X" 指令:與 FR-132 A.6 的 v-can 同形,權限名由 BE 產常數檔。
  • (b) FE 自己拉角色定義展開。

建議 (a)。回退規則在 BE 算好,FE 不重算。

D8 流程圖 main_role 怎麼翻

BPMN UserTask 的 main_role 今天存角色名(auditor),進行中實例有約 4,400 份 XML 快照。客戶改了角色名字、或加了新角色,角色名不能再當程式依據。

選項:

  • (a) main_role 改存能力點名(例 proj.audit.judge),舊值讀取時換算:_resolve_main_roles 遇到舊四值就對到對應能力點;設計器範本與 stage_objects.default_main_roles 用 migration 改成能力點;實例 XML 不碰。
  • (b) 改存角色 uid:客戶刪角色流程就壞。
  • (c) 全部 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) 參與者表、enum、serializer 的 owner 是 jedi_task_platform,改在套件;其他五支只改呼叫端參數(角色 tuple → 能力點名):走 poetry path dependency 開發,完成後一次發版。
  • (b) 主專案 monkeypatch:第二套真相。

建議 (a)。套件異動照 CLAUDE.md 規範先列清單等你點頭,B.1 盤點後給。

D10 分版

整案橫跨三 repo、六支套件、五張表、約 4,400 份 XML。

選項:

  • (a) 兩版:第一版 B.1+B.2+B.3+B.4(角色頁兩個 tab、參與者讀表、判定改能力點),流程圖 main_role 第一版只做讀取換算不改編輯器;第二版 B.5 編輯器改選能力點。
  • (b) 一版全做。

建議 (a)。第一版完成客戶就能自訂專案角色;流程編輯器改選項是錦上添花。

§5

做出來長什麼樣

三頁雛形在 prototype/index.html,純前端假資料,用現有設計系統刻。

頁 入口 新增/改變
1 角色管理(兩個 tab) 系統設定 / 存取管理 / 角色 頁頂多「系統角色|專案角色」兩個 tab;專案角色 tab 列四個範例角色+自訂角色,欄位:名稱、說明、能力點數、使用中專案數、預設負責人標記
2 專案角色詳情 點任一專案角色 與系統角色詳情同形(基本資料卡、能力點矩陣、儲存列),但矩陣只列 proj.*、沒有「可見選單預覽」、多一個「預設負責人」開關
3 專案參與者 專案 / 設定 / 參與者 角色下拉從專案角色表讀,含自訂角色;稽核員才顯示稽核範圍欄;下拉旁一行「管理專案角色」連去頁 1

專案能力點清單草案(D1 定稿後 B.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 的人。

§6

資料模型草案

%%{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
圖 1 — 專案角色併入後的資料關係(粗線是本案新增)
表 動作
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(實例快照)不碰,讀取換算
§7

判定流程

%%{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
圖 2 — 一次「按下審核」的判定

守門入口維持 common/authz/project.py,四支函式簽名改收能力點名:assert_project_capability(cap, ...)(回退版)、assert_project_capability_exact(cap, ...)(精確版)、assert_project_participant 不變(任一參與者)。舊四支留 deprecated shim 一版,內部把 allowed_roles 對到能力點。

§8

現況盤點:改哪裡

元件 現況 本案動作 階段
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)
§9

風險與不做

⚠️ 三個最可能出事的點

  • 升級對照:舊資料四值→角色 id 要每租戶各對一次;租戶沒建範例角色(例如用 SQL 直接灌的測試租戶)升級腳本要先補建。排練用真實舊庫(memory:封包前排練真實舊庫)。
  • SQL 讀模型效能:原本一個 role = 'manager' 變成 join 展開表。做法是先查「這個租戶哪些專案角色有能力點 X」(角色數很少,可快取),再 project_role_id IN (...),查詢形狀不變。
  • 流程實例快照:4,400 份 XML 不改,讀取換算要有測試覆蓋四值與逗號多值;新建流程範本存能力點名後,舊換算層一版後才能拆。

明確不做:專案角色的部門範圍、起訖日(參與者指派本來就是專案範圍、無時效);拒絕規則;把專案角色匯出 JSON(第二版再看);process_participants(子流程參與者)若 B.1 盤點確認無人讀 role 欄就不改。