FR-065.0 DB 初始化基線——盤點報告(CM-1207 盤點棒)

  • 日期:2026-08-15
  • 性質:唯讀盤點。本文件是「從零建庫需要什麼」的完整清單與方案草案,不含任何已執行的 DB 異動
  • 資料源:DEV DB 192.168.50.188:25432/guidant_ai_dev(帳號 cmmgr,全程唯讀:pg_dump --schema-onlySELECT、catalog 查詢)+ BE repo scripts/sql/ + jedi-auth 套件源碼。
  • 母案:CM-1189(FR-065 落地版 Installer);本卡 CM-1207。
  • 路徑註記:派工單寫 docs/features/FR-065-2608-installer/,但 docs/features/README.md 登記名為 FR-065-2608-onprem-installer,依 FR 編號鐵則採 README 登記名。

1. Schema 全量盤點

1.1 總量(DEV 實況,2026-08-15 pg_dump 為準)

項目 數量 備註
PostgreSQL 版本 16.4 Debian 套件版
Schema 5 public(57 表)、compliance(45)、oscal(58)、config(11)、survey(15)
Table 186 含 2 張分區母表(public.api_logspublic.system_logs)與其月分區子表
Sequence 156
Index 579
View 6 v_role_routesv_user_capabilitiesv_user_routesvw_user_job_queuedebug_org_unitsv_tenant_parent_debug(後兩支為 debug 用,是否出貨待裁決)
Function 82(去除 extension 後約 40 支自訂) app_* RLS helper 一族、trg_* trigger function 一族、get_user_task_queue(SP)、maintain_log_partitionsscope_rankset_tenant_path
Trigger 18 touch/validate/fill-tenant 類
Event trigger 1 trg_auto_grant_cm_app(見 §2)
RLS 啟用表 29 policy 共 107 條
Extension uuid-ossp 1.1、pg_trgm 1.6(+內建 plpgsql) init 必裝
pg_dump --schema-only 31,119 行 含 400 行 GRANT、18 行 ALTER DEFAULT PRIVILEGES、107 行 CREATE POLICY

分區維護:maintain_log_partitions() 負責 api_logs / system_logs 月分區滾動;init 時需建立母表+當月分區+default 分區(DEV 現況有 2026_05~2026_08 各月子表——歷史月份子表不屬基線,init 只需母表+default+當期)。

1.2 版位追蹤三張表現況

現況 init 基線角色
public.schema_migrations 138 筆(filename / applied_at / note),首筆 __baseline_pre_2026-05-28__ 是上次基線化的 marker 基線版位標記主體——init 完成時應把「已收斂進基線的 migration 檔名」全數登記(或登記一筆新 baseline marker),讓後續增量 migration 判斷起點(方案見 §5)
public.schema_version 2 筆(1.4.0 / 1.5.0 semver stamp) init 收尾補一筆基線版號
public.alembic_version 1 筆 f20ca741a9bd ⚠️ 待裁決:repo 內無 alembic 目錄,此值來源不明(推測是 legacy 或某 jedi 套件早期 migration 殘留)。需查明是否有任何程式在讀它;若無 → 基線不帶此表

1.3 對照 scripts/sql/ migration 串

  • scripts/sql/ 有 224 支 .sqlmanifest.tsv(CM-1086 建,四欄:filename/phase/envs/note)。
  • manifest 分類:pre-baseline 83 筆(永不套,2026-05-28 baseline 之前的歷史)+ active 138 筆。active 中 127 筆 envs=*、11 筆是環境限定(dev/stg/poc 專屬——如 DEV cleanup、STG cutover 類,不屬產品基線)。
  • DEV schema_migrations 登記 138 筆與 manifest active 段一致(含 __baseline_pre_2026-05-28__ marker 與 2 支無日期前綴的 oscal-v2 檔)。
  • 結論:基線收斂的「真相」取 DEV pg_dump 實況;manifest.tsv 已提供哪些 migration 屬環境專屬(不進基線)的判準,收斂時逐支核對 envs 欄即可。
  • 隨附資產:scripts/sql/view/vw_user_job_queue.sqlscripts/sql/store_procedure/sp_get_user_task_queue.sql(view/SP 的 canonical 源,dump 內已含定義)、scripts/sql/seeds/bpmn/ 4 支內建 BPMN(builtin-full-audit / builtin-full-audit-with-review / builtin-internal-check / builtin-self-assessment,對應 flow_templates 內建流程)。

