手冊首頁 / 專案與任務 / SSP 參與方

SSP 參與方(Parties — 單位 / 人員)

功能群:專案管理|上層見 系統安全計畫編輯器(本頁=其兩個子 Tab「單位」與「參與人員」,同一 /parties API 只差 party_type 過濾)

事實基準:2026-07-06 從 FE / BE code 掃出;表結構對 db_schema.json、endpoint 對 routes.json、欄位對 marshmallow serializer + mapper 組裝碼逐條核

1. 功能描述

參與方是 SSP 的 parties 維護區:登錄與此系統範圍相關的單位(organization)人員(person)。UI 是同一個 Vue 元件ModuleFramePartiesPanel.vue)掛兩次——SSP 編輯器的子 Tab「單位」(party-type=organization)與「參與人員」(party-type=person),兩者共用同一支 /ssp/{uid}/parties API,僅以 party_type 過濾與預設。故本頁一頁涵蓋兩個子 Tab,避免重複 drift。

每筆 party 可「鉤稽」既有的系統帳號(人員 → userMenu)或組織單位(單位 → orgUnitMenu),把 matched_user_id / matched_org_unit_id 軟參照存進 props,讀取時 enrich 回 nickname / 單位名。party 掛在 SSP 的 metadataparties.metadata_id)之下,無獨立 mapping 表。與 SSP 其餘子區共用可編輯 gate(manager + AP=planning),凍結後唯讀。

主要使用者:專案 manager(寫)、專案任一參與者(讀)。

角色速覽(完整定義見 _overview §3):

角色 一句話
manager 專案經理 — 唯一能增改刪參與方者(需 AP=planning)
參與者 auditor / reviewer / viewer — 可讀不可寫

⚠️ 注意:此處「role」指 party 的 OSCAL 角色(system-owner / authorizing-official 等 9 種,見 §5),與上表的專案角色(manager / auditor…)是兩件事——前者是這筆單位/人員在 SSP 裡擔任的職責,後者是使用者對專案的權限。

1.1 功能總覽(本頁全部功能)

# 功能 說明 位置 詳述
1 單位清單 party_type=organization 的 party(DataTable) 子 Tab 單位 §5
2 人員清單 party_type=person 的 party 子 Tab 參與人員 §5
3 新增 / 編輯 party 鉤稽帳號/單位 或 自由輸入(role + name + email/電話/地址) Dialog UC-SP-01
4 鉤稽帳號 / 組織單位 userMenu/orgUnitMenu → 軟參照 + 自動帶名/email Dialog Dropdown UC-SP-01
5 刪除 party 移除一筆(真刪 party row,見 §12) 每列 §6.2

UC(§2)展開 UC-SP-01(新增/編輯 party),涵蓋鉤稽 vs 自由輸入、role/name 必填(FE + BE 雙檢)、name 編輯鎖定(自然鍵)三個關鍵分支;清單顯示屬純顯示見 §5。

2. Use Case

角色速覽(同 §1)。流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。

UC-SP-01 新增 / 編輯參與方(單位 / 人員)

項目 內容
角色 專案 manager
前置條件 已載入 living SSP;AP=planning;鉤稽時對應帳號 / 組織單位須存在於 menu
產出 / 後置條件 oscal.parties(掛 SSP metadata_id);email→email_addresses[0]、role/title/電話/地址/matched_* → props;編輯不動 party_type/name(自然鍵);回傳 enrich 後 DTO
sspparty_uc_01 start open 開新增/編輯 Dialog(限 manager+AP planning) 子 Tab 決定 party_type(單位=organization / 人員=person) start->open link 鉤稽既有帳號 / 組織單位?(選填) open->link linkfill pick userMenu/orgUnitMenu → 帶入 matched_*_id 新增時自動填 name(人員再帶 email) link->linkfill fields 選 role(必填,預設 人員=system-owner / 單位=responsible-organization)+ name(編輯時鎖,自然鍵) + email / 電話 / 地址(選填) link->fields linkfill->fields save FE 檢查:name 與 role 皆非空? fields->save feerr FE toast warn(name/role 必填,不送出) save->feerr api 新增 POST /ssp/{uid}/parties 編輯 PUT .../parties/{party_uid}(strip None) save->api mgr BE 檢查:manager 且 AP 可編輯? api->mgr permerr 非 manager → GRC_403051 AP 關閉/封存 → GRC_412028 mgr->permerr valid BE 驗證:party_type∈{person,organization} 且 name 非空 且 role 非空? mgr->valid valerr type 錯 → GRC_400014 name 空 → GRC_400015 / role 空 → GRC_400016 valid->valerr write 寫 oscal.parties(掛 SSP metadata_id) email→email_addresses[0]、role/title/phone/address/matched→props 新增無同名去重(MF 版才有) valid->write done 回 DTO(enrich matched_user_name/uid、 matched_org_unit_name)→ 重載 write->done
UC-SP-01 流程:開 Dialog(manager+AP planning,子 Tab 定 party_type)→可選鉤稽帳號/單位帶入 matched+name+email→選 role(必填有預設)+name(編輯鎖)+聯絡方式→FE 檢查 name/role 非空(缺 toast 不送)→POST/PUT strip None→BE require_manager(GRC_403051/412028)→BE 驗 type/name/role(GRC_400014/400015/400016)→寫 oscal.parties(新增無去重)→enrich 重載

