# C2 — SSP-scoped Endpoints 補齊

> **級別**：中
> **依賴**：C1（角色 master data，供 party endpoint dropdown）
> **被依賴**：C3（受評範圍 derived，依賴 SSP items 可被讀）、C5（FE SSP tab 全靠這些 endpoints）

---

## 1. 目標

補齊「在專案內直接編輯 SSP」所需的 SSP-scoped CRUD endpoints。仿照既有 `module_frame_*` endpoints 改造，**Service 層 80% 可複用**（既有 MF endpoint 內部已寫 SSP scope）。

## 2. 範圍

### In scope

| Endpoint group | Method 數 | 仿哪個 MF 版本 |
|---------------|----------|--------------|
| `/ssp/<ssp_uid>/parties` | 4 (GET list / POST / PUT / DELETE) | `module_frame_party_service` |
| `/ssp/<ssp_uid>/ssp-resources` (devices + info_systems) | 4 (GET list / POST item / PUT item / DELETE item) | `module_frame_ssp_resources_service` |
| `/ssp/<ssp_uid>/leveraged` | 4 (GET list / POST / PUT / DELETE) | 借 ssp_resources pattern + leveraged type |
| `/ssp/<ssp_uid>/system-characteristic` | 2 (GET / PUT) | 新建 |
| `/ssp/<ssp_uid>/excel-import` | 系列 (preview / confirm / discard) | `ssp_excel_import_app_service` |
| `/ssp/<ssp_uid>/current-for-project` | 1 (GET) | 新建 helper |

### Out of scope

- `/ssp/<uid>/control-implementations`（既有，不動）
- `/ssp/<uid>/reference-documents`（既有，不動）
- `/ssp/<uid>/export`（B 階段）
- FE UI（C5）

## 3. 共用權限 / 前置邏輯

### 3.1 SSP uid → project 推導

每個 SSP-scoped endpoint 都需要：
1. 從 ssp_uid 查回 SSP entity
2. 透過 AP（OscalAssessmentPlan.ssp_id = ssp.id）→ ProjectAssessmentPlanMapping → Project
3. 驗 current user 的 `project_participants.role == 'manager'`（寫操作）
4. 寫操作前也驗該 AP 是否還在 active 狀態（rejected：closed / archived AP 不能改 SSP）

**抽 helper**：
```python
# domain/oscal/service/ssp_project_resolver.py (新)
class SspProjectResolver:
    def resolve(self, ssp_uid: str) -> SspProjectContext:
        """從 ssp_uid 反查 project + AP + 驗 active 狀態。
        Returns:
            SspProjectContext(ssp, ap, project)
        Raises:
            NotFound: ssp / ap / project 不存在
            PreconditionFailedError: AP 已 closed / archived
        """
```

```python
# common/middleware/permission/ssp_permission.py (新)
def require_ssp_manager(ssp_uid: str):
    """Decorator / service helper：驗當前 user 在 SSP 對應 project 是 manager。"""
    ctx = ssp_project_resolver.resolve(ssp_uid)
    user_id = get_user_context().user_id
    participant = project_participant_domain.get_one(
        ProjectParticipantQueryEntity(project_id=ctx.project.id, user_id=user_id)
    )
    if participant is None or participant.role != ParticipantRole.MANAGER:
        raise ForbiddenError(GrcErrorCode.GRC_NOT_MANAGER)
    return ctx
```

讀操作放寬：reviewer / auditor / viewer 也能讀（read-only 模式）。

### 3.2 Error codes (新增 `common/code/grc_error_code.py`)

| Code | HTTP | 訊息 |
|------|------|------|
| `GRC_SSP_NOT_FOUND` | 404 | SSP 不存在 |
| `GRC_AP_NOT_ACTIVE` | 412 | AP 已關閉，無法編輯 SSP |
| `GRC_NOT_PROJECT_MANAGER` | 403 | 僅專案 manager 可編輯 SSP |
| `GRC_SSP_PARTY_NOT_FOUND` | 404 | SSP 內參與人員不存在 |
| `GRC_SSP_RESOURCE_NOT_FOUND` | 404 | SSP 資源不存在 |
| `GRC_INVALID_OSCAL_ROLE` | 400 | 不合法的 OSCAL role（不在 system_menus 內）|

