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

# 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
        """
# 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 設計):

{
  "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)