2. Table 權限現況 diff(cm_app)

2.1 掃描結果:目前零缺口

has_table_privilege / has_sequence_privilege / has_function_privilege 對 cm_app 全量掃描(5 schema、186 表、156 sequence、6 view、全部 function):

  • 表缺 SELECT/INSERT/UPDATE/DELETE 任一者:0 張
  • Sequence 缺 USAGE:0 支
  • View 缺 SELECT:0 支;Function 缺 EXECUTE:0 支

「歷史 migration 常漏 GRANT」的坑在 DEV 已被兩層機制補平:

  1. ALTER DEFAULT PRIVILEGES:五個 schema 都設了(grantor=cmmgr)——tables SELECT,INSERT,UPDATE,DELETE、sequences USAGE,SELECT,UPDATE、functions EXECUTE 自動授予 cm_app。之後由 cmmgr 建的新物件天生有權限。
  2. Event trigger trg_auto_grant_cm_appauto_grant_cm_app_on_new_schema()):CREATE SCHEMA 時自動 GRANT USAGE +補設該 schema 的 default privileges。

基線 init 的正確做法不是逐表 GRANT 400 行,而是複製這套機制:建 schema 後先設 default privileges,再建物件;event trigger 一併帶入。逐表 GRANT 僅作為驗收檢查(掃描 SQL 可直接沿用本次盤點語句)。

2.2 DB 角色現況與 RLS 關係

