狀態:設計定案(待開實作 session)|提出:2026-06-28|來源:雷門 Notion case:合規框架與客戶儲存空間脫鉤,改由系統部門集中管理(任務清單 DB) relates to FR-039(檔案儲存抽象 / RemoteAgent)、FR-024(框架 PDF 匯入 v2)
合規框架是產品資產,應由系統持有者(系統部門 / 最高層 tenant)集中維護,不下放給客戶 user。框架不應綁在客戶儲存空間。具體兩個需求:
| 元件 | 現況 | 是否符合需求 |
|---|---|---|
框架/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)。
連鎖反應:
upload_files 列無 tenant_id、不記是哪個後端。_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 以實際表名為準。
| 環境 | 框架 | 有 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)。
| 面向 | 決定 |
|---|---|
| 系統儲存形態 | 框架 PDF 掛在最高層 tenant 名下,用它的 STORAGE_CONFIG 當系統儲存;下載以 super-admin context 讀,繞呼叫者 RLS(不新增游離於 RLS 外的全域 config) |
| 標記機制 | upload_files 加 storage_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),不必重做。
套件 schema 變更(jedi-file-upload) ⚠️ 動外部套件
upload_files 加 storage_scope(system/customer,預設 customer);UploadFile model + mapper + entity 同步。儲存解析(主專案 ManagedFileUploadService)
STORAGE_CONFIG」建 adapter。scope=system。Migration
oscal_framework_versions.file_uid 對應的 upload_files → bytes 從舊後端搬到系統 bucket → 改 storage_scope + 必要的 save_file_name/path。INSERT public.schema_migrations。下載 auth
@jwt_required、登入即可讀。/file/download/<uid>;若共用,避免誤傷其他用途(可能需區分框架專用唯讀 route 或用 storage_scope 判斷)。實查 dev(188/guidant_ai_dev)後對定案的修正:
| design 假設 | 實際 | 處置 |
|---|---|---|
oscal.oscal_frameworks / oscal_framework_versions |
實際 oscal.frameworks / oscal.framework_versions(無 oscal_ 前綴) |
表名校正 |
| 定義表系統層、無 RLS | ✅ frameworks/framework_versions/catalogs/catalog_controls 全 rls=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 無法實測。
| 面向 | 定案 → 實作 |
|---|---|
| 系統檔標記 | 方案 A:jedi-file-upload upload_files 加 storage_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/CUSTOMERinfra/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_scopeports/.../upload_file_provider.py + infra/adapter/local|minio — save_file(..., storage_scope='customer')app/service/file_upload_service.py — upload_file/upload_files(..., storage_scope=...)主專案
infra/upload_file/system_storage_config_reader.py(新)— super-admin 讀 root tenant STORAGE_CONFIGinfra/upload_file/remote_agent_adapter.py — save_file 簽名 + entity 同步app/upload_file/service/managed_file_upload_service.py — system adapter + scope-aware get/convert/delete + upload_system_fileapp/oscal/service/framework_parse_job_service.py — 框架 PDF 改 upload_system_filescripts/sql/2026-06-28-fr042-upload-files-add-storage-scope.sql(新,可立即跑 dev/stg/poc)cmmgr,dev/stg/poc 各一次)。upload_system_file 重寫 → 更新 framework_versions.file_uid)。