建議 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

欄位規格(design.md §6.1.1)

欄位/物件      型別                說明
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)   批次列表與清理都以租戶為前綴

怎麼做(逐步)

# 建議的 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