Role 屬性 用途
cmmgr LOGIN, SUPERUSER, CREATEDB, CREATEROLE, BYPASSRLS;DB owner 管理帳號:套 migration、init。RLS 對它無效
cm_app LOGIN(無任何特權,受 RLS);member of cm_app_group 應用程式連線帳號(.env DB_USER)
cm_app_group NOLOGIN 群組 目前僅 cm_app 一個成員;群組自身未見額外授權(實際授權都直接給 cm_app)——基線可簡化或保留,待裁決(傾向保留以利未來多 app 帳號)
其他(ammgr/nicsmgr/camunda/audit_manager_mgr 同主機他系統/legacy 角色 不屬本產品基線,init 不建

RLS 運作方式:107 條 policy 全部掛在 public(即對所有非 BYPASSRLS 角色生效),判斷條件不是 DB role 而是 session GUC——app.user_idapp.allowed_tenant_pathsapp.is_super_admin 等,由 session_scope() 每次查詢前注入(helper:app_tenant_allowed_for_session() 一族)。因此:

  • cm_app 的資料隔離完全靠「應用層注入 GUC + policy」實現,不依賴 per-tenant DB 帳號
  • cmmgr 因 BYPASSRLS 可做系統層寫入(seed、migration);
  • init 對 RLS 的責任=把 29 張表的 ENABLE ROW LEVEL SECURITY + 107 條 policy + app_* helper functions 原樣建齊(皆已含在 pg_dump 內)。
  • 另注意 pg_db_role_setting 為空——沒有任何 DB/role 層級的 GUC 預設值,不需搬。

3. Seed 資料逐表分類草案

分類定義:①產品必備=新客戶空庫必 seed;②環境相依=installer 參數化或安裝後由管理介面設定;③DEV 殘留=不隨產品出貨。業務資料表(projects / ssp / 評鑑 / survey 答題 / job 執行 / log 類)在新庫本來就該是空的,不逐一列出——DEV 裡它們有資料屬開發測試產物,全數不出貨。

3.1 ①產品必備(字典/系統定義類,值不因客戶而異)

DEV 行數 判斷理由
public.operations 8 操作類型字典(VIEW/INSERT/…/VERIFY),程式 enum 對應
public.system_menus 73 16 組下拉選單字典(ANS_TYPE/DEVICE_TYPE/STORAGE_TYPE/TASK_TYPE…),UI 依賴
public.ui_routes 53 前端選單樹(真選單來源),有 unique(name) 約束
public.capabilities 130 權限能力字典(is_platform 24/一般 106),FR-048 授權體系核心
public.route_capabilities 136 路由↔︎capability 綁定
public.tenants 只取 id=1 root tenant(Guidant.AI,path /1/)——B-2 模型:系統級資源掛 root,「tenant_id NULL=壞資料」。各環境 root 固定 id=1 是硬約定(migration 直接寫字面值 1),init 必須保證
public.org_units 只取 id=1 root org unit(同上,id=1 硬約定)
public.rolesrole_capabilities 只取 root 的 Administrator(DEV id=2,掛 130 條 capability) 平台管理員角色。其餘 10 個 role 皆 tenant-scoped(DEV/測試租戶的),不出貨
public.usersuser_tenants/user_org_units/user_roles 只取 1 個超管帳號 初始 admin(見 §4);密碼不出廠寫死,安裝時產生
public.system_configsRUNTIME_CONFIG 7 筆(tenant 1) 登入/密碼/JWT 運行參數(CHANGE_PASSWORD_THRESHOLD_DAYS=90、JWT_EXPIRES、LOGIN 鎖定、MFA_REQUIRED),FR-063.1g 已有 seed migration 2026-08-14-fr063-1g-runtime-config-seed.sql 可直接復用
public.system_configsWEB_IDEL_CONFIG 1 筆 前端閒置逾時預設值
config.detection_tools 8 檢測工具定義(openvas/nessus/sonarqube/openscap/zap/inspec(cinc)/nmap/gcb),FR-056~058 一整串 seed migration 的收斂結果
config.detection_tool_param_schemas 14 各工具參數 schema
compliance.flow_templates 只取 tenant 1 的 4 筆 內建稽核流程範本(對應 scripts/sql/seeds/bpmn/ 4 支 builtin BPMN);tenant 102 的 9 筆是 DEV 自建測試
public.schema_migrations / schema_version init 收尾 stamp(§1.2、§5)

3.2 ②環境相依(installer 參數化,或出廠帶「未啟用」空殼)

DEV 行數 說明
public.system_configsSMTP 1 寄信主機——安裝時填或裝完後台設定;出廠建議帶 disabled 空殼
public.system_configsSTORAGE_CONFIG 3(tenant 1/102/131 各一) MinIO/儲存後端座標——只出貨 root(tenant 1)一筆且值參數化(bucket/endpoint 隨客戶環境)
public.system_configsTHIRD_PARTY_LOGIN(LDAP) 2 客戶自家 AD/LDAP——不出廠帶值,後台設定
config.tenant_licenses / tenant_license_events / public.license_clock_watermark 25/29/1 FR-062 license 導入後 runtime 自建——init 不 seed,首次匯入 license 時產生(watermark 亦 runtime 初始化)
初始 admin 密碼 安裝時隨機產生或安裝者輸入(§4/§5)

3.3 ③DEV 殘留(不出貨)

  • 測試租戶整族:tenants 102(Billows Tech)/131(歐洲航空)/152/153/158(JEDI) 及其 org_units、roles、users(blsadmin/blsit/e2e_* 等 38 個帳號)、module_frames 154 筆、flow_templates 9 筆、surveys、devices、labels 泰半、issues/feedback_issues、drive_* 整族、totp_secrets、tenant_drive_integrations。
  • 業務資料:projects 210、job_executions 1 萬+、workflow_executions、SSP/AP/AR/POA&M 全族、question_answers、upload_files 2668、login_tokens/login_logs、api_logs/system_logs 分區資料。
  • 明確垃圾表survey.question_answers_dedup_backup_20260428config.detection_tool_profiles_deprecated_20260803public.system_logs_old——基線 schema 不帶這三張(連結構都不帶)。
  • public.system_configsNOTIFY_CONFIG(tenant 102 的 Discord/Telegram webhook,內含真實 secret)——不出貨。

3.4 ⚠️ 待裁決清單(拿不準,留給決策者)

# 項目 拿不準的點
D1 CMMC 框架 catalog 整族oscal.frameworks 1 / framework_versions 3 / catalogs 79 / catalog_groups 296 / catalog_controls 980 / catalog_control_parts 5651 / catalog_control_params,加 oscal.metadata/profiles/profile_imports 中屬框架的部分) 是產品資產沒錯,但 DEV 的 79 個 catalog 混雜「內建 CMMC」與「開發測試匯入的」,無法從 schema 判斷哪些屬出貨集。且出貨形式有兩路:(a) seed dump 直灌;(b) 出廠帶 OSCAL JSON 檔、首次啟動走既有匯入流程(見 §5 方案 C 註)。需要 user 指認出貨框架清單
D2 內建 workflow_templates(tenant NULL、provider=Billows-Official 的控制項任務範本+snapshot 的凍結快照,共 191 筆;另 tenant 1 有 59 筆) provider=Billows-Official 看起來是內建 canonical,但 snapshot 類是專案運行產物;且它們與 D1 的框架綁定。需一併裁決出貨範圍
D3 樣板 SSP/MF defaults 家族compliance.module_frames tenant 1/158 各 1 筆、oscal.system_security_plans 中 template 性質者;FR-036 已把 defaults 遷進 template SSP) 新客戶初始該有哪些「內建模組框架+樣板 SSP」?DEV 現況分不出「產品內建」vs「開發試作」
D4 檢測 Profile 庫config.detection_profiles tenant 1 的 10 筆+detection_profile_versions 11/_controls 4841/_taxonomies 13) 資料列看是產品資產(dev-sec 基準+TWGCB 8 支),但 profile 版本的檔案實體存在 storage(MinIO),DB seed 灌了列、檔案不在就是壞連結——出貨機制需連檔案一起解(隨 installer 帶檔上傳?or 首次啟動重新上傳?)
D5 public.labels 35 筆 前段(系統回饋/系統操作/選單>專案管理…)像產品字典,後段(textbook-、resource-)像特定專案用——需逐筆指認
D6 system_configsISSUE_INTEGRATE_CONFIG(GITHUB/GITLAB,enable=false 空殼) 功能是否對客戶開放?開放則出廠帶 disabled 空殼,否則不 seed
D7 public.alembic_version 來源不明(repo 無 alembic);查無 reader 則基線不帶
D8 root tenant/org/admin 的出廠命名 DEV 值是 Guidant.AI/Guidant.AI Manage/admin——正式出貨是否沿用(或 installer 讓客戶命名 root tenant)?
D9 debug view(debug_org_units/v_tenant_parent_debug)與 cm_app_group 空群組 出貨基線帶不帶
D10 DB 命名:新標準 <系統>_{dev|stg}、prod 不帶後綴 → 落地版 DB 名應為 guidant_ai?installer 預設值需定案 2026-08-08 新制說「guidant_ai_* 舊制沿用不回頭改」,但落地新客戶算新建置,適用新制與否待拍板

