C1 — OSCAL SSP 角色 Master Data 化

級別:中型 依賴:無 被依賴:C2(party endpoint dropdown)/ C8(清理 _ROLE_LABEL_MAP 後同步)


1. 目標

把目前散落於程式碼的 OSCAL SSP 角色(hardcoded enum / regex / label map)統一為 system_menus 內的 master data,FE dropdown 改 API 讀取,新增角色零 code 變更。

2. 範圍

  • In scope
    • DB seed 9 個 OSCAL 角色到 system_menus (group='ssp_party_role')(UI / API dropdown 用)
    • DB seed 同 9 個 OSCAL 角色到 oscal.oscal_roles(OSCAL 文件結構用,B 階段匯出 metadata.roles[] 需要)
    • FE i18n 檔新增 oscal-role 區段(zh-tw + en)
  • Out of scope
    • 既有 hardcoded role 的 caller 改造(屬 C8 範圍)

為什麼兩個表都要 seed

用途
system_menus (group='ssp_party_role') UI / API dropdown master data — 給 FE picker 用
oscal.oscal_roles OSCAL 文件結構 — B 階段匯出 SSP 為 JSON/XML/YAML 時,spec 規定 metadata.roles[] 區段必須列出該 SSP 用到的所有角色定義(id / title / description)

兩個服務不同對象(FE UI vs OSCAL 文件規格),同 9 個內容同步維護。新增第 10 個角色時兩邊都要加。

3. 資料設計

3.1 Master data(9 個角色)

sort key(OSCAL role_id 標準) value zh-tw label en label party_type 提示
10 responsible-organization responsible-organization 專案負責單位 Responsible Organization organization
20 system-owner system-owner 系統所有者 System Owner person
30 system-security-officer system-security-officer 系統安全官 (ISSO) System Security Officer person
40 authorizing-official authorizing-official 授權機關 (AO) Authorizing Official person
50 information-owner information-owner 資料所有者 Information Owner person
60 information-provider information-provider 資料提供者 Information Provider person
70 information-receiver information-receiver 資料接收者 / 窗口 Information Receiver person
80 prepared-by prepared-by 文件撰寫者 Prepared By person
90 prepared-for prepared-for 文件對象 Prepared For organization

約定

  • valuekey 相同(皆為英文 OSCAL role_id)— FE 從 key 查 i18n
  • sort 預留 10 為間隔,未來插隊不用大規模 renumber
  • enable=1public=1 全部公開啟用

3.2 SQL Migration

檔名:scripts/sql/2026-MM-DD-seed-oscal-ssp-roles.sql

-- Date: 2026-MM-DD
-- Purpose: C1 — seed OSCAL SSP 9 個角色到 system_menus + oscal.oscal_roles
--          system_menus:UI dropdown 用
--          oscal_roles:OSCAL 文件結構用(B 階段匯出 metadata.roles[])
-- 帳號:cmmgr(避 RLS)

BEGIN;

-- ─────────────────────────────────────────────────────────────────────────────
-- A. seed system_menus (2026-MM-DD) — UI / API dropdown master
-- ─────────────────────────────────────────────────────────────────────────────
-- 前置:確認 UNIQUE(group, key) constraint 已存在;若無,先加 (pre-flight 確認)
-- ALTER TABLE system_menus ADD CONSTRAINT uq_system_menus_group_key UNIQUE ("group", key);

INSERT INTO system_menus ("group", key, value, sort, enable, public) VALUES
('ssp_party_role', 'responsible-organization', 'responsible-organization', 10, 1, 1),
('ssp_party_role', 'system-owner',             'system-owner',             20, 1, 1),
('ssp_party_role', 'system-security-officer',  'system-security-officer',  30, 1, 1),
('ssp_party_role', 'authorizing-official',     'authorizing-official',     40, 1, 1),
('ssp_party_role', 'information-owner',        'information-owner',        50, 1, 1),
('ssp_party_role', 'information-provider',     'information-provider',     60, 1, 1),
('ssp_party_role', 'information-receiver',     'information-receiver',     70, 1, 1),
('ssp_party_role', 'prepared-by',              'prepared-by',              80, 1, 1),
('ssp_party_role', 'prepared-for',             'prepared-for',             90, 1, 1)
ON CONFLICT DO NOTHING;  -- idempotent

-- ─────────────────────────────────────────────────────────────────────────────
-- B. seed oscal.oscal_roles (2026-MM-DD) — OSCAL 文件結構(給 B 階段匯出用)
-- ─────────────────────────────────────────────────────────────────────────────
-- OSCAL spec: metadata.roles[] 必須含每個被引用 role 的 id + title + description
-- 內容必須跟 system_menus 同步(同 9 個 role_id),但描述可詳細些供 OSCAL 文件用

-- idempotent:role_id 既存就跳過
INSERT INTO oscal.oscal_roles (role_id, title, description) VALUES
('responsible-organization', 'Responsible Organization',
 'The organization responsible for the system''s operation at the organizational level.'),
('system-owner',             'System Owner',
 'The individual ultimately responsible for the system''s security and operation, including risk decisions.'),
('system-security-officer',  'System Security Officer',
 'The individual responsible for implementing system security controls and monitoring.'),
