# SSP 在專案內直接編輯 — 設計分析

> 日期：2026-05-22
> 觸發來源：SSP Excel 匯入匯出 Phase 2（A+B）測試後發現的範圍歸屬問題
> 性質：Phase 2 的延伸補強（Phase C），在同一個 branch `feature/ssp-import-export-phase2` 繼續推進

---

## 背景

SSP Excel 匯入匯出 Phase 2（Track A 匯入 + Track B 匯出）全部 ship 後，user 測試對亞航 CMMC SSP 樣板資料時發現：

1. **角色語意混亂**：SSP 文件需要「系統 owner / AO / 安全官」(OSCAL 文件角色)，專案則用「manager / reviewer / auditor / viewer」(工作流角色)，現有 UI 把兩者塞進同一個 picker 看起來像同一回事
2. **受評範圍歸屬錯位**：`compliance.project_device_mapping` / `compliance.project_information_systems` 把範圍綁在 project 層，但實務上範圍會隨 AP 週期變動（年度稽核增減設備）— 語意應該是 per-SSP
3. **亞航文件大量資料系統內無對應**：Table 1（Leveraged FedRAMP）/ Table 2（External Systems）/ external_systems 等 SSP-only metadata 無欄位可存
4. **專案內無法直接編 SSP**：MF 編輯頁可改 SSP 樣板，但專案啟動後想對該專案的 SSP 做客製化修改，目前要回 MF 改（會污染樣板）

---

## 決策匯總（已拍板）

| 議題 | 決策 |
|------|------|
| **編 SSP 的範圍** | C 路線：編「**當前 AP 期間**對應的 SSP」（per-AP SSP） |
| **誰能編** | 只有 **`manager`** 角色（project_participants.role='manager'） |
| **編 SSP 後是否 sync 回 MF** | **否**，SSP 是 snapshot，跟 MF 漂移允許 |
| **SSP 角色 vs 專案角色** | **分開**：SSP 用 OSCAL 標準角色（9 個），專案維持 ParticipantRole 四角色 |
| **OSCAL 角色 master data 存哪** | 複用 `system_menus`，group=`ssp_party_role`，**value 存英文 key**，FE 用 key 查 i18n 顯示中/英文 |
| **受評範圍歸屬遷移路線** | A 路線：**全面遷移到 SSP scope**，廢除 `project_device_mapping` / `project_information_systems` |
| **第 N+1 個 AP 啟動時 SSP 從哪 copy** | **從 v_n SSP deep clone**（已實作於 `SspVersioningService.clone_to_new_version`，包含 9 個 OSCAL 擴充欄位） |
| **「專案規劃」頁加 SSP 編輯入口** | **方案 B**：新增 SSP tab（不另開 fullpage 編輯頁），子區塊 lazy load 避免單次大量 fetch |

---

## 1. 五個資料域的「Tenant ↔ MF ↔ SSP ↔ Project」對應

### 1.1 參與人員

| 層 | Table | 角色欄位 | 角色取值 |
|----|-------|---------|---------|
| Tenant | `public.users` (jedi-auth) | — | 純使用者識別 |
| MF（樣板）| `oscal_parties` (party_type='person', user_id soft ref) + `oscal_responsible_parties` (context_type='ssp', context_id=NULL or MF 內部 SSP.id) | `role_id` | OSCAL 角色 9 個 |
| SSP（版本快照） | 同上，`context_type='system_security_plan'`, `context_id=ssp.id` | `role_id` | OSCAL 角色 9 個（從 MF copy） |
| Project（流程） | `compliance.project_participants` | `role` (varchar) | `manager / reviewer / auditor / viewer` |

### 1.2 設備

| 層 | Table | 說明 |
|----|-------|------|
| Tenant | `public.devices` (jedi-device) | 設備本體 |
| MF | ❌ 無 | MF 是控制項樣板，不綁設備 |
| SSP | `oscal.ssp_system_implementation_items` (implementation_type='hardware', `device_id` soft ref) | OSCAL inventory-item 鏡像 |
| Project | `compliance.project_device_mapping` ⚠️ **要廢除** | 改 derived from SSP |

