下面整段是給執行 session 的 prompt。前置:Phase 2b 與 2b-2 都已收完(2c 與它們動同一批
app/grc/service/檔案,必須序列執行;2b-2 見同目錄2026-07-07-phase2b2-remainder-prompt.md)。未收完就停下回報。
【接手主題】FR-048 統一授權守門 Phase 2c — common/authz/project.py 泛化 assert_project_role,grc 7 份私有 _check_* 複製品改為委派(行為保真、零行為變更)
必讀:
docs/features/FR-048-2607-unified-auth-guard/endpoint-authz-matrix.md §4 B 類(本波 scope 定義)docs/analysis/2026-07-07-unified-auth-guard-design.md §2 軸模型common/authz/project.py 現況【branch】fix/v1.8.0-bugs,禁止切 branch。不動 jedi-* 套件。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【本波鐵則】
【7 份複製品與語意差異表(2026-07-07 實查,執行前先驗證仍準確——2b 可能又動過這些檔)】
| # | 檔:行 | method | 入口 | 判定 | 特殊點 |
|---|---|---|---|---|---|
| 1 | app/grc/service/audit_service.py:27 |
_check_role(project_uid, user_id, required_role) |
uid→resolve+404 | exact match,無 manager override | error_map 按 required_role 選碼 |
| 2 | app/grc/service/poam_app_service.py:91 |
_check_manager(project_id, user_id) |
id | manager-only | |
| 3 | app/grc/service/audit_round_app_service.py:99 |
_check_role(project_id, user_id, required_role) |
id | required_role 含 manager override | 錯誤碼按 required_role 選 NOT_MANAGER/NOT_AUDITOR |
| 4 | app/grc/service/job_import_service.py:81 |
_check_manager_role(project_uid, user_id) |
uid→resolve+404 | manager-only | |
| 5 | app/grc/service/task_execution_service.py:47 |
_check_manager(project_uid, user_id) |
uid→resolve+404 | manager-only | 回傳 project entity |
| 6 | app/grc/service/assessment_plan_app_service.py:230 |
_check_auditor(project_id, user_id) |
id | auditor+manager override | |
| 7 | app/grc/service/assessment_result_app_service.py:111 |
_check_auditor(project_id, user_id) |
id | auditor+manager override |
⚠️ 查法差異(不可混):這 7 份都是「project 層 participant 精確查詢」(participant_domain_service.get_one(ProjectParticipantQueryEntity(project_id, user_id)))。既有 canonical assert_project_manager 走的是 role_service.get_user_role(control→group→project 由下往上 fallback)——兩者語意不同,不能把 7 份改成走 assert_project_manager。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【要做的事】
common/authz/project.py 新增泛化 canonicaldef assert_project_role(
participant_domain_service,
project_id: int,
user_id: int,
allowed_roles: tuple = ("manager",),
error_code=None,
) -> None:
"""Project 層 participant 精確查詢守門(不含 group/control fallback——那是
assert_project_manager 的軸)。participant 為 None 或 role 不在 allowed_roles → ForbiddenError。
error_code 未給時按 allowed_roles 選預設:含 "auditor" → GRC_NOT_AUDITOR,否則 GRC_NOT_MANAGER。
manager override 由 caller 用 allowed_roles=("auditor", "manager") 表達,本函式不隱含 override。
caller 必須在 @transaction scope 內。
"""assert_project_manager(fallback 查法)的分工,兩支並列在 __init__.py 決策表。def _check_manager(self, project_id: int, user_id: int) -> None:
# FR-048 2c:委派 canonical,語意同前(project 層精確查詢、manager-only)
assert_project_role(self._participant_domain_service, project_id, user_id)allowed_roles=("manager",);#6/#7 → ("auditor", "manager");#3 → required_role=="manager" 時 ("manager",)、=="auditor" 時 ("auditor", "manager")(保住 override 語意與錯誤碼選擇);#1 見下。#1 的 exact match 沒有 manager override,與 #3/#6/#7 不同調(同是 required_role 語意)。這可能是 bug 也可能刻意:
allowed_roles=(required_role,) 保住無 override 的現行為;pytest test/(用 python -m pytest)全綠比對:baseline worktree 集合 diff(P3 review 用過的方法)——在 2c 前 commit 開 worktree 跑同套件,comm 比對 FAILED/ERROR 清單,新增失敗必須為 0。【commit】一個 commit 即可(收編性質單一):refactor(FR-048): grc 7 份私有 _check_* 收編為 common.authz assert_project_role 委派(行為保真)。顯式 git add、禁 -am、不 push。
【收尾】commit 完停下,回報:① 一句話 status;② audit_service override 不一致案(等 user 定調);③ 若 2b 期間又長出新的私有 check helper,列出來。收尾動作等 user 下令。