序號依現行 `GrcErrorCode` 接續編號（pre-flight 確認下一個可用序號）。

## 4. Endpoint 設計

### 4.1 `/ssp/<ssp_uid>/parties` （party CRUD）

#### GET — 列出 SSP 內所有 parties

```
GET /ssp/<ssp_uid>/parties

Response:
{
  "code": 1,
  "data": {
    "parties": [
      {
        "uid": "<party_uid>",
        "party_type": "person",
        "name": "陳大文",
        "title": "資安組長",
        "email_address": "...",
        "telephone_number": "...",
        "user_uid": "...",       // soft ref to public.users.uid (nullable)
        "user_login_name": "...",
        "user_nickname": "...",
        "role_ids": ["system-owner", "system-security-officer"]  // 1 個 party 可有多 role
      },
      ...
    ]
  }
}
```

權限：**任意 project_participant** 可讀。

#### POST — 新增 party

```
POST /ssp/<ssp_uid>/parties

Body:
{
  "party_type": "person",
  "name": "陳大文",
  "title": "資安組長",
  "email_address": "...",
  "telephone_number": "...",
  "user_uid": "..." (optional, 鉤稽 tenant user),
  "org_unit_id": ... (optional, 對 organization party),
  "role_ids": ["system-owner"]
}

Response: 201, party object 同 GET 格式
```

權限：manager。
Validation：`role_ids` 內每個值都要 in `system_menus where group='ssp_party_role'` (C1)，否則 `GRC_INVALID_OSCAL_ROLE`。

#### PUT — 更新 party

```
PUT /ssp/<ssp_uid>/parties/<party_uid>

Body: 同 POST，全欄位可改（含 role_ids 替換）
Response: 200
```

#### DELETE — 刪除 party

```
DELETE /ssp/<ssp_uid>/parties/<party_uid>

Response: 204
```

注意：刪除前要確認沒有 `responsible_party` link 在引用此 party 於其他 context（control / task）— 若有，cascade delete 還是 reject？**[C2-D1 待拍板]**

### 4.2 `/ssp/<ssp_uid>/ssp-resources` （devices + info_systems）

#### GET — 列出 SSP 內 resources（devices + info_systems）

```
GET /ssp/<ssp_uid>/ssp-resources

Response:
{
  "code": 1,
  "data": {
    "devices": [
      {
        "item_uid": "...",
        "name": "Web Server 01",
        "description": "...",
        "device_id": 12,           // matched device FK (nullable)
        "matched_device_uid": "...",
        "matched_device_name": "..."
      }, ...
    ],
    "information_systems": [
      {
        "item_uid": "...",
        "name": "HR System",
        "description": "...",
        "system_characteristic_id": 5,
        "matched_info_system_uid": "...",
        "matched_info_system_name": "..."
      }, ...
    ]
  }
}
```

權限：任意 project_participant 可讀。

#### POST — 新增 item

```
POST /ssp/<ssp_uid>/ssp-resources/items

Body:
{
  "implementation_type": "hardware" | "system" | "component" | ...,
  "name": "...",
  "description": "...",
  "device_id": 12 (optional, hardware 才填),
  "system_characteristic_id": 5 (optional, component/system 才填),
  "title": "..."
}

Response: 201, item object
```

權限：manager。

仿 `module_frame_ssp_resources_service.add_item()` 直接複用（內部已是 SSP scope），只需換 entry uid（從 mf_uid → ssp_uid）。

#### PUT — 更新 item

```
PUT /ssp/<ssp_uid>/ssp-resources/items/<item_uid>

Body: 同 POST
Response: 200
```

#### DELETE — 刪除 item