### 1.3 資訊系統

| 層 | Table | 說明 |
|----|-------|------|
| Tenant | `compliance.information_systems` | 系統本體 |
| MF | ❌ 無直接綁定 | — |
| SSP | A. `oscal.system_security_plans_system_characteristics` (1:1) <br> B. `oscal.ssp_system_implementation_items` (implementation_type IN ('component','system','subsystem','service','software'), `system_characteristic_id` soft ref) | OSCAL 雙層：被描述系統 + 子元件 |
| Project | `compliance.project_information_systems` ⚠️ **要廢除** | 改 derived from SSP |

### 1.4 06_外部利用服務（目前隱藏中）

| 層 | Table | 說明 |
|----|-------|------|
| Tenant | ❌ 無對應 entity | 系統內無 「FedRAMP authorized service catalog」 |
| MF | ❌ 無 | — |
| SSP | `oscal.ssp_system_implementation_items` (implementation_type='leveraged-authorization', `party_uuid` → `oscal_parties` 作為 provider) | OSCAL leveraged-authorization 鏡像 |
| Project | ❌ 無 | — |

**現況**：v2.0.0（2026-05-21）06 sheet **已從樣板 ALL_SHEETS 隱藏**，parser 也跳過讀（`parser.py:70` `leveraged=[]`）。但 DB / write strategy / DI / handler 全 stack 還在。要恢復只需兩步：
1. `sheet_definitions.py:259 ALL_SHEETS` 加回 `SHEET_LEVERAGED`
2. `parser.py:70` 改回呼叫 `sh.parse_leveraged_sheet(...)`

### 1.5 亞航 Table 2（External Systems）

**目前完全沒對應 entity**，系統內無存。OSCAL spec 對應 `system-implementation.components` with type='interconnection' or 'service'，即 `ssp_system_implementation_items.implementation_type='component'`（純文字 + `system_characteristic_id=NULL`，因為外部系統不會在 tenant 內有對應 information_system 記錄）。

亞航 Table 2 缺欄位：類別 / Protocol / Security/Auth — 全是 SSP-only metadata。

---

## 2. SSP 在專案內直接編輯的可行性

### 2.1 資料層已 scope-aware（核心優勢）

每張 SSP 相關表都已內建 scope 隔離：

| Table | Scope 欄位 |
|-------|-----------|
| `oscal.ssp_system_implementation_items` | `scope_type` + `scope_id` (`'ssp'` / `'module_frame'`) |
| `oscal.ssp_system_implementations` (main) | 同上 |
| `oscal.oscal_responsible_parties` | `context_type` + `context_id` (`'ssp'` + ssp.id) |
| `oscal.system_security_plans_system_characteristics` | `system_security_plan_id` FK |
| `oscal.ssp_reference_documents` | `context_type='ssp'` |

→ project clone SSP 後，這些 row 已經是 project 專屬的，**改它們不影響 MF 也不影響其他 project**。

### 2.2 MF 編輯路徑其實也是寫 SSP scope

`module_frame_ssp_resources_service.py:204` 直接寫：
```python
scope_type="ssp"
scope_id=ssp_id  # MF 的內部 SSP
```

等於說 MF 的編輯邏輯，整套搬到 project SSP **只要換 entry 參數從 MF uid → SSP uid 就行**。Service 內部 90% 邏輯可以共用。

### 2.3 部分 SSP-scoped endpoints 已存在

```
✅ /ssp/<ssp_uid>/control-implementations      (CRUD 控制項實作)
✅ /ssp/<ssp_uid>/.../objective/...            (CRUD AO 現況)
✅ /ssp/<ssp_uid>/reference-documents          (程序書)
✅ /ssp/<ssp_uid>/document-pool                (程序書池)
✅ /ssp/<ssp_uid>/control-implementations/import (docx import per SSP)
✅ /ssp/<ssp_uid>/export                       (預留給 B 階段)
```

