建議 model:opus/effort:medium — 要 grep 全部寫入者、設計回填 SQL、寫 RLS policy,改錯會讓既有上傳功能全掛。
本卡屬 FR-107(母卡 CM-1843)第 .1 棒(子需求卡 CM-1844),子任務 T-1.1:把系統存所有上傳檔的那張表補上「這份檔屬於哪家公司、誰上傳的」,並讓資料庫自己擋跨租戶讀取。
系統所有上傳的檔案都記在一張表 upload_files 裡。這張表現在不知道每份檔屬於哪家公司(租戶)、也不知道是誰上傳的,而且沒有開 RLS(Row Level Security,資料列層級權限——讓 PostgreSQL 自己在每次查詢時擋掉不屬於你的資料列)。
以前這樣沒出事,是因為這張表靠「掛它的表」間接保護:例如任務證據表 job_evidences 有 RLS,你查不到別家的證據列,自然也拿不到對應的檔案編號。
但 FR-107 要拿這張表當暫存區用——使用者上傳一批還沒分類、還沒掛任何任務的檔。這批檔沒有任何表掛著它,間接保護完全不存在,任何人拿到檔案編號就能讀別家公司的證據。
所以這張卡要補三樣:tenant_id(哪家公司)、owner_user_id(誰上傳的)、RLS 四條 policy。並且把既有的資料列回填租戶編號——查不到歸屬的孤兒列一律歸 ROOT(tenant_id=1),migration 要輸出回報有幾筆(D10 裁示)。
這不是加分項,是 FR-107 准不准上線的門檻。
套件(走 path dependency 開發)
~/Projects/Jedicogy/module/jedi-python-package/jedi-file-upload/
.../infra/models/upload_file.py:16 ← 加 tenant_id / owner_user_id 兩個欄位
.../domain/ports.py:54 IUploadFileProvider ← 介面不加方法(見下方「不要做的事」)
對應 entity / mapper 也要同步加欄位
主專案
scripts/sql/packages/jedi_file_upload/00N-upload-files-tenant-owner-rls.sql ← 新增(N 接現有最大號)
app/upload_file/service/managed_file_upload_service.py:324 upload_files_for_tenant() ← canonical 寫入路徑
(還要 grep 出全部其他寫入者,見「怎麼做」②)
RLS 範本(照抄它的冪等寫法)
scripts/sql/packages/jedi_asset/002-asset-rls-grants.sql:49-75
欄位/物件 型別 說明
tenant_id int NOT NULL 回填「之後」才加 NOT NULL
owner_user_id int NULL 上傳者;既有列留空
RLS ENABLE ROW LEVEL SECURITY + 四條 policy(select/insert/update/delete)
照 jedi_asset/002 的 pattern:
is_super_admin OR public.app_tenant_allowed_for_session(tenant_id)
🔴 例外:storage_scope='system' 的列,select 放行給所有租戶
(框架匯入的共享資產,FR-042 語意不可變——改壞會讓框架匯入的
公版資源跨租戶看不到,而且不會報錯)
索引 (tenant_id, created_at) 批次列表與清理都以租戶為前綴
jedi-file-upload 的 infra/models/upload_file.py 加 tenant_id(int,NOT NULL)與 owner_user_id(int,nullable),對應的 domain entity 與 mapper 同步。欄位加 comment= 說明(ORM comment 會灌進 DB COMMENT,是 ERD 的單一來源)。tenant_id/owner_user_id。這一步是本卡最容易漏的地方——grep SQL 關鍵字抓不到 ORM 寫入,要同時 grep session.add/merge/flush 與 repo 的 create/save。已知至少四處:managed_file_upload_service.py(canonical,upload_files_for_tenant() 在 :324)、Google Drive 匯入、合規框架匯入、detection(檢測工具)回收。實際清單以 grep 為準,不要只信這四個。# 建議的 grep(在主專案與兩支套件都跑)
grep -rn "upload_files\|UploadFile\|save_file" app/ infra/ core/ \
~/Projects/Jedicogy/module/jedi-python-package/*/src/
grep -rn "session.add\|session.merge\|\.create(\|\.save(" | grep -i upload
INSERT public.schema_migrations。倒過來會炸(既有列沒值就加 NOT NULL 直接失敗)。upload_files.id/uid 的表(本卡 runner 用 grep -rn "upload_files\|file_id\|file_uid" infra/ ~/Projects/Jedicogy/module/jedi-python-package/*/src/*/infra 實查),逐表 UPDATE ... FROM 回填。已知鏈路至少含 job_evidences → workflow_execution → 專案 → 租戶。storage_scope='system' 的列歸 ROOT。剩下的孤兒也歸 ROOT(tenant_id=1)。RAISE NOTICE '孤兒列 N 筆歸 ROOT',把實際筆數寫進本卡回寫(D10 明文要求)。psql -h localhost -p 5432 -U cmmgr -d guidant_ai_dev --single-transaction -v ON_ERROR_STOP=1 -f <檔>。只套 DEV。