```
DELETE /ssp/<ssp_uid>/ssp-resources/items/<item_uid>

Response: 204
```

### 4.3 `/ssp/<ssp_uid>/leveraged` （leveraged services）

設計上跟 ssp-resources 對等，只是 implementation_type 鎖在 `'leveraged-authorization'`：

```
GET    /ssp/<ssp_uid>/leveraged
POST   /ssp/<ssp_uid>/leveraged
PUT    /ssp/<ssp_uid>/leveraged/<item_uid>
DELETE /ssp/<ssp_uid>/leveraged/<item_uid>
```

欄位（精簡，沿用 v2.0 設計）：
```json
{
  "item_uid": "...",
  "service_name": "AWS S3",
  "provider": "Amazon Web Services",
  "purpose": "Object storage for log archive"
}
```

權限：manager（寫）/ 任意（讀）。

### 4.4 `/ssp/<ssp_uid>/system-characteristic`

#### GET

```
GET /ssp/<ssp_uid>/system-characteristic

Response:
{
  "code": 1,
  "data": {
    "uid": "...",
    "name": "亞航 GRC 平台",
    "description": "...",
    "system_identifier": "AIDC-DPCE",
    "security_sensitivity_level": "moderate",
    "target_type": "it_system",
    "scope_description": "...",
    "status": "active",
    "owner_uid": "<user_uid>"  // soft ref
  }
}
```

權限：任意 project_participant。

#### PUT

```
PUT /ssp/<ssp_uid>/system-characteristic

Body: 同 GET response data（全欄位可改）
Response: 200
```

權限：manager。

### 4.5 `/ssp/<ssp_uid>/excel-import` （SSP-scoped Excel 匯入）

仿既有 MF-scoped Excel import endpoints（A2/A5 phase），entry uid 改 ssp_uid：

```
POST   /ssp/<ssp_uid>/excel-import/upload     (上傳 + parse)
GET    /ssp/<ssp_uid>/excel-import/<parse_uid>/preview
PUT    /ssp/<ssp_uid>/excel-import/<parse_uid>/decisions  (row decisions)
POST   /ssp/<ssp_uid>/excel-import/<parse_uid>/confirm
DELETE /ssp/<ssp_uid>/excel-import/<parse_uid>            (discard)
```

權限：manager。

Service 層：`ssp_excel_import_app_service` 已存在（A5），內部 `_confirm_update_flow` 已是 SSP scope。**改造**：
- 入口 source_type 多支援 `'ssp'`（既有 `'framework_version'` / `'module_frame'`）
- `_confirm_update_flow` 拆 `_confirm_ssp_update_flow`（直接寫對應 SSP scope）

### 4.6 `/ssp/<ssp_uid>/current-for-project` （helper endpoint）

讓 FE 從 `project_uid` + 「我想編 current AP 的 SSP」推導出 ssp_uid：

```
GET /projects/<project_uid>/current-ssp-uid

Response:
{
  "code": 1,
  "data": {
    "ssp_uid": "...",
    "ap_uid": "...",
    "ap_status": "active" | "closed",
    "is_editable": true / false   // false if AP closed
  }
}
```

FE 拿到 ssp_uid 後就能餵給上面所有 endpoint。

權限：任意 project_participant 可讀。

## 5. URL pattern 一覽