### 2.4 缺什麼

| 缺的 endpoint | 仿哪個 MF 版本 | 工作量 |
|--------------|--------------|--------|
| `GET/POST/PUT/DELETE /ssp/<ssp_uid>/parties` | `module_frame_party_service` | 小 — service 80% 可複用 |
| `GET/POST /ssp/<ssp_uid>/ssp-resources` (devices/info_systems) | `module_frame_ssp_resources_service` | 極小 — 內部已是 SSP scope，換 entry 即可 |
| `PUT /ssp/<ssp_uid>/ssp-resources/items/<item_uid>` | 同上 | 同上 |
| `PUT /ssp/<ssp_uid>/system-characteristic` | 新建 | 中（修 sensitivity / authorization-boundary / system-owner 等）|
| `GET/POST /ssp/<ssp_uid>/leveraged` | 借 ssp_resources | 小 |
| `POST /ssp/<ssp_uid>/excel-import` | `ssp_excel_import_app_service` | 中（scope 切換）|

預估純後端 endpoint 補齊：2~3 天工。

---

## 3. C4 深度影響分析：廢除 project_device_mapping / project_information_systems

### 3.1 完整 caller 盤點

**A. 核心 CRUD（必須改）**

| Caller | 用途 | 改造方向 |
|--------|------|---------|
| `app/grc/service/project_service.py:GrcProjectService` | 讀寫專案範圍（建立/更新時）<br> - Line 258-281：讀 `audit_systems` / `devices` 組 DTO <br> - Line 367-...：audit_systems 替換 <br> - Line 432-444：devices 替換 | 改吃 current SSP 的 `system_implementation_items` |
| `app/project/service/oscal_project_service.py` | <br> - Line 456-461 Step C：建專案時 INSERT devices <br> - Line 1100：刪專案 cascade delete | 拿掉 Step C（範圍移交 SSP 編輯頁）+ 拿掉 cascade（搬到 SSP CASCADE 上） |
| `app/associations/service/project_device_mapping_service.py` | 設備 mapping CRUD | 整個服務廢除 |

**B. API endpoints（必須處理）**

| Endpoint | 用途 | 改造方向 |
|----------|------|---------|
| `/project-devices` (`ProjectDevicesRoute`) | 查專案設備清單 | 改成從 current SSP 抓 + 返回相同格式（前端零改） |
| `/project-device` (`ProjectDeviceRoute`) | 單筆設備 CRUD | 同上 derive，或廢除（改用 SSP 編輯頁） |
| `/project/job-execution-devices` (`JobExecutionDevicesRoute`) | 工作流任務查設備 | 同上 derive |
| `/project/job-execution-device` | 工作流設備 | 同上 |

**C. Request / Response Schema 改造**

| Schema | 改動 |
|--------|------|
| `OscalProjectStartRequest` (api/project/serializers/project.py:30) | 移除 `audit_systems` + `devices` 欄位 |
| `ProjectUpdateRequestSchema` (api/grc/serializers/project.py:77) | 移除 `audit_systems` + `devices` 欄位 |
| `ProjectResponseSchema.audit_systems` / `.devices` | 改 derived from current SSP（API 對外格式不變） |

**D. SSP write strategy（不影響）**

- `domain/oscal/strategy/ssp_write_strategy.py:258-277` 有 `TODO Phase E.2`，從未實作 → 連帶拆掉 TODO

**E. Dashboard / AI Dashboard（不影響）**

- 沒找到直接使用，安全

**F. SSP versioning（不影響，反而受益）**

- `ssp_versioning_service.clone_to_new_version` 已包含 `system_implementation_items` 全 9 欄位
- 移除 project mapping 表後跨 AP 連續性更乾淨（單一資料源）

### 3.2 風險評估