3. 權限矩陣

操作 FE 判定 BE 強制 BE 檢查位置
讀清單 參與者即可 require_participant app/oscal/service/ssp_party_app_service.py:69list_parties
新增 / 編輯 / 刪除 readonly prop(非 manager / AP 非 planning 時父層傳入 → 隱藏新增/編輯/刪除鈕) require_manager(manager + AP 可編輯,否則 GRC_403051 / GRC_412028 同檔 add_party:78 / update_party:99 / remove_party:115

route 層僅 @jwt_required(),角色守門在 app service 的 SspPermissionChecker

4. 狀態機與前置條件

承接 SSP 可編輯 gate(見 SSP 編輯器 §4):AP=planning 且 manager 才可寫,凍結後唯讀。額外前置:

條件 效果 出處
編輯既有 party name 欄 disabled(自然鍵,只能改聯絡方式 / role / 鉤稽) ModuleFramePartiesPanel.vue:458:disabled="!!editing.uid"
鉤稽 picker 已被其他列使用 該選項從 dropdown 排除(編輯時保留自身) 同檔 availablePickerOptions:197-210

5. UI 設計

版面骨架與區塊職責(單位 / 人員兩子 Tab 各一張 DataTable,共用同一 Vue 元件與 Dialog;彩色區塊可點跳本頁小節錨點,API 僅標代表性):

SSP 編輯器 › 兩子 Tab:[單位 organization]/[參與人員 person] — 同一 ModuleFramePartiesPanel.vue 掛兩次,共用 /parties API 跳至 §2 Use Case(UC-SP-01 單位:鉤稽 orgUnitMenu 或自由輸入) 子 Tab「單位」DataTable(party_type=organization) 名稱 | 角色(Tag) | email | 地址 | 鉤稽狀態 | 動作 資安處 ・ owner ・ sec@corp ・ 台北 ・ ✓已鉤稽 | ✎ 🗑 稽核室 ・ assessor ・ aud@corp ・ 台北 ・ — | ✎ 🗑 「新增」→ 鉤稽組織單位 orgUnitMenu (→ matched_org_unit_id 軟參照)或自由輸入 GET/POST /ssp/{ssp_uid}/parties GET /system/menu/ssp_party_role(9 role) → 點我跳 §2 UC-SP-01 跳至 §2 Use Case(UC-SP-01 人員:鉤稽 userMenu 或自由輸入) 子 Tab「參與人員」DataTable(party_type=person) 名稱 | 角色(Tag) | email | 電話 | 鉤稽狀態 | 動作 王小明 ・ assessor ・ ming@corp ・ 0912… ・ ✓已鉤稽 | ✎ 🗑 李四 ・ poc ・ lee@corp ・ 0928… ・ — | ✎ 🗑 「新增」→ 鉤稽系統帳號 userMenu (→ matched_user_id + 帶 name/email)或自由輸入 PUT/DELETE /ssp/{ssp_uid}/parties/{party_uid} GET /users/menu(鉤稽帳號來源) → 點我跳 §2 UC-SP-01 新增/編輯 Dialog(單位/人員共用同一組,只差 picker 來源,開啟才出現) 鉤稽 Dropdown | 角色*(9 OSCAL role)| 名稱*(編輯時 disabled,自然鍵)| email/電話/地址 BE 驗 type/name/role → GRC_400014 / 400015 / 400016;掛 SSP metadata_id,無獨立 mapping 表 彩色=可點跳本頁小節錨點(框內標錨點+代表性 API)|灰=本頁描述|虛線框=Dialog(開啟才出現) API 僅列代表性 2~3 條,完整規格見 §6;本頁無 socket,寫入後 invalidateFn + 重載
SSP 參與方頁版面 wireframe:子 Tab 切換列、單位 DataTable、參與人員 DataTable、共用新增/編輯 Dialog(9 OSCAL role)

📸 截圖待補(亮色模式):單位子 Tab(DataTable + Dialog)sspparty-org.png、參與人員子 Tab sspparty-person.png

每個子 Tab 一張 DataTable + 一個「新增」開 Dialog。DataTable 欄位:名稱(+副行 title)/ 角色(Tag info,roleLabel i18n)/ email / 電話 / 地址 / 鉤稽狀態(已鉤稽 Tag success 顯示 matched 名、否則 Tag warning「未鉤稽」)/ 動作(編輯 + 刪除)。

Dialog 欄位ModuleFramePartiesPanel.vue template,單位 / 人員共用同一組,只差 picker 來源):