```
GET    /ssp/<ssp_uid>/parties
POST   /ssp/<ssp_uid>/parties
PUT    /ssp/<ssp_uid>/parties/<party_uid>
DELETE /ssp/<ssp_uid>/parties/<party_uid>

GET    /ssp/<ssp_uid>/ssp-resources
POST   /ssp/<ssp_uid>/ssp-resources/items
PUT    /ssp/<ssp_uid>/ssp-resources/items/<item_uid>
DELETE /ssp/<ssp_uid>/ssp-resources/items/<item_uid>

GET    /ssp/<ssp_uid>/leveraged
POST   /ssp/<ssp_uid>/leveraged
PUT    /ssp/<ssp_uid>/leveraged/<item_uid>
DELETE /ssp/<ssp_uid>/leveraged/<item_uid>

GET    /ssp/<ssp_uid>/system-characteristic
PUT    /ssp/<ssp_uid>/system-characteristic

POST   /ssp/<ssp_uid>/excel-import/upload
GET    /ssp/<ssp_uid>/excel-import/<parse_uid>/preview
PUT    /ssp/<ssp_uid>/excel-import/<parse_uid>/decisions
POST   /ssp/<ssp_uid>/excel-import/<parse_uid>/confirm
DELETE /ssp/<ssp_uid>/excel-import/<parse_uid>

GET    /projects/<project_uid>/current-ssp-uid
```

合計 **17 個 endpoint**（其中 12 個直接複用 MF service）

## 6. Service 層複用策略

| MF 既有 service | SSP 版本作法 |
|----------------|-------------|
| `module_frame_party_service.list_parties(mf_uid)` | 抽出共用 `_list_parties_by_context(context_type, context_id)`，wrapper 呼叫 |
| `module_frame_party_service.create_party(mf_uid, payload)` | 同上抽共用 |
| `module_frame_ssp_resources_service.list_items(mf_uid)` | 同上 — 內部已是 SSP scope，只是 entry 是 mf_uid |
| `module_frame_ssp_resources_service.add_item(mf_uid, payload)` | 抽 `add_item_to_ssp(ssp_uid, payload)`，MF wrapper 呼叫 |

**重構步驟**：
1. 識別 MF service 內已是「SSP scope」的方法
2. Refactor — 把 SSP scope 邏輯抽到 base method（接 ssp_uid）
3. MF wrapper 保留為「ssp_uid 從 mf_uid 推導」的薄殼
4. C2 新建 SSP route 直接呼叫 base method

## 7. 邊界條件

| 情境 | 行為 |
|------|------|
| SSP 對應的 AP 已 closed | 寫操作 reject `GRC_AP_NOT_ACTIVE` 412；讀操作正常 |
| 使用者不是 manager | 寫操作 reject `GRC_NOT_PROJECT_MANAGER` 403；讀操作正常 |
| Party 被其他 context 引用（control / task） | 預設 reject delete；或加 `?cascade=true` query param 強制 cascade |
| Excel 匯入流程：confirm 時 SSP 已被別人改 | 仿 A5 既有 race condition 處理（先讀後寫的 stale）|
| OSCAL role 不在 system_menus | reject `GRC_INVALID_OSCAL_ROLE` 400 |

## 8. 待 user 拍板的小決策

| 編號 | 問題 | 我建議 |
|------|------|--------|
| C2-D1 | Party DELETE 時若被其他 context 引用，cascade or reject？ | **預設 reject**（避免不可預期的連動刪除），加 `?cascade=true` query 強制 |
| C2-D2 | 「AP closed → SSP 不可編」是否完全 lock，還是 reviewer 可加 comment？ | **完全 lock 寫操作**（comment 屬另一獨立 feature） |
| C2-D3 | C2 一次性 ship 全部 17 個 endpoint，還是分批？ | **建議分兩批**：B1 party + ssp-resources + system-characteristic + current-ssp-uid（11 個）→ B2 excel-import + leveraged（6 個） |
| C2-D4 | `current-ssp-uid` 路徑放在 `/projects/<uid>/...` 還是 `/ssp/...`？ | **`/projects/<uid>/current-ssp-uid`**（語意上是「這個 project 的 current SSP」） |

---

## 9. 開發後狀態

- 17 個 SSP-scoped endpoint 可用
- Service 層 MF 邏輯抽共用 base，SSP route 直接呼叫
- 共用 helper：`SspProjectResolver` + `require_ssp_manager`
- 新 error codes 6 個進 GrcErrorCode
- 既有 MF-scoped endpoints 維持運作（無破壞）
- FE 可開始開發 SSP tab（C5）