4. 帳號與密碼機制現況

4.1 DB 帳號(cmmgr / cm_app)

  • 建立紀錄不在 repo 內scripts/sql/ 與 docs 全量 grep 無 CREATE ROLE cmmgr/cm_app——推測(標註:推測)是當年 DB server 手動建立,屬 cluster-level 操作本來就不進單庫 migration。→ init 機制必須自己補上這塊(首次在客戶主機建 cluster 帳號)。
  • 現況屬性見 §2.2。關鍵設計事實:cmmgr=superuser+BYPASSRLS 的管理帳號、cm_app=受 RLS 的應用帳號,兩帳號同密碼是 DEV 慣例,出貨不應沿用(installer 應分別產生)。
  • RLS 不依賴帳號屬性以外的東西:只要應用連線帳號 BYPASSRLS/superuser,policy 就生效;GUC 注入由應用層負責。
  • ⚠️ 出貨層面注意:cmmgr 目前是 SUPERUSER——若客戶端 DB 是客戶自管(RDS 類),SUPERUSER 拿不到。基線最低需求其實是「DB owner + CREATEROLE + BYPASSRLS」,init 設計時建議降規(列入 §5 考量,實作棒驗證)。

4.2 應用層帳號(users 表)

  • 密碼儲存:bcrypt $2b$12$(cost 12),per-user saltusers.salt 欄(bcrypt.gensalt()),hash 60 字元存 users.password。DEV 39 個帳號全部同一格式,無 legacy 雜湊。實作在 jedi-auth user_domain_service.add_user()
  • DEV 超管現況admin(tenant 1,is_super_admin=true,DEV id=16)+blsadmin(tenant 102 的租戶超管)。admin 的建立同樣查無 seed SQL(推測 legacy 手動/早期工具建立);後續由 2026-06-29-b2-admin-root-tenant.sql 把它掛進 root tenant(users.tenant_id→1 + user_tenants + user_org_units 三筆,B-2 模型)。init 建初始 admin 時這三件事要一次做齊,否則重演「super admin 寫入 tenant_id=NULL 孤兒」bug。
  • is_super_admin 語意:RLS policy 以 GUC app.is_super_admin='t' 放行全庫,值由登入時從 users.is_super_admin 帶出。