# i18n label 型別 必填 列舉 / 來源 儲存 備註
1 link_account(人員)/ link_org(單位) 鉤稽 Dropdown(filtershowClear 人員 userMenu(value=user.id)/ 單位 orgUnitMenu(value=org.id) matched_user_id / matched_org_unit_id(→ props) 選後自動帶入 name(新增時)、email(人員);已被別列用的排除
2 role_label 角色 Dropdown 9 OSCAL rolesspPartyRoleMenu,見下) props['responsible-role'] 預設:人員 system-owner、單位 responsible-organization
3 field_name 名稱 InputText name 編輯時 disabled(自然鍵)
4 field_email email InputText email_addresses[0](text[]) 單位 / 人員都顯示
5 field_phone 電話 InputText props['telephone-number'] FE 單一字串,走 props
6 field_address 地址 InputText props['address'] FE 單一字串,走 props

9 個 OSCAL party role(seed scripts/sql/2026-05-22-seed-oscal-ssp-roles.sql,master data system_menus group=ssp_party_role):responsible-organization / system-owner / system-security-officer / authorizing-official / information-owner / information-provider / information-receiver / prepared-by / prepared-for。person / organization 共用同 9 個(C1 spec 的 party_type 提示為建議非強制)。

狀態呈現:無 socket,寫入後 invalidateFn + 重載;子 Tab header 帶計數徽章(count-change emit);readonly 時隱藏新增/編輯/刪除鈕。

6. API 規格

Envelope:成功 {"status": true, "data": …}、失敗 {"error_code": "…", "msg": "…"}

6.1 總清單

分類 Method + Path 說明 完整規格
party GET / POST /ssp/{ssp_uid}/parties 列出(不分 type,FE 前端過濾)/ 新增 §6.2
party PUT / DELETE /ssp/{ssp_uid}/parties/{party_uid} 編輯(type/name 不動)/ 刪除 §6.2
picker GET /system/menu/ssp_party_role 9 個 OSCAL role master GAI-SD-02
picker GET /users/menu、org unit menu 鉤稽帳號 / 組織單位來源 GAI-SD-02

6.2 核心 endpoint

[POST] /ssp/{ssp_uid}/parties

新增 party。Request(SspPartyUpsertSchema):