('authorizing-official',     'Authorizing Official',
 'The individual with authority to formally accept the system''s operational risks.'),
('information-owner',        'Information Owner',
 'The individual with authority over access, use, and disposition of specific information categories.'),
('information-provider',     'Information Provider',
 'The individual or organization providing information to the system.'),
('information-receiver',     'Information Receiver',
 'The individual or organization receiving output information from the system.'),
('prepared-by',              'Prepared By',
 'The individual responsible for preparing this SSP document.'),
('prepared-for',             'Prepared For',
 'The organization for which this SSP document is primarily prepared.')
ON CONFLICT (role_id) DO NOTHING;  -- idempotent;前提:oscal_roles 已有 UNIQUE(role_id)

-- 注意:若 oscal_roles 無 UNIQUE(role_id) constraint,pre-flight 先加:
--   ALTER TABLE oscal.oscal_roles ADD CONSTRAINT uq_oscal_roles_role_id UNIQUE (role_id);

COMMIT;

-- 驗證
-- SELECT * FROM system_menus WHERE "group" = 'ssp_party_role' ORDER BY sort;
-- → 應看到 9 筆
-- SELECT role_id, title FROM oscal.oscal_roles ORDER BY role_id;
-- → 應看到 9 筆

注意 (pre-flight)

  • system_menus 目前是否有 UNIQUE(group, key)?若無,先加再 seed
  • oscal.oscal_roles 目前是否有 UNIQUE(role_id)?若無,先加再 seed
  • 兩個 table seed 前先 audit 既有資料:是否有 role_id 衝突

4. API 設計

無新 endpoint,沿用 system_menu 模組既有:

  • GET /system/menu/ssp_party_role
    • Response: { "code": 1, "data": { "menus": [{ key, value, sort, enable, public }, ...] } }
    • 排序 by sort,過濾 enable=1
    • FE 直接餵 PrimeVue Dropdown

5. FE i18n 結構

檔案:src/config/locales/i18n/zh-tw/oscal-role.json(新增)

{
  "oscal_role": {
    "ssp_party_role": {
      "responsible-organization": {
        "label": "專案負責單位",
        "description": "對系統運作負組織級責任的單位"
      },
      "system-owner": {
        "label": "系統所有者",
        "description": "對系統的安全與營運負最終責任,包括風險決策"
      },
      "system-security-officer": {
        "label": "系統安全官 (ISSO)",
        "description": "負責執行系統安全控制與監控"
      },
      "authorizing-official": {
        "label": "授權機關 (AO)",
        "description": "有權正式接受系統運作風險的人員"
      },
      "information-owner": {
        "label": "資料所有者",
        "description": "對特定資料類別的存取、使用與處置擁有權限"
      },
      "information-provider": {
        "label": "資料提供者",
        "description": "提供資料給系統使用的人員或單位"
      },
      "information-receiver": {
        "label": "資料接收者 / 窗口",
        "description": "接收系統輸出資料的人員或單位"
      },
      "prepared-by": {
        "label": "文件撰寫者",
        "description": "負責撰寫此 SSP 文件的人員"
      },
      "prepared-for": {
        "label": "文件對象",
        "description": "此 SSP 文件主要服務的對象單位"
      }
    }
  }
}

同步建立 en 版本 src/config/locales/i18n/en/oscal-role.json(label/description 換英文)。

i18n key 對映規則:lang.oscal_role.ssp_party_role.<role_id>.label / .description

6. 邊界條件

情境 行為
seed migration 重跑 ON CONFLICT DO NOTHING 跳過已存在,不報錯
FE 拿到 system_menus 但 i18n key 找不到 fallback 顯示 key 字串(FE 已有此 fallback pattern)
某角色 enable=0 GET /system/menu/<group> 自動過濾,dropdown 看不到,但既有 SSP party 已寫入該角色不影響
新增第 10 個角色 INSERT 一行 + 補 i18n 兩個檔案,不用 deploy code

7. 開發者使用約定

任何需要 OSCAL SSP 角色 dropdown 的地方,都應:

  1. 後端 NOT hardcode 角色 enum,從 system_menus
  2. 前端 dropdown 的 options 從 GET /system/menu/ssp_party_role
  3. 顯示文字用 lang.oscal_role.ssp_party_role.<key>.label

禁止:在 service 層用 hardcoded role string list 做業務邏輯判斷(除了「party_type 推導」這種架構級邏輯)。

8. 決策(已 finalized)

編號 決策
C1-D1 ✅ 加 UNIQUE(group, key);pre-flight verify 既有無重複後再加
C1-D2 ✅ description 文案以 OSCAL spec / NIST SP 800-18 慣例為準(已寫入 §3.2 SQL)
C3-D5 oscal.oscal_roles 同步 seed 9 個(UI / OSCAL 文件雙用途)

9. 開發後狀態

  • system_menus 表內多 9 筆 ssp_party_role 資料(UI dropdown 用)
  • oscal.oscal_roles 表內多 9 筆對應 role row(OSCAL 文件用)
  • FE 多 2 個 i18n 檔(zh-tw / en oscal-role.json
  • 既有任何 hardcoded role enum 暫不動(屬 C2 / C8 範圍)