From a84e5c1273a5e3f1aa1fa6165c7566be2f3e4c37 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sat, 8 Aug 2026 13:13:32 +0900 Subject: [PATCH] feat: harden CRUD resource contract checks (AEG-V16-020) Reject incomplete permission, version, concurrency, idempotency, and sensitive-column contracts. Preserve runtime regression evidence; status remains IN_PROGRESS pending predecessor acceptance. --- ...AEG-V16-020_CRUD_RESOURCE_V2_SLICE_NOTE.md | 20 +++++++++++++++++++ .../CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv | 1 + .../src/shared/crud/resourceDefinition.js | 16 ++++++++++++--- .../src/shared/crud/resourceDefinition.ts | 9 ++++++++- .../crud/tests/resourceDefinition.spec.ts | 16 +++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 docs/CURRENT/AEG-V16-020_CRUD_RESOURCE_V2_SLICE_NOTE.md create mode 100644 frontend/src/shared/crud/tests/resourceDefinition.spec.ts diff --git a/docs/CURRENT/AEG-V16-020_CRUD_RESOURCE_V2_SLICE_NOTE.md b/docs/CURRENT/AEG-V16-020_CRUD_RESOURCE_V2_SLICE_NOTE.md new file mode 100644 index 00000000..4f1f568d --- /dev/null +++ b/docs/CURRENT/AEG-V16-020_CRUD_RESOURCE_V2_SLICE_NOTE.md @@ -0,0 +1,20 @@ +# AEG-V16-020 — CRUD Resource v2 + +## Scope + +- **WBS / Requirement / UI / Test:** AEG-V16-020 / REQ-V16-FEC-04 / UI-V16-FEC-04 / T-V16-FEC-04 +- **Classification:** shared CRUD contract hardening; no API, database, or policy change. + +## Source / Assumption / Unknown / Decision Required + +- **Source:** `contracts/ui/crud-resource.v2.json`, `frontend/src/shared/crud/resourceDefinition.*`, and the shared CRUD contract types. +- **Assumption:** individual resource definitions originate from approved API contracts; this assertion prevents invalid local definitions but does not authorize a server mutation. +- **Unknown:** predecessor AEG-V16-019 is pending formal UX/a11y evidence. +- **Decision Required:** no new permission names or sensitive-field masks are introduced; resource owners supply them through approved definitions. + +## Acceptance mapping and execution evidence + +- Zod schema/version, permission, concurrency, idempotency, and sensitive-column consistency are explicitly checked. +- A sensitive field now requires both a declared column and `sensitive: true`, avoiding accidental unmasked display. +- `frontend: pnpm test -- --run src/shared/crud/tests/resourceDefinition.spec.ts` passed: 1 file / 3 tests. +- `frontend: pnpm typecheck` passed before the generated JS companion synchronization; the later JS-only synchronization is covered by the passing runtime test. diff --git a/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv b/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv index 7fe93495..547d7e22 100644 --- a/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv +++ b/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv @@ -11,6 +11,7 @@ AEG-V16-016,S0,VS-00,Vendor boundary fitness,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V AEG-V16-015,S0,VS-00,Adapter rollback runbook,IN_PROGRESS,TBD,"docs/CURRENT/ui-provider-switch.md","FE Lead","2026-08-08: Created startup-only provider switch and rollback runbook with fail-closed configuration, immutable-artifact rollback, and append-only evidence record. Visual/a11y/performance rehearsal evidence is not present and is explicitly required before completion; dependency AEG-V16-014 is also not evidenced in the tracker." AEG-V16-018,S6,Cross,DataContextHeader,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-018_DATA_CONTEXT_HEADER_SLICE_NOTE.md; frontend/src/shared/ui/components/KsDataContextHeader.vue; frontend/src/shared/ui/components/tests/KsDataContextHeader.spec.ts","FE Lead","2026-08-08: Made projectionVersion and watermark required so stale/rebuildable read-model context cannot be omitted; added visible and accessible stale state plus VersionSet propagation tests. Actual evidence: targeted Vitest 2/2 PASS, frontend typecheck PASS, production build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending predecessor AEG-V16-017 acceptance and UX/a11y evidence." AEG-V16-019,S6,Cross,CommandBar,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-019_COMMAND_BAR_SLICE_NOTE.md; frontend/src/shared/ui/components/KsCommandBar.vue; frontend/src/shared/ui/components/tests/KsCommandBar.spec.ts","FE Lead","2026-08-08: Command boundary now suppresses disabled/busy execute events and exposes aggregate busy state. Actual evidence: targeted Vitest 1/1 PASS; frontend typecheck PASS. COMPLETED is blocked pending predecessor AEG-V16-018 acceptance and UX/a11y evidence." +AEG-V16-020,S6,Cross,CRUD Resource v2,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-020_CRUD_RESOURCE_V2_SLICE_NOTE.md; contracts/ui/crud-resource.v2.json; frontend/src/shared/crud/resourceDefinition.ts; frontend/src/shared/crud/tests/resourceDefinition.spec.ts","FE Lead","2026-08-08: Hardened runtime resource-definition checks for schema versions, permission policy, concurrency/idempotency modes, and sensitive grid columns. Actual evidence: resource definition Vitest 3/3 PASS; frontend typecheck PASS. COMPLETED is blocked pending predecessor AEG-V16-019 acceptance and UX/a11y evidence." AEG-X-008,S0,Cross,OpenAPI artifact 고도화,COMPLETED,2026-08-04,.gitea/workflows/openapi-gate.yml + docs/api/openapi.json,BE/FE Architect,"✅ OpenAPI diff gate implemented: CI/CD automation detects breaking changes (3 checks: parameter removal, status code removal, field removal), blocks merge without approval, auto-comments on PR" AEG-VS-00-01,S0,VS-00,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-06,"docs/CURRENT/SLICE_SPECS/VS-00-SLICE_SPEC.md + commit e7913db",PM/Architect,"✅ SLICE_SPEC produced: VS-00-SLICE_SPEC.md (state transitions, RBAC, governance gates, DQ rules, compliance). Commit e7913db. 249/253 tests PASS." AEG-VS-00-02,S0,VS-00,데이터 시점·스키마·정합성 계약,COMPLETED,2026-08-06,"contracts/data/platform-data-contract.v1.json + commit e7913db",Data Architect/DBA,"✅ DATA_CONTRACT v1.0 produced: PIT envelope (published_at/correlation_id/revision), 5 table schemas, DQ rules/lineage, GDPR/PCI-DSS compliance. JSON schema + validation. 249/253 tests PASS." diff --git a/frontend/src/shared/crud/resourceDefinition.js b/frontend/src/shared/crud/resourceDefinition.js index a3ee5bbc..f9591b4c 100644 --- a/frontend/src/shared/crud/resourceDefinition.js +++ b/frontend/src/shared/crud/resourceDefinition.js @@ -1,8 +1,18 @@ export function assertCrudResourceDefinition(definition) { if (!definition.resourceId.trim()) throw new Error('resourceId is required'); + if (!definition.querySchemaVersion.trim() || !definition.responseSchemaVersion.trim()) + throw new Error('schema versions are required'); + if (!definition.permissionPolicy.trim()) + throw new Error('permissionPolicy is required'); + if (definition.concurrencyMode !== 'NONE' && definition.concurrencyMode !== 'ETAG_IF_MATCH') + throw new Error('invalid concurrencyMode'); + if (definition.idempotencyMode !== 'NONE' && definition.idempotencyMode !== 'IDEMPOTENCY_KEY') + throw new Error('invalid idempotencyMode'); const fields = new Set(definition.columns.map(x => x.field)); - for (const sensitive of definition.sensitiveFields) - if (!fields.has(sensitive)) - throw new Error(`Sensitive field '${sensitive}' has no grid column contract`); + for (const sensitive of definition.sensitiveFields) { + const column = definition.columns.find(x => x.field === sensitive); + if (!fields.has(sensitive) || !column?.sensitive) + throw new Error(`Sensitive field '${sensitive}' has no sensitive grid column contract`); + } } diff --git a/frontend/src/shared/crud/resourceDefinition.ts b/frontend/src/shared/crud/resourceDefinition.ts index 3d64f0b4..b1119277 100644 --- a/frontend/src/shared/crud/resourceDefinition.ts +++ b/frontend/src/shared/crud/resourceDefinition.ts @@ -17,6 +17,13 @@ export interface CrudResourceDefinition { } export function assertCrudResourceDefinition(definition: CrudResourceDefinition): void { if (!definition.resourceId.trim()) throw new Error('resourceId is required') + if (!definition.querySchemaVersion.trim() || !definition.responseSchemaVersion.trim()) throw new Error('schema versions are required') + if (!definition.permissionPolicy.trim()) throw new Error('permissionPolicy is required') + if (definition.concurrencyMode !== 'NONE' && definition.concurrencyMode !== 'ETAG_IF_MATCH') throw new Error('invalid concurrencyMode') + if (definition.idempotencyMode !== 'NONE' && definition.idempotencyMode !== 'IDEMPOTENCY_KEY') throw new Error('invalid idempotencyMode') const fields = new Set(definition.columns.map(x => x.field)) - for (const sensitive of definition.sensitiveFields) if (!fields.has(sensitive)) throw new Error(`Sensitive field '${sensitive}' has no grid column contract`) + for (const sensitive of definition.sensitiveFields) { + const column = definition.columns.find(x => x.field === sensitive) + if (!fields.has(sensitive) || !column?.sensitive) throw new Error(`Sensitive field '${sensitive}' has no sensitive grid column contract`) + } } diff --git a/frontend/src/shared/crud/tests/resourceDefinition.spec.ts b/frontend/src/shared/crud/tests/resourceDefinition.spec.ts new file mode 100644 index 00000000..b1ccb25d --- /dev/null +++ b/frontend/src/shared/crud/tests/resourceDefinition.spec.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' +import { describe, expect, it } from 'vitest' +import { assertCrudResourceDefinition, type CrudResourceDefinition } from '../resourceDefinition' + +const definition: CrudResourceDefinition = { + resourceId: 'client-advisory', querySchemaVersion: '1', responseSchemaVersion: '1', + querySchema: z.unknown(), rowSchema: z.unknown(), formSchema: z.unknown(), + columns: [{ field: 'email', header: 'Email', sensitive: true }], sensitiveFields: ['email'], + permissionPolicy: 'ClientAdvisory.Read', concurrencyMode: 'ETAG_IF_MATCH', idempotencyMode: 'IDEMPOTENCY_KEY' +} + +describe('CrudResourceDefinition', () => { + it('accepts an explicit secure resource contract', () => expect(() => assertCrudResourceDefinition(definition)).not.toThrow()) + it('rejects sensitive fields without a sensitive column contract', () => expect(() => assertCrudResourceDefinition({ ...definition, columns: [{ field: 'email', header: 'Email' }] })).toThrow('sensitive grid column')) + it('rejects missing permission policy', () => expect(() => assertCrudResourceDefinition({ ...definition, permissionPolicy: ' ' })).toThrow('permissionPolicy')) +})