級別:中 依賴:C1(角色 master data,供 party endpoint dropdown) 被依賴:C3(受評範圍 derived,依賴 SSP items 可被讀)、C5(FE SSP tab 全靠這些 endpoints)
補齊「在專案內直接編輯 SSP」所需的 SSP-scoped CRUD endpoints。仿照既有 module_frame_* endpoints 改造,Service 層 80% 可複用(既有 MF endpoint 內部已寫 SSP 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 |
/ssp/<uid>/control-implementations(既有,不動)/ssp/<uid>/reference-documents(既有,不動)/ssp/<uid>/export(B 階段)每個 SSP-scoped endpoint 都需要:
project_participants.role == 'manager'(寫操作)抽 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 模式)。
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 確認下一個可用序號)。
/ssp/<ssp_uid>/parties (party CRUD)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 /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 /ssp/<ssp_uid>/parties/<party_uid>
Body: 同 POST,全欄位可改(含 role_ids 替換)
Response: 200
DELETE /ssp/<ssp_uid>/parties/<party_uid>
Response: 204
注意:刪除前要確認沒有 responsible_party link 在引用此 party 於其他 context(control / task)— 若有,cascade delete 還是 reject?[C2-D1 待拍板]
/ssp/<ssp_uid>/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 /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 /ssp/<ssp_uid>/ssp-resources/items/<item_uid>
Body: 同 POST
Response: 200
DELETE /ssp/<ssp_uid>/ssp-resources/items/<item_uid>
Response: 204
/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(寫)/ 任意(讀)。
/ssp/<ssp_uid>/system-characteristicGET /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 /ssp/<ssp_uid>/system-characteristic
Body: 同 GET response data(全欄位可改)
Response: 200
權限:manager。
/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。改造:
'ssp'(既有 'framework_version' / 'module_frame')_confirm_update_flow 拆 _confirm_ssp_update_flow(直接寫對應 SSP scope)/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 可讀。
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)
| 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 呼叫 |
重構步驟:
| 情境 | 行為 |
|---|---|
| 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 |
| 編號 | 問題 | 我建議 |
|---|---|---|
| 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」) |
SspProjectResolver + require_ssp_manager