{
  "party_type": "person",
  "name": "王小明",
  "role": "system-owner",
  "title": "資安長",
  "email_address": "ming@example.com",
  "telephone_number": "02-1234-5678",
  "address": "台北市…",
  "matched_user_id": 42,
  "matched_org_unit_id": null
}
欄位 型別 必填 說明
party_type string OneOf person / organization(非此 → GRC_400014
name string 空 → GRC_400015parties.name
role string 空 → GRC_400016;9 role 之一 → props['responsible-role']
title string|null props['title']
email_address string|null email_addresses[0](text[];空 → None
telephone_number string|null props['telephone-number']
address string|null props['address']
matched_user_id int|null 人員鉤稽 → props['matched-user-id'](軟參照)
matched_org_unit_id int|null 單位鉤稽 → props['matched-org-unit-id'](軟參照)

出處:app/oscal/service/ssp_party_app_service.py:76-95、mapper app/module_frame/service/module_frame_party_service.py:_build_props:300-323

[GET] /ssp/{ssp_uid}/parties

Response data(陣列,SspPartyResponseSchema + enrich):

欄位 型別 說明
uid string party uuid(改刪用)
party_type string type
name string name
role string props['responsible-role'](legacy fallback role;空回 ""
title string|null props['title']
email_address string|null email_addresses[0]
telephone_number / address string|null props['telephone-number'] / props['address']
matched_user_id / matched_org_unit_id int|null ← props 軟參照
matched_user_uid string|null enrich:由 matched_user_id 反查 user uid
matched_user_name string|null enrich:user nickname(fallback login_name)
matched_org_unit_name string|null enrich:org unit name

enrich 出處:module_frame_party_service.py:_enrich_user_org_names:212-245(批次查 UserQueryEntity(_in_id=…) + get_org_units_by_id_list)。

[PUT] /ssp/{ssp_uid}/parties/{party_uid}

編輯。party_type / name 是自然鍵,不更新(FE 亦鎖 name);只改 email_address 與 props(role/title/電話/地址/matched_*,非破壞性合併)。route strip None。party_uid 不存在 → GRC_404024。出處:ssp_party_app_service.py:97-111

[DELETE] /ssp/{ssp_uid}/parties/{party_uid}

刪除。實際行為=真刪 oscal.parties rowdelete_party(existing.id))——⚠️ route docstring 寫「party row preserved」是 v1 遺留、與現行 v2 實作不符(見 §12)。party_uid 不存在 → GRC_404024。出處:ssp_party_app_service.py:113-118

7. 前端檔案地圖(compliance-manager-fe/)

檔案 角色
src/components/grc/ModuleFramePartiesPanel.vue 本子區(單一 type 的 DataTable + Dialog + 鉤稽 picker;SSP 與 MF 共用,apiBase 區別)
src/service/SspService.js party CRUD(或經 BaseService 直打 ${API.SSP}/{uid}/parties
src/stores/menuStore.js sspPartyRoleMenufetchSspPartyRoleMenu/system/menu/ssp_party_role)/ userMenu / orgUnitMenu(picker cache)

8. 後端檔案地圖(本頁核心鏈路)

鏈路 Route App Service 底層
party CRUD api/oscal/routes/ssp/ssp_party_route.py SspPartyAppServicerequire_participant 讀 / require_manager 寫;經 SspPermissionChecker oscal.parties(掛 SSP metadata_id);純 mapping 共用 ModuleFramePartyService_build_props / _to_dict / _enrich_user_org_names,單一真相)

DDD 提醒:app service 不自寫 mapping,直接複用範本版 ModuleFramePartyService 的 helper。⚠️ SSP 版 add_party 無同名去重,MF 版有(見 §12)——共用 helper 但 public method 各自實作,勿誤以為行為相同。

9. DB

9.1 資料表總清單(本頁讀寫的全部表)

讀寫 說明 詳述
oscal.parties 讀 / 單位 / 人員本體(掛 SSP metadata) §9.3
oscal.metadata party 的 containment 父(parties.metadata_id GAI-SD-03
public users / org_units 鉤稽 enrich 來源(軟參照,非 FK) GAI-SD-03

9.2 ER 圖

party 隸屬 SSP metadata(containment),並被 leveraged / responsible 以 uuid 軟式參照,見 SSP 編輯器 §9.2 ER;本頁不另繪。

9.3 核心表欄位(取自 DEV DB dump,含 DB comment)

oscal.parties

欄位 型別 說明
metadata_id bigint containment FK → oscal.metadata.id(party 掛接點,ON DELETE CASCADE;非獨立 mapping 表
uuid uuid [必填] party 全域識別(對外 uid;被 leveraged/responsible 軟參照)
type varchar(20) [必填] person / organization(決定子 Tab)
name text [選填 OSCAL / 本頁必填] 正式全名
email_addresses text[] [選填] email 清單(本頁只寫 [0]
telephone_numbers jsonb [選填] OSCAL 電話陣列——本頁 UI 的電話走 props['telephone-number'],非此欄
addresses jsonb [選填] OSCAL 地址陣列——本頁 UI 的地址走 props['address'],非此欄
props jsonb [選填] [{name,value}];本頁保留鍵 responsible-role(legacy role)/ title / telephone-number / address / matched-user-id / matched-org-unit-id
created_user / updated_user varchar(255) Audit:login_name

⚠️ schema 註:本頁 UI 的 role / title / 電話 / 地址 / 鉤稽 一律存 props,不是 DB 的 telephone_numbers / addresses OSCAL 核心欄(FE 為單一字串、走 props 保 round-trip)。改鍵名要同步 mapper 的 _PROP_* 常數。

10. 頁面邏輯與資料對應

GET /parties 回全部(不分 type),FE partiesForTypeparty_type 前端過濾分兩 Tab。欄位對應

畫面元素 FE state API 欄位 → DB
鉤稽 dropdown editing.matched_user_id / matched_org_unit_id 同名 → props['matched-*-id']
角色 editing.role roleprops['responsible-role']
名稱 editing.name nameparties.name
email editing.email_address email_addressemail_addresses[0]
電話 / 地址 editing.telephone_number / .address props['telephone-number'] / props['address']

儲存流程saveEditing 先 client 檢查 name.trim() + role 非空(缺 → toast warn 不送)→ POST(新增)/ PUT(editing.uid)→ invalidateFn → toast → 重載。鉤稽自動帶入onPickLinked 選帳號/單位後,新增時帶 name、人員再帶 email。錯誤對應GRC_400014/GRC_400015/GRC_400016(type/name/role)/ GRC_404024(party 不存在)/ GRC_403051/GRC_412028(權限)→ 全域 handleAxiosError

11. 背景行為與外部依賴

類型 內容
socket invalidateFn + 重載)
store sspPartyRoleMenu(9 role master)/ userMenu / orgUnitMenu(picker,一次性 cache 多 panel 共用)
enrich 讀取時由 matched_*_id 批次反查 user nickname/uid + org unit name(軟參照,非 FK JOIN)
共用 同一元件被 module-frame 範本編輯復用(apiBase='module-frame');⚠️ 但 add 去重行為兩端不同

12. 邊界情況與已知坑

  1. DELETE 實際是真刪 party row,route docstring 誤導SspPartyResource.delete 的 docstring 寫「Remove the party link from this SSP (party row preserved)」,但 v2 service remove_party 呼叫 delete_party(existing.id) 直接刪 oscal.parties row(v2 party 是 metadata-scoped、無 link 表,doc 是 v1 遺留)。刪前留意此 party uuid 是否被 ssp_leveraged_authorizations.party_uuid / ap_task_participants 軟參照(無 FK 保護,follow-up)。
  2. SSP 版新增無同名去重、MF 版有ModuleFramePartyService.add_party 有 case-insensitive 同名 ConflictError(MODULE_FRAME_ORG_NAME_EXISTED / PERSON_NAME_EXISTED);但 SspPartyAppService.add_party 沒有這段——SSP scope 可建同名 party。兩端共用 mapping helper 卻不共用 public method 行為,維護留意。
  3. name 是自然鍵、編輯鎖定:FE 編輯時 name disabled、BE update 也不動 type/name(只改 email + props)。要改名=delete + 重建。
  4. role 有 legacy fallback:早期 import adapter 誤把角色存成 props['role'],現行寫 responsible-role;讀取 _to_dict 兩鍵都 fallback,避免舊資料顯示空白。
  5. 電話 / 地址不落 OSCAL 核心欄props['telephone-number'] / props['address'] 是產品擴充(FE 單一字串),parties.telephone_numbers / addresses OSCAL jsonb 陣列本頁 UI 不寫——OSCAL 匯出時留意兩處來源。
  6. SSP 寫入無 phase guard(BE):唯讀由 FE 鎖;凍結後直接打 API 仍會改 living SSP——同 SSP 編輯器 §12

13. 開發與驗證

  • 跑起來:起 BE + FE;登入見 .env / 部署文件測試帳號(需該專案 manager)
  • 導航:規劃頁 Tab1「系統安全計畫」→ 子 Tab「單位」或「參與人員」
  • 前置資料:需有 living SSP 的專案(AP=planning);備幾個 user / org unit 測鉤稽 + enrich;測編輯時 name 鎖定、測同名可否新增(SSP 可、MF 擋)
  • 相關:其餘 SSP 子區見 基本資料 / 元件 / 利用授權 / 資產清冊;OSCAL parties 概念見 oscal-knowledge skill