FR-042 合規框架與客戶儲存空間脫鉤,改由系統部門集中管理

狀態:設計定案(待開實作 session)|提出:2026-06-28|來源:雷門 Notion case:合規框架與客戶儲存空間脫鉤,改由系統部門集中管理(任務清單 DB) relates to FR-039(檔案儲存抽象 / RemoteAgent)、FR-024(框架 PDF 匯入 v2)

1. 背景 / 需求

合規框架是產品資產,應由系統持有者(系統部門 / 最高層 tenant)集中維護,不下放給客戶 user。框架不應綁在客戶儲存空間。具體兩個需求:

  1. 所有 tenant 都讀得到框架(定義 + PDF)。
  2. 框架 PDF 要跨 tenant 抓得到 — 目前 PDF 上傳到系統儲存後,其他 tenant 拿不到系統儲存 config,導致要不到 PDF。這是主要待解問題。

2. 現況機制(已驗證)

元件 現況 是否符合需求
框架/catalog 定義表(oscal.oscal_frameworks / oscal_framework_versions / catalogs / catalog_controls 系統層、無 tenant_id、無 RLS、全客戶共用 需求 1 定義層已成立(實作前 psql 複查一次 RLS
框架 PDF 實體檔(upload_files,在 jedi-file-upload 套件 無 tenant_id,只記 storage_type 字串,不記後端連線(bucket/endpoint/drive) ❌ 核心問題所在
匯入暫存 oscal.framework_parse_jobs tenant-scoped(tenant_id + RLS) 暫存安全

真正的根因(一句話)app/upload_file/service/managed_file_upload_service.py:51_load_config()上傳和下載時都system_config(group=STORAGE_CONFIG, key=CONFIG) 讀儲存設定,而該 config 是 tenant-scoped(吃 RLS)

連鎖反應:

  1. 系統部門 admin 上傳框架 PDF → 落在 admin tenant 後端
  2. upload_files 列無 tenant_id、不記是哪個後端。
  3. 別的 tenant 下載 → _load_config() 解析它自己STORAGE_CONFIG → 指向它自己的後端 → 檔案不在 → 抓不到。

儲存解析跟著「呼叫者」走,但檔案實體跟著「上傳者」走,兩者對不上。

關聯鏈(migration 盤點用):oscal.framework_versions.file_uid(= framework_parse_job.file_path 存的 upload uid)→ public.upload_files.uid

⚠️ 實作注意:jedi-oscal ORM __tablename__oscal_framework_versions,但 DB 實際表是 oscal.framework_versions(無 oscal_ 前綴,FR-038 重設計後落差)。寫 migration / 直接 SQL 以實際表名為準。

環境普查(2026-06-28,回答 pre-flight ①「有沒有正式框架檔」)

環境 框架 有 PDF 版本 published+PDF 後端分布
DEV 1 1 1 remote_agent ×1
STG 1 5 2 minio ×2、remote_agent ×3
POC 1 3 2 remote_agent ×3

結論:migration 不可省,且主力在 remote_agent(FR-039 分散式客戶 agent)。 minio 可 server 端 copy;remote_agent 的 bytes 在客戶端 agent 上,搬遷需「透過 agent fetch 回來 → push 到系統儲存」,跨網路、依賴 agent 在線、需 mTLS+JWT 認證——這是整條 migration 最硬的部分,難度維持「高」。

底層 tenant 為物化路徑階層set_tenant_path trigger + parent_id),root tenant = parent_id IS NULL;有 is_super_admin 可繞 RLS(Tenant ORM 在 jedi-auth,實作時確認確切 locator)。

3. 定案決策(2026-06-28 與雷門確認)

面向 決定
系統儲存形態 框架 PDF 掛在最高層 tenant 名下,用它的 STORAGE_CONFIG 當系統儲存;下載以 super-admin context 讀,繞呼叫者 RLS(不新增游離於 RLS 外的全域 config)
標記機制 upload_filesstorage_scope(system/customer,預設 customer),框架檔標 system
既有檔 寫 migration 一次性把 bytes 從舊後端搬到系統儲存 + 改標記(dev/stg/poc 各跑一次,SQL 走 cmmgr)
下載存取 框架 PDF 下載端補 @jwt_required登入即可讀、不分 tenant(產品共享資產)

設計取向:以「storage_scope」為主軸,但讓「系統儲存來源」可日後收斂成更通用的 owner_tenant_id(scope=system ≡ owner=最高層 tenant),不必重做。

4. 實作輪廓(4 塊)

  1. 套件 schema 變更(jedi-file-upload) ⚠️ 動外部套件

    • upload_filesstorage_scope(system/customer,預設 customer);UploadFile model + mapper + entity 同步。
    • dev 階段走 poetry path dependency,feature 完成才發版(依 CLAUDE.md jedi-* 規範)。
  2. 儲存解析(主專案 ManagedFileUploadService

    • 新增 system-scope 來源:以 super-admin context 讀「最高層 tenant 的 STORAGE_CONFIG」建 adapter。
    • 框架匯入上傳:寫系統儲存 + 標 scope=system
    • 下載/預覽:依「檔案的 scope」解析後端(system → 系統儲存;其餘 → 維持現行 caller-tenant 解析)。
  3. Migration

    • oscal_framework_versions.file_uid 對應的 upload_files → bytes 從舊後端搬到系統 bucket → 改 storage_scope + 必要的 save_file_name/path
    • dev/stg/poc 各跑一次;收尾 INSERT public.schema_migrations
  4. 下載 auth

    • 框架 PDF 下載 route 補 @jwt_required、登入即可讀。
    • 先確認是否走共用 /file/download/<uid>;若共用,避免誤傷其他用途(可能需區分框架專用唯讀 route 或用 storage_scope 判斷)。

5. Pre-flight(實作 session 開工前驗)

  1. psql 實查框架定義表 RLS(需求 1 是否真已成立 — case 說已確認,但探查報告有矛盾)。
  2. jedi-auth 裡「最高層 tenant」確切 locator。
  3. 下載 route 是否共用、補 auth 的影響面。

6. 風險 / 待確認

  • 動 jedi-file-upload 套件 schema → 影響其他 consumer(所有用 upload_files 的模組);加欄位有預設值、向後相容,風險低但需報備。
  • 「最高層 tenant 的 STORAGE_CONFIG」必須先在系統儲存(獨立於客戶前綴的 bucket/folder)配好。
  • migration 搬 bytes 需確認舊後端(local/minio/remote_agent)逐型搬法。
  • 維護權限(import/publish 限 system-admin、客戶端唯讀)本 FR 暫未納入,視需要另議或併入。

7. Pre-flight 驗證結果與校正(2026-06-28 實作 session)

實查 dev(188/guidant_ai_dev)後對定案的修正:

design 假設 實際 處置
oscal.oscal_frameworks / oscal_framework_versions 實際 oscal.frameworks / oscal.framework_versions(無 oscal_ 前綴) 表名校正
定義表系統層、無 RLS frameworks/framework_versions/catalogs/catalog_controlsrls=f 無 tenant_id 需求 1 已成立,不動
system_config 實際 public.system_configs(複數),有 tenant_id + RLS=t 根因確認;讀 root config 須繞 RLS
最高層 tenant = parent_id IS NULL ⚠️ 兩筆:id=1 Guidant.AI(/1/)、id=134 測試豬戶A(/134/) 定位改為「parent_id IS NULL最小 id」= id=1(與雷門確認)
既有框架檔分布 dev 僅 1 筆:framework_versions id=178,落 remote_agent 192.168.50.123:8443 bytes 搬移見 §8 待辦(b)

前置依賴(未完成):root tenant(id=1)目前無 STORAGE_CONFIG。雷門將於「系統設定 → 儲存設備」以系統管理員手動配一份;配好前整條 end-to-end 無法實測。

8. 實作決策與結果(取代 §3 部分細節)

面向 定案 → 實作
系統檔標記 方案 A:jedi-file-upload upload_filesstorage_scope(system/customer,預設 customer,向後相容)
系統 root 定位 parent_id IS NULL 取 min(id) = id=1 Guidant.AI(編碼於 system_storage_config_reader
root config 讀法 另開獨立 SessionLocal()app.is_super_admin='t' 讀單列,不污染呼叫端請求 session 的 RLS 狀態(infra 層 raw SQL)
雙 adapter 解析 ManagedFileUploadService 依「檔案自己的 storage_scope」選 adapter:system→系統 adapter(root 儲存)/ customer→呼叫者 adapter。下載/預覽 route 不需改(LOCAL 系統檔走 route 直讀檔案系統;MINIO/REMOTE_AGENT 走 service 內部 scope 解析)
框架上傳 framework_parse_job_service 改呼叫 upload_system_file() → 寫系統儲存 + 標 scope=system
Block 4 下載 auth ⚠️ 與現況衝突,未實作:共用 /file/download/file/pdf-preview 故意無 auth(FE 用 window.open/iframe,瀏覽器原生 GET 無法帶 bearer,靠 unguessable uuid 當憑證)。加 @jwt_required打爆所有預覽/下載含框架。核心需求 #2 由 storage_scope 解析解決,不靠 auth。建議不加,待雷門拍板(若要鎖需配套改 FE 用 fetch+blob 帶 bearer)

已實作檔案

jedi-file-upload 套件(dev path dependency,未發版)

  • common/code/storage_scope_code.py(新)— StorageScopeCode.SYSTEM/CUSTOMER
  • infra/models/upload_file.py — 加 storage_scope 欄(NOT NULL server_default 'customer')
  • domain/entities/upload_file_entity.py / infra/mapper/upload_file_mapper.py / app/dto/upload_file.py — 同步
  • infra/repository/upload_file_repo_impl.py — add/update 持久化 storage_scope
  • ports/.../upload_file_provider.py + infra/adapter/local|miniosave_file(..., storage_scope='customer')
  • app/service/file_upload_service.pyupload_file/upload_files(..., storage_scope=...)

主專案

  • infra/upload_file/system_storage_config_reader.py(新)— super-admin 讀 root tenant STORAGE_CONFIG
  • infra/upload_file/remote_agent_adapter.pysave_file 簽名 + entity 同步
  • app/upload_file/service/managed_file_upload_service.py — system adapter + scope-aware get/convert/delete + upload_system_file
  • app/oscal/service/framework_parse_job_service.py — 框架 PDF 改 upload_system_file
  • scripts/sql/2026-06-28-fr042-upload-files-add-storage-scope.sql(新,可立即跑 dev/stg/poc)

待辦 / gated

  • (a) 欄位 migration:可立即跑(cmmgr,dev/stg/poc 各一次)。
  • (b) 既有框架檔 bytes 搬移:gated 於 root STORAGE_CONFIG 配好。跨後端(remote_agent→系統儲存)非純 SQL。dev 僅 1 筆,最簡單是配好後重新匯入框架;stg/poc/prod 有實資料才需正式搬移腳本(pull old backend bytes → upload_system_file 重寫 → 更新 framework_versions.file_uid)。
  • 套件發版:feature 完成 + 雷門明示後才 bump jedi-file-upload 推 Nexus,主專案改回 pin。
  • Block 4 auth:待拍板(建議不加)。