| 風險 | 嚴重度 | 緩解 |
|------|-------|------|
| `/project-devices` API 改成 derived 時 schema 不一致 | 🟡 中 | API 回傳 schema **保持不變**，前端零改 |
| 既有專案 migration | 🟡 中 | 寫一次性 SQL：每個專案的 current SSP 內 INSERT 對應 items；對舊資料做 idempotent 補洞 |
| `oscal_project_service.Step C` 改造影響建專案流程 | 🟢 低 | 直接拿掉這段；建專案時不寫設備，等 user 進 SSP 編輯頁加 |
| Domain / Infra 整套程式碼留下會混淆 | 🟢 低 | C 階段完工後一次性 `git rm` 整個 `domain/associations/` / `infra/associations/` / `app/associations/` stack |
| 多 tenant RLS policy 是否依賴這兩張表 | 🟡 中 | C 開工前 `grep -r "project_device_mapping\|project_information_systems" scripts/sql/ infra/grc/` 確認 |
| B 階段（匯出）會用到？ | 🟡 中 | B 已 ship，需確認匯出邏輯是否 join 這兩張表，如有要連帶改 |

---

## 4. OSCAL SSP 角色 master data 設計

### 4.1 採用 system_menus 而非新建 oscal_roles 表

**理由**：
- `system_menus` 已是 generic enum lookup table（group/key/value/sort/enable/public）
- 既有 API `/system/menu/<group>` 可直接餵 FE dropdown，零自製
- 新增角色 = INSERT 一行 SQL，不用 deploy code
- 同樣模式可套到其他 OSCAL enum（system-implementation-type / system-status / sensitivity-level）

### 4.2 9 個角色 master list（seed data）

```sql
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);
```

> `value` 存英文 key（跟 `key` 同），FE 拿到後用此 key 查 i18n 顯示中/英文 label。這比 value 存中文更乾淨（不混 i18n 邏輯到 DB）。

### 4.3 FE i18n 檔案結構

```json
// src/config/locales/i18n/zh-tw/oscal-role.json
{
  "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": "..." },
    "prepared-for": { "label": "文件對象", "description": "..." }
  }
}
```

### 4.4 需要連帶改的程式碼

| 修改點 | 動作 |
|--------|------|
| `sheet_definitions.py:138` Excel 03_參與人員 role | 從 ParticipantRole 四選一 → 改 OSCAL 角色 dropdown（讀 system_menus） |
| `ssp_docx_generator.py:32 _ROLE_LABEL_MAP` | 拿掉 manager/auditor/viewer（不該出現於 SSP 文件），補齊 OSCAL 角色，修 `security-officer` ↔ `system-security-officer` 對不上 |
| `cmmc_ssp_adapter.py:49` regex 對應 | 擴增到 9 個角色對應的中文 pattern |
| FE 預覽頁 / template-edit 頁 role picker | 從 hardcoded 4 個 → 改讀 `/system/menu/ssp_party_role` API |
| `OscalRole` ORM model | 評估留還是廢（目前可能未 seed 也無 caller） |

---

## 5. 「專案規劃」頁加 SSP 編輯入口（UI 設計）

### 5.1 採用方案 B：tab in 專案規劃頁

```
專案規劃頁
├── Tab: 概覽 (default)
├── Tab: AP 列表 / 進度
├── Tab: 參與人員
└── Tab: SSP   ← 新加
    └── 下拉選 AP → 顯示該 AP 對應 SSP 的編輯區
        ├── 基本資料（system_characteristic）
        ├── 受評範圍（devices / info_systems / leveraged）
        ├── OSCAL Parties
        ├── 控制項實作（既有 /ssp/<uid>/control-implementations）
        └── 程序書
```

### 5.2 Lazy load 策略（解決 user 擔心的「單頁資料太多」）

- Tab 切換到「SSP」時才呼叫後端
- AP 切換時才載入該 AP 對應 SSP 的詳細資料
- 每個子區塊（範圍 / parties / 控制項）獨立 endpoint，分別 fetch
- 子區塊內可再用 collapse / accordion 預設折疊避免一次渲染全部