4.3 首登改密/密碼治理(機制已存在,init 可直接復用)

jedi-auth 已內建完整機制,init 不必新造:

  • FIRST_REGISTER 變更請求add_user() 流程會自動產 12 碼複雜密碼(generate_complex_password(12))+建立 user_change_password_request(type=FIRST_REGISTER,7 天效期)+寄密碼信。登入時有 pending request 會被導去改密頁;change_password_by_request() 走密碼政策驗證(≥12 碼+大小寫+數字,CM-683)。
  • 密碼過期RUNTIME_CONFIG.CHANGE_PASSWORD_THRESHOLD_DAYS(預設 90)+ type=PASSWORD_EXPIRE 請求。
  • MFARUNTIME_CONFIG.MFA_REQUIRED + totp_secrets 表。
  • 初始 admin 的「隨機產生+強制首登改密」拼圖現成:init 只需以 bcrypt 正確格式寫入 users+一筆 FIRST_REGISTER change request(效期建議放寬),或最簡單——密碼由 installer 產生後印給安裝者、同時 stamp 一筆過期的 threshold 逼首登改密。細節屬實作棒設計。

5. Init 機制方案建議(只提案不實作)

5.0 共同前提(不管選哪案都成立)

  • 基線真相=DEV pg_dump,不是 224 支 migration 重放。歷史串重放不可行:早期檔互相覆蓋、部分含 DEV 專屬資料修補、環境限定檔混雜。
  • 權限不逐表 GRANT:按 §2.1,建 schema →設 default privileges →建物件的順序天生全對,再帶 event trigger 保未來。
  • 層次切分:cluster 層(roles、database、extension)與 database 層(schema/物件/seed)必須分開——前者需要連 postgres 庫的權限,後者連目標庫。
  • 冪等性:所有 seed 用自然鍵 ON CONFLICT DO NOTHING / INSERT ... WHERE NOT EXISTS;schema 段以「schema_migrations 有無 baseline marker」判斷跳過整段。root tenant/org id=1 硬約定需在空庫 seed 時顯式指定 id 並 setval sequence。
  • 收尾 stamp:init 完成時往 schema_migrations 登記(建議登記一筆 __init_baseline_v<版號>__ marker + 把已收斂的 active migration 檔名全數補登),讓日後增量 migration 與三環境 diff 工具無縫接軌;schema_version 補基線版號。這與既有 sql-migration 慣例(檔頭 Date、--single-transaction、cmmgr 執行)完全相容——基線之後的新 migration 照舊制走,不受影響

5.1 方案 A:單一 init.sql(大一統檔)

pg_dump schema 段+seed 段+stamp 段串成一支,installer 用 psql --single-transaction -v ON_ERROR_STOP=1 一發套完。

  • ✅ 最簡單、單一交易原子性、與現行 migration 套法零學習成本。
  • ❌ 3 萬行大檔難維護難 review;參數注入(admin 密碼、storage 座標)得靠 psql -v 變數穿插在巨檔裡,易錯;cluster 層(CREATE ROLE/DATABASE 不能在交易內、要連不同庫)根本塞不進同一支——實務上一定會裂成「殼腳本+大 SQL」,那不如直接選 B。

5.2 方案 B:分段腳本組(建議傾向)★

