SSP 參與方(Parties — 單位 / 人員)
功能群:專案管理|上層見 系統安全計畫編輯器(本頁=其兩個子 Tab「單位」與「參與人員」,同一
/partiesAPI 只差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 的
metadata(parties.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 |
3. 權限矩陣
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 讀清單 | 參與者即可 | require_participant |
app/oscal/service/ssp_party_app_service.py:69(list_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 僅標代表性):
📸 截圖待補(亮色模式):單位子 Tab(DataTable + Dialog)
sspparty-org.png、參與人員子 Tabsspparty-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(filter、showClear) |
否 | 人員 userMenu(value=user.id)/ 單位
orgUnitMenu(value=org.id) |
matched_user_id / matched_org_unit_id(→
props) |
選後自動帶入 name(新增時)、email(人員);已被別列用的排除 |
| 2 | role_label 角色 |
Dropdown | 是 | 9 OSCAL
role(sspPartyRoleMenu,見下) |
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_400015 → parties.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
row(delete_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 |
sspPartyRoleMenu(fetchSspPartyRoleMenu →
/system/menu/ssp_party_role)/ userMenu /
orgUnitMenu(picker cache) |
8. 後端檔案地圖(本頁核心鏈路)
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| party CRUD | api/oscal/routes/ssp/ssp_party_route.py |
SspPartyAppService(require_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/addressesOSCAL 核心欄(FE 為單一字串、走 props 保 round-trip)。改鍵名要同步 mapper 的_PROP_*常數。
10. 頁面邏輯與資料對應
GET /parties 回全部(不分 type),FE
partiesForType 依 party_type 前端過濾分兩
Tab。欄位對應:
| 畫面元素 | FE state | API 欄位 → DB |
|---|---|---|
| 鉤稽 dropdown | editing.matched_user_id /
matched_org_unit_id |
同名 → props['matched-*-id'] |
| 角色 | editing.role |
role → props['responsible-role'] |
| 名稱 | editing.name |
name → parties.name |
editing.email_address |
email_address → email_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. 邊界情況與已知坑
- DELETE 實際是真刪 party row,route docstring
誤導:
SspPartyResource.delete的 docstring 寫「Remove the party link from this SSP (party row preserved)」,但 v2 serviceremove_party呼叫delete_party(existing.id)直接刪oscal.partiesrow(v2 party 是 metadata-scoped、無 link 表,doc 是 v1 遺留)。刪前留意此 partyuuid是否被ssp_leveraged_authorizations.party_uuid/ap_task_participants軟參照(無 FK 保護,follow-up)。 - 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 行為,維護留意。 name是自然鍵、編輯鎖定:FE 編輯時namedisabled、BE update 也不動type/name(只改 email + props)。要改名=delete + 重建。- role 有 legacy fallback:早期 import adapter
誤把角色存成
props['role'],現行寫responsible-role;讀取_to_dict兩鍵都 fallback,避免舊資料顯示空白。 - 電話 / 地址不落 OSCAL
核心欄:
props['telephone-number']/props['address']是產品擴充(FE 單一字串),parties.telephone_numbers/addressesOSCAL jsonb 陣列本頁 UI 不寫——OSCAL 匯出時留意兩處來源。 - SSP 寫入無 phase guard(BE):唯讀由 FE 鎖;凍結後直接打 API 仍會改 living SSP——同 SSP 編輯器 §12。