### 5.3 權限門檻

- 只有 `manager` 角色看得到 SSP tab 內的編輯按鈕（reviewer/auditor/viewer 可進但 read-only）
- 後端每個 SSP-scoped write endpoint 都要驗 `project_participants.role='manager'`

---

## 6. 專案建立/編輯流程改造

### 6.1 建立專案 (POST /oscal-project/start)

**現況**：
- `OscalProjectStartRequest` 接受 `participants` + `audit_systems` + `devices` 三 list
- `oscal_project_service.start_oscal_project` Step C：寫 device mappings；Step audit_systems：寫 information_system mappings

**改造**：
- `OscalProjectStartRequest`：**移除** `audit_systems` + `devices` 欄位
- `start_oscal_project`：**拿掉 Step C** 跟對應 audit_systems INSERT
- 新流程：建專案 → 立即啟動第一個 AP → auto-create draft SSP → user 進 SSP tab 編範圍

### 6.2 編輯專案 (PUT /grc/project/<uid>)

**現況**：
- `ProjectUpdateRequestSchema` 接受 `audit_systems` + `devices`
- `update_project` Section 3 + Section 5 做替換邏輯

**改造**：
- `ProjectUpdateRequestSchema`：**移除** `audit_systems` + `devices` 欄位
- `update_project`：**移除** Section 3 + Section 5 整段
- 保留 name / description / start_date / end_date / status / owner_uid / participants

### 6.3 專案 Response (GET /grc/project/<uid>)

**現況**：`ProjectResponseSchema.audit_systems` + `.devices` 從 project_device_mapping / project_information_systems 抓

**改造（API schema 不變，內部換資料源）**：
- `dto.audit_systems`：改 derive from current SSP 的 `ssp_system_implementation_items` (scope_type='ssp', current_ssp_id, implementation_type IN ('component','system','subsystem','service','software'))
- `dto.devices`：改 derive from `ssp_system_implementation_items` (implementation_type='hardware')

→ 前端零改（schema 不動）

### 6.4 前端表單調整

- **建立專案表單**：拿掉「受評範圍」section（devices / audit_systems pickers）
- **編輯專案表單**：同上
- **專案詳情頁 / 規劃頁**：「受評範圍」section 從專案層改為 link 到 SSP tab

---

## 7. 已棄用 / 待清理項目

| 項目 | 處理方式 |
|------|---------|
| `project_device_mapping` table | C4 完工後 DROP TABLE（含 migration 資料遷移） |
| `project_information_systems` table | 同上 |
| `domain/associations/` / `infra/associations/` / `app/associations/` 整個 stack | C4 完工後 `git rm` |
| `ssp_write_strategy.py:258-277` TODO Phase E.2 | 連帶移除 |
| `_ROLE_LABEL_MAP` 內 manager/auditor/viewer | 拿掉（不該出現於 SSP 文件） |
| `OscalRole` ORM model | 評估，可能廢 |

---

## 8. Phase C 工作清單（細項見 README.md）

| ID | 子題 | 級別 |
|----|------|------|
| C1 | OSCAL SSP 角色 master data 化 | 中 |
| C2 | SSP-scoped endpoints 補齊（6 個） | 中 |
| C3 | 受評範圍歸屬遷移到 SSP（廢 project_device_mapping / project_information_systems）| **重** |
| C4 | 專案建立/編輯流程改造（拿掉 audit_systems / devices）| 中 |
| C5 | 專案規劃頁加 SSP tab | 中 |
| C6 | 移除專案設定 UI 的「受評範圍」 | 小 |
| C7 | 06_外部利用服務恢復（可選）| 小 |
| C8 | `_ROLE_LABEL_MAP` 清理 + role id 修正 | 小 |

依賴：C1 + C2 → C3 → C4 → C5/C6（前端依賴後端 endpoint 完整）
