級別:重型(Phase C 核心 refactor) 依賴:C2(SSP items endpoints 可讀寫,C3 改 derived 路徑依賴 SSP items 是 source of truth) 被依賴:C4(request schema 變動)、C5(UI 拿掉專案範圍編輯)、C6(FE 同步) 不可獨立 ship — 必須跟 C4 同 PR / 同 release
把「受評範圍」(哪些設備 / 資訊系統屬於這次稽核)從 project 層遷移到 SSP 層(per-AP)。實務上稽核範圍會隨 AP 週期變動(年度增減設備),語意上應該 per-SSP。
完成後:
dto.audit_systems / dto.devices 由 current AP 對應 SSP 的 system_implementation_items derive 出來compliance.project_device_mapping + compliance.project_information_systems 兩張表廢除domain/associations/ + infra/associations/ + app/associations/ 整套 stack 拆除jedi_information_system/...project_information_system_* 系列同樣拆除| 議題 | 決策 |
|---|---|
| C3-Q1 Migration 策略 | 補洞 + idempotent:把 project_device_mapping / project_information_systems 既有資料 INSERT 到對應 current SSP 的 system_implementation_items,重複資料 skip |
| C3-Q2 廢除順序 | 兩階段:PR1 = service 改 derived + write endpoint 退 410 → 觀察 1-2 週 → PR2 = DROP TABLE + 拆 stack |
| C3-Q3 API breaking | 回 410 Gone(寫操作);GET 改 derived 仍可用 |
| C3-Q4 DROP scope | 完整清:table + RLS policy + FK + index + sequence |
| C3-Q5 空資料 | 回空 list [] |
目的:FE 仍可拿到 device / audit_systems response,但不再寫 project mapping 表
動作清單:
project_device_mapping 既有資料 INSERT 到對應 current SSP 的 ssp_system_implementation_items(type='hardware')project_information_systems 既有資料 INSERT(type='system',system_characteristic_id soft ref)ON CONFLICT DO NOTHING 或 WHERE NOT EXISTS idempotentGrcProjectService.get_project() 內 dto.devices / dto.audit_systems 改從 current SSP 的 items 抓[]POST/PUT/DELETE /project-device → 回 410 Gone + msg「請至 SSP 編輯頁維護」POST/PUT/DELETE /project/job-execution-device 同上update_project Section 3/5 寫操作改 no-op(接受 request 但不寫,回 410 也可)前提:觀察 PR1 後 user / FE 都正常運作,無 regression
動作清單:
DROP TABLE compliance.project_device_mapping CASCADE(含 RLS / FK / index / sequence)DROP TABLE compliance.project_information_systems CASCADEgit rm -r domain/associations/git rm -r infra/associations/git rm -r app/associations/git rm jedi_information_system/...project_information_system_*檔名:scripts/sql/2026-MM-DD-c3-backfill-ssp-items-from-project-mappings.sql
-- Date: 2026-MM-DD
-- Purpose: C3.PR1 — 補洞:把 project_device_mapping / project_information_systems
-- 既有資料 backfill 到對應 current SSP 的 ssp_system_implementation_items
-- 帳號:cmmgr(避 RLS)
-- 策略:idempotent — 重複資料 skip
BEGIN;
-- ─────────────────────────────────────────────────────────────────────────────
-- Step 1 — 找每個 project 對應的 current SSP(最新 active AP 的 SSP)(2026-MM-DD)
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TEMP TABLE _project_current_ssp AS
SELECT
p.id AS project_id,
p.uid AS project_uid,
ssp.id AS ssp_id,
ssp.uid AS ssp_uid,
si.id AS system_implementation_id -- main row
FROM compliance.projects p
JOIN compliance.project_assessment_plan_mapping papm ON papm.project_id = p.id
JOIN oscal.assessment_plans ap ON ap.id = papm.assessment_plan_id
JOIN oscal.system_security_plans ssp ON ssp.id = ap.ssp_id
JOIN oscal.ssp_system_implementations si
ON si.scope_type = 'ssp' AND si.scope_id = ssp.id
WHERE ap.status = 'active' -- 取 active AP 對應 SSP
-- 若一個 project 有多個 active AP,取最新一個
AND ap.id = (
SELECT ap2.id FROM oscal.assessment_plans ap2
JOIN compliance.project_assessment_plan_mapping papm2 ON papm2.assessment_plan_id = ap2.id
WHERE papm2.project_id = p.id AND ap2.status = 'active'
ORDER BY ap2.created_at DESC
LIMIT 1
);
-- 統計:應該每個有 active AP 的專案都有對應 row
-- SELECT COUNT(*) FROM _project_current_ssp;
-- ─────────────────────────────────────────────────────────────────────────────
-- Step 2 — Backfill devices (project_device_mapping → ssp_system_implementation_items) (2026-MM-DD)
-- ─────────────────────────────────────────────────────────────────────────────
INSERT INTO oscal.ssp_system_implementation_items (
uid, system_security_plan_id, system_implementation_id,
scope_type, scope_id, implementation_type,
name, device_id, created_user, updated_user
)
SELECT
gen_random_uuid(),
pcs.ssp_id,
pcs.system_implementation_id,
'ssp',
pcs.ssp_id,
'hardware',
d.name,
d.id,
'c3-backfill-2026-MM-DD',
'c3-backfill-2026-MM-DD'
FROM compliance.project_device_mapping pdm
JOIN _project_current_ssp pcs ON pcs.project_id = pdm.project_id
JOIN public.devices d ON d.id = pdm.device_id
WHERE NOT EXISTS (
-- idempotent:current SSP 內已有同 device_id 就 skip
SELECT 1 FROM oscal.ssp_system_implementation_items existing
WHERE existing.scope_type = 'ssp'
AND existing.scope_id = pcs.ssp_id
AND existing.implementation_type = 'hardware'
AND existing.device_id = d.id
);
-- ─────────────────────────────────────────────────────────────────────────────
-- Step 3 — Backfill information_systems (project_information_systems → items) (2026-MM-DD)
-- ─────────────────────────────────────────────────────────────────────────────
-- 找對應 SSP characteristic id(每個 SSP 有 1:1 characteristic)
CREATE TEMP TABLE _ssp_characteristic AS
SELECT id, system_security_plan_id
FROM oscal.system_security_plans_system_characteristics;
INSERT INTO oscal.ssp_system_implementation_items (
uid, system_security_plan_id, system_implementation_id,
scope_type, scope_id, implementation_type,
name, system_characteristic_id, created_user, updated_user
)
SELECT
gen_random_uuid(),
pcs.ssp_id,
pcs.system_implementation_id,
'ssp',
pcs.ssp_id,
'system',
isys.name,
sc.id,
'c3-backfill-2026-MM-DD',
'c3-backfill-2026-MM-DD'
FROM compliance.project_information_systems pis
JOIN _project_current_ssp pcs ON pcs.project_id = pis.project_id
JOIN compliance.information_systems isys ON isys.id = pis.information_system_id
LEFT JOIN _ssp_characteristic sc ON sc.system_security_plan_id = pcs.ssp_id
WHERE NOT EXISTS (
SELECT 1 FROM oscal.ssp_system_implementation_items existing
WHERE existing.scope_type = 'ssp'
AND existing.scope_id = pcs.ssp_id
AND existing.implementation_type = 'system'
AND existing.name = isys.name -- 用 name 為 idempotent key(characteristic_id 可能 NULL)
);
-- ─────────────────────────────────────────────────────────────────────────────
-- Step 4 — 驗證 (2026-MM-DD)
-- ─────────────────────────────────────────────────────────────────────────────
-- backfill 後計數
DO $$
DECLARE
dev_backfilled int;
sys_backfilled int;
BEGIN
SELECT COUNT(*) INTO dev_backfilled
FROM oscal.ssp_system_implementation_items
WHERE updated_user = 'c3-backfill-2026-MM-DD' AND implementation_type='hardware';
SELECT COUNT(*) INTO sys_backfilled
FROM oscal.ssp_system_implementation_items
WHERE updated_user = 'c3-backfill-2026-MM-DD' AND implementation_type='system';
RAISE NOTICE 'C3 backfill done: % hardware items, % system items', dev_backfilled, sys_backfilled;
END $$;
-- 對照 — 應該大致對等(除了沒 active AP 的專案):
-- SELECT 'project_device_mapping', COUNT(*) FROM compliance.project_device_mapping
-- UNION ALL
-- SELECT 'project_information_systems', COUNT(*) FROM compliance.project_information_systems;
DROP TABLE _project_current_ssp;
DROP TABLE _ssp_characteristic;
COMMIT;前置條件:
-- C3.PR1 rollback:刪除 backfill 出來的 SSP items
BEGIN;
DELETE FROM oscal.ssp_system_implementation_items
WHERE updated_user = 'c3-backfill-2026-MM-DD';
COMMIT;GrcProjectService.get_project() — derived response改動前:
def get_project(self, uid, ...):
...
# 從 project_device_mapping 抓 devices
dev_mappings = self._device_mapping_service.get_all_by_project_uid(uid)
dto.devices = [DeviceSummaryDto(...) for dm in dev_mappings if dm.device]
# 從 project_information_systems 抓 audit_systems
pis_entities = self._pis_domain_service.get_all(
ProjectInformationSystemQueryEntity(project_id=entity.id)
)
...改動後:
def get_project(self, uid, ...):
...
# 從 current AP 對應 SSP 的 implementation_items derive
current_ssp_id = self._resolve_current_ssp_id(project_id=entity.id)
if current_ssp_id is None:
dto.devices = []
dto.audit_systems = []
else:
items = self._ssp_item_domain_service.get_all(
SspSystemImplementationItemQueryEntity(
system_security_plan_id=current_ssp_id, scope_type='ssp',
)
)
# devices ← items where implementation_type='hardware'
# audit_systems ← items where implementation_type IN ('component','system','subsystem','service','software')
dto.devices = [self._item_to_device_summary(i) for i in items if i.implementation_type == 'hardware']
dto.audit_systems = [self._item_to_system_summary(i) for i in items if i.implementation_type in ('component','system','subsystem','service','software')]
def _resolve_current_ssp_id(self, project_id: int) -> Optional[int]:
# 找 project → latest active AP → SSP
...注意:_resolve_current_ssp_id 邏輯與 C2 ProjectCurrentSspRoute 一致,可抽共用 helper。
GrcProjectService.update_project() — 寫操作改 410改動前:Section 3 / 5 處理 audit_systems / devices 替換
改動後(PR1):
建議:搭配 C4 同 PR 一起拿掉 request schema fields,service 內對應段落同步拿掉。/project-device / /project/job-execution-device POST/PUT/DELETE endpoint 改 410。
/project-device 系列改 410(PR1)— 用 GrcErrorCode 走專案規範檔案:api/project/routes/project_device_route.py 等
新增 error code(加入 common/code/grc_error_code.py):
# 410 Gone — endpoint 已下架
GRC_PROJECT_DEVICE_DEPRECATED = (
"專案設備寫操作已下架,請至 SSP 編輯頁維護受評範圍",
"GRC_410001",
)
GRC_PROJECT_INFO_SYSTEM_DEPRECATED = (
"專案資訊系統寫操作已下架,請至 SSP 編輯頁維護受評範圍",
"GRC_410002",
)Route 改動:
from common.code.grc_error_code import GrcErrorCode
class ProjectDeviceRoute(MethodResource):
def post(self):
return self._gone_response(GrcErrorCode.GRC_PROJECT_DEVICE_DEPRECATED)
def put(self, ...):
return self._gone_response(GrcErrorCode.GRC_PROJECT_DEVICE_DEPRECATED)
def delete(self, ...):
return self._gone_response(GrcErrorCode.GRC_PROJECT_DEVICE_DEPRECATED)
@staticmethod
def _gone_response(error_code):
"""回傳 410 Gone,FE 用 error code 查 i18n。"""
msg, code = error_code.value # GrcErrorCode tuple 解構
return {
"code": 0,
"msg": msg, # zh-tw fallback(FE 優先用 code 查 i18n)
"data": {
"error_code": code, # FE 用此 code 查 i18n
"ssp_endpoints": [ # 引導 FE 改用 SSP-scoped endpoints
"GET /projects/<project_uid>/current-ssp-uid",
"POST /ssp/<ssp_uid>/ssp-resources/items"
]
}
}, 410 # HTTP 410 GoneFE i18n 對應(FE repo src/config/locales/i18n/zh-tw/error.json):
{
"error": {
"GRC_410001": "專案設備寫操作已下架,請至 SSP 編輯頁維護受評範圍",
"GRC_410002": "專案資訊系統寫操作已下架,請至 SSP 編輯頁維護受評範圍"
}
}FE 拿到 response 後優先用 data.error_code 查 i18n 顯示,msg 為 fallback。
GET 操作保留正常(改 derived from SSP)。
新檔:
scripts/sql/2026-MM-DD-c3-backfill-ssp-items-from-project-mappings.sqldomain/oscal/service/ssp_current_resolver.py(_resolve_current_ssp_id 抽 helper)改檔:
app/grc/service/project_service.py — get_project derived;update_project 拿掉 Section 3/5app/project/service/oscal_project_service.py — start_oscal_project 拿掉 Step Capi/project/routes/project_device_route.py — 寫操作回 410api/project/routes/project_*.py 其他 device write endpoints — 同上api/project/__init__.py — route 仍註冊(410 由 route 內部回,URL 仍存在)api/project/serializers/project.py — OscalProjectStartRequest 拿掉 audit_systems/devices(C4 同期)api/grc/serializers/project.py — ProjectUpdateRequestSchema 同上不動(保留至 PR2):
domain/associations/ — 整個保留infra/associations/ — 整個保留app/associations/ — 整個保留compliance.project_device_mapping table — 保留compliance.project_information_systems table — 保留新檔:
scripts/sql/2026-MM-DD-c3-drop-project-mapping-tables.sql刪檔:
domain/associations/* 整個infra/associations/* 整個app/associations/* 整個jedi_information_system/...project_information_system_* 系列api/project/routes/project_device_route.py — 廢檔(route 內 410 拿掉)api/project/routes/project_*.py 其他 device endpoints 同上改檔:
api/project/__init__.py — 拿掉 ProjectDevicesRoute / ProjectDeviceRoute 等註冊domain/oscal/strategy/ssp_write_strategy.py:258-277 — 拿掉 TODO Phase E.2 段-- Date: 2026-MM-DD
-- Purpose: C3.PR2 — DROP project_device_mapping + project_information_systems
-- 帳號:cmmgr
BEGIN;
-- ─────────────────────────────────────────────────────────────────────────────
-- DROP project_device_mapping (2026-MM-DD)
-- ─────────────────────────────────────────────────────────────────────────────
-- 確認沒有殘留 caller
-- SELECT COUNT(*) FROM compliance.project_device_mapping;
-- 此時可能還有資料但已無 caller 引用
-- 廢 RLS policy
DROP POLICY IF EXISTS project_device_mapping_select ON compliance.project_device_mapping;
DROP POLICY IF EXISTS project_device_mapping_insert ON compliance.project_device_mapping;
DROP POLICY IF EXISTS project_device_mapping_update ON compliance.project_device_mapping;
DROP POLICY IF EXISTS project_device_mapping_delete ON compliance.project_device_mapping;
-- 廢 FK / index(CASCADE 會處理大部分,但顯式列出較明確)
ALTER TABLE compliance.project_device_mapping
DROP CONSTRAINT IF EXISTS uq_project_device_mapping;
DROP INDEX IF EXISTS compliance.ix_pdm_project_id;
DROP INDEX IF EXISTS compliance.ix_pdm_device_id;
-- 廢 table
DROP TABLE IF EXISTS compliance.project_device_mapping CASCADE;
-- DROP TABLE 會自動 drop sequence 跟 owned objects
-- ─────────────────────────────────────────────────────────────────────────────
-- DROP project_information_systems (2026-MM-DD)
-- ─────────────────────────────────────────────────────────────────────────────
DROP POLICY IF EXISTS project_information_systems_select ON compliance.project_information_systems;
DROP POLICY IF EXISTS project_information_systems_insert ON compliance.project_information_systems;
DROP POLICY IF EXISTS project_information_systems_update ON compliance.project_information_systems;
DROP POLICY IF EXISTS project_information_systems_delete ON compliance.project_information_systems;
ALTER TABLE compliance.project_information_systems
DROP CONSTRAINT IF EXISTS uq_project_information_system;
DROP TABLE IF EXISTS compliance.project_information_systems CASCADE;
COMMIT;
-- 驗證
-- \dt compliance.project_device_mapping -- 預期:Did not find any relation
-- \dt compliance.project_information_systems -- 同上| 風險 | 嚴重度 | 緩解 |
|---|---|---|
| Backfill SQL 漏資料(無 active AP 的 project) | 🟡 中 | Pre-flight 跑 SQL 統計沒有 active AP 的 project 數量 + 列名單,由業務決定是否補建 AP |
| Backfill 後 SSP items 出現 user 沒預期的「設備」 | 🟡 中 | backfill 用 c3-backfill-2026-MM-DD 標記在 updated_user,可一次性 audit / cleanup |
| PR1 後 reviewer/auditor 角色看不到 device/audit_systems | 🟢 低 | derived response 對所有角色都回(permission 只擋寫),仍可看 |
| PR2 DROP 後發現有遺漏 caller | 🔴 高 | PR1 ~ PR2 之間 1-2 週觀察期;PR1 加 SQL audit query 監控 mapping 表 INSERT 是否 0 |
| 既有 dashboard / search 依賴 mapping 表(沒在 caller 盤點到) | 🔴 高 | PR1 前期需再次 grep pg_class WHERE relname='project_device_mapping' + RLS / view 依賴 |
| jedi-information-system 套件外被引用 | 🟡 中 | 套件層 grep 確認 |
| backfill 資料量大 timeout | 🟢 低 | dev 環境總量小(< 1000 row),prod 跑前估算 |
PR1 ship 後到 PR2 期間,跑 SQL 監控確認 mapping 表沒有新 INSERT:
-- 應該 0(PR1 後寫操作回 410,無新增)
SELECT COUNT(*) FROM compliance.project_device_mapping
WHERE created_at > '<PR1-ship-date>';
SELECT COUNT(*) FROM compliance.project_information_systems
WHERE created_at > '<PR1-ship-date>';若觀察到非 0,表示有遺漏 caller,要先補修才能進 PR2。
| 編號 | 決策 |
|---|---|
| C3-D1 | ✅ Backfill SQL 人工跑(PR1 ship 後 DBA 監督執行) |
| C3-D2 | ✅ PR1 ~ PR2 觀察期 1 週 |
| C3-D3 | ✅ 沒 active AP 的 project:backfill 跳過(user 啟動 AP 後 SSP 從 MF clone 帶範圍) |
| C3-D4 | ✅ 410 response 用 GrcErrorCode pattern(GRC_410001 / GRC_410002);FE 用 error_code 查 i18n,msg 為 zh-tw fallback。對齊專案規範。 |
| C3-D5 | ✅ OscalRole ORM 保留(OSCAL 文件結構用,跟 system_menus 服務不同對象);C1 同步 seed oscal_roles 9 筆 |
PR1 後 (2026-05-23 上午, commit range d2c3df2..1253a92):
dto.devices / dto.audit_systems 從 current SSP derivePR2 後 (2026-05-23 下午, commit 0960070 + 143f354):
scripts/sql/2026-05-23-c3-pr2-drop-project-mapping-tables.sql 已執行)git rmapi/project/__init__.py 拿掉 register)_project_info_sys_repo)拿掉PR2 vs 原 plan T11 偏差:plan 寫 git rm -r app/associations/ 過度激進,實際只能拆 device 兩個系列;保留其他 6 個 mapping(org_unit / system_characteristic / assessment_plan / task_workflow / profile_assessment_workflow)+ job_execution_device(屬「任務指派裝置」非 C3 scope)。詳見 changelog 2026-05-23-tweak-c3-pr2-drop-project-mapping-tables.md §「Plan 矯正」。
跳過觀察期:原 plan §11 寫 PR1 ship 後 1-2 週觀察期再進 PR2,user 確認 DB 有備份、 production 已 0 caller,跳過直接拔。
C3 是重型 phase,建議獨立 test-plan:
implementation-plan-C3.md 內 §測試規格