本卡屬 FR-095(母卡見本 arc README front matter),第 3 棒 P2:任務指派+六張參與者表資料存取+migration(jedi 套件 repo)。只掃不修。
「這個任務指派給誰」的 API(新增/更新/刪除指派、查指派清單),加上六張成員相關資料表的資料存取層(repo,就是「真正下 SQL 查表」的那層)、資料表模型與建表的 migration(建表腳本)。共 35 檔。只找問題、不修問題。
守門是在 service 層做的,但資料真正被讀出來是在 repo 層——如果 repo 的查詢沒有「限定在某個專案內」的條件,service 層守門再對,換一個參數就能撈到別的專案。而這六張表沒有一張有租戶欄、隔離全關(migration 檔頭自陳刻意),資料庫層零兜底。把「守門條件」與「查詢條件」與「表結構」放同一棒,一眼就能看完三層是不是全空。
這些是首腦讀過程式碼後認為最容易出事的地方,是思考起點不是檢查清單。每條後面的「首腦核對」是首腦親自開檔的結果——✅ 屬實的不必重做核對,直接當錨點追下去;標「待 runner 核對」的才要你自己開檔確認。工具沒答的自己開檔查了再寫,標明「(工具未報,人工查證)」:
participant/app/service/task_assignee_service.py:213-214 原文 if existing is not None and existing.project_id is not None: assert_project_manager(...)——existing 撈不到、或 project_id 為空,就整段跳過;刪除那支 :247-250 有專案交叉核對,更新沒有。為什麼可疑:拿一個不存在的組合(control_id/task_id/user_id)就不會被擋,接著往下走到 validate 與寫入。首腦核對:✅ 屬實。要追:跳過之後真的會寫入嗎、寫入的 project_id 從哪來。POST /task-assignees 只驗登入、task_id/user_id 呼叫端自填(疑點 #7 的第 5 支):task_assignee_service.py 的 get_task_assignees(**kwargs) 與 P1 那四支同款。為什麼可疑:登入+知道任務編號就能查誰負責這個任務(含帳號、暱稱、部門)。首腦核對:✅ 屬實(同 P1 的 get_all 形狀)。participant/infra/repository/*_repo_impl.py 六支+user_enrichment.py。要逐支看:get_all/get_one/list_by_* 有沒有任何一支在 query entity 沒帶 project_id 時就回全表;user_enrichment.py 把 user 資料 join 進來時有沒有跨租戶撈到別家帳號。為什麼可疑:表沒租戶欄,「沒帶條件就全表」=跨客戶。(工具未報時人工查證,交對照表)migrations/001-participant-tables.sql:16-21 寫「本套件的表沒有 RLS、也沒有 tenant_id 欄位……租戶隔離由上游的專案層擋(participants 一律先經 project_id 收斂)」。首腦查過 projects 的隔離是關的(總表第 43 條)。要答:「一律先經 project_id 收斂」這句在程式碼裡是不是真的每條路都成立——上面兩條疑點已經是反例。002-participant-fks.sql 的外鍵有沒有 ON DELETE 行為讓刪專案時留下孤兒指派。task_assignee_service.py:186-190 batch_add_task_assignees stub 恆回 [],對應 route POST /task-assignees/batch。首腦核對:✅ FE src/config/api/api.js:128 仍定義常數但 grep src 無呼叫者。報告非資安段記一筆「可刪」即可,不算發現。participant/domain/ports.py 的介面契約:IProjectRoleGuard 等 port 定義了宿主要提供什麼——看有沒有方法被標 Optional 而讓宿主「少給一個也能過」。授權判定怎麼流(首腦 2026-09-14 開檔核對,行號可直接打開):
common/authz/project.py(120 行):assert_project_manager :16-30/assert_project_role_fallback :33-57/assert_project_role :60-98/assert_project_participant :101-120。守門函式本身沒有 fail-open(fail-open 就是「查不到就放行」的壞習慣):查不到角色就 raise、沒有拿租戶代替專案、沒有 super_admin 短路。participant/app/service/participant_role_service.py:33-83,三層往上找:project_control_participants → control_group_participants → project_participants → 都沒有就回 None。core/plugins/participant.py:90-102 的 adapter → 套件 participant/common/guard.py:26-52 的 configure();沒接線時 :45-49 直接 raise、不放行(這一段是好的)。task 那一半的三道守門(license/project_role/identity)在 core/plugins/task.py:89-98,與 jedi-compliance-audit 共用同一組 adapter。participant/api/guards.py:10-12 的 CAPABILITIES 是空 tuple,入口只有宿主注入的 jwt_required(只驗「你有登入」)。