scripts/init/
  00-cluster.sql        # CREATE ROLE cmmgr(降規版)/cm_app/cm_app_group、CREATE DATABASE(連 postgres 庫,殼腳本執行)
  01-extensions.sql     # uuid-ossp、pg_trgm
  02-schema.sql         # 由 pg_dump --schema-only 產出後整理(去 DEV 殘留表、去歷史月分區)
  03-grants.sql         # default privileges ×5 schema + event trigger +(驗收用逐項掃描)
  04-seed-core.sql      # §3.1 字典/權限/選單/root tenant+org/內建流程範本
  05-seed-catalog.sql   # D1~D4 裁決後的框架/profile 資產(可能改走匯入流程,見下)
  06-admin.sql          # 初始 admin(bcrypt hash 由外部算好以 -v 傳入)+ B-2 三件套
  99-stamp.sql          # schema_migrations baseline marker + schema_version
  init.sh               # 依序執行、參數注入(env vars)、冪等判斷、失敗即停
  • ✅ 每段可獨立 review/重跑;參數注入面窄(只有 00/04/06 吃參數);cluster/database 層天然分離;與 manifest.tsv 的維護思路一致;基線升版時只 regenerate 02/03,seed 段不動
  • ❌ 段間非單一交易(02~99 可各自包交易,但整體失敗需 DROP DATABASE 重來——init 場景可接受,殼腳本做好「失敗即整庫重建」語意即可)。
  • 觸發者:installer 觸發(呼叫 init.sh),理由見 5.3。

5.3 方案 C:entrypoint 自動 init(容器啟動偵測空庫自動建)

服務容器 entrypoint 起 binary 前檢測 DB,空庫就跑 init。

  • ✅ 客戶體驗最滑:docker run 即全好。
  • 服務容器必須持有 cmmgr 級憑證——違反最小權限(runtime 只該有 cm_app);FR-063 產物是 Nuitka binary,entrypoint 塞 psql/初始化工具鏈會撐大 image;空庫誤判(DB 連錯台)時風險是「在錯的庫建整套 schema」。
  • 折衷變體:獨立 init job 容器(同 image family 另一個 one-shot container,或 installer 內嵌 psql client),只有 init 當下拿 superuser 憑證,跑完即棄。若未來要「升級也自動套 migration」,這個 job 容器形態可沿用。

5.4 取捨建議

B(installer 觸發的分段腳本)為主,C 的 init-job 容器作為執行載體:installer 負責產參數(兩組 DB 密碼、admin 初始密碼)→ 起 one-shot init 容器(或直接在主機跑 psql)執行 init.sh → 成功後才起服務容器(只給 cm_app 憑證)。A 不建議單獨採用。

D1 框架資產的特別註記:若裁決結果是「出廠帶 OSCAL JSON、首次啟動匯入」,則 05-seed-catalog.sql 整段換成應用層匯入指令(既有框架匯入 pipeline),DB init 只管空殼——這會顯著縮小 seed dump 的維護面,但拉長首次安裝時間且依賴應用已能啟動。兩路成本留給實作棒對照裁決結果細算。

5.5 參數注入接口(留給 FR-065 installer 的契約草案)

參數 用途 預設
INIT_DB_HOST / INIT_DB_PORT 目標 DB —(必填)
INIT_DB_NAME 資料庫名 guidant_ai(D10 待裁決)
INIT_CMMGR_PASSWORD / INIT_CMAPP_PASSWORD 兩組帳號密碼 installer 隨機產生
INIT_ADMIN_LOGIN / INIT_ADMIN_PASSWORD 初始 admin admin/隨機產生+首登改密(D8)
INIT_ROOT_TENANT_NAME root tenant 顯示名 D8 待裁決
INIT_STORAGE_* STORAGE_CONFIG 座標 安裝時填或裝後後台設

附錄:本次盤點使用的唯讀查核語句(實作棒驗收可複用)

-- 缺 GRANT 的表(應回 0 列)
SELECT n.nspname||'.'||c.relname FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
WHERE c.relkind IN ('r','p') AND n.nspname IN ('public','compliance','config','oscal','survey')
AND NOT (has_table_privilege('cm_app',c.oid,'SELECT') AND has_table_privilege('cm_app',c.oid,'INSERT')
     AND has_table_privilege('cm_app',c.oid,'UPDATE') AND has_table_privilege('cm_app',c.oid,'DELETE'));
-- 缺 USAGE 的 sequence(應回 0 列)
SELECT schemaname||'.'||sequencename FROM pg_sequences
WHERE NOT has_sequence_privilege('cm_app', quote_ident(schemaname)||'.'||quote_ident(sequencename),'USAGE');
-- RLS 啟用表清單/policy 數
SELECT n.nspname||'.'||c.relname FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE c.relrowsecurity;
SELECT count(*) FROM pg_policies;