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.
This commit is contained in:
2026-08-08 13:13:32 +09:00
parent 30c941cc5b
commit a84e5c1273
5 changed files with 58 additions and 4 deletions
@@ -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.
@@ -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."
1 WBS_ID Sprint Slice_ID Task Status Completion_Date Evidence_Link Owner Notes
11 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.
12 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.
13 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.
14 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.
15 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
16 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.
17 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.
+13 -3
View File
@@ -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`);
}
}
@@ -17,6 +17,13 @@ export interface CrudResourceDefinition<TQuery, TRow, TForm> {
}
export function assertCrudResourceDefinition(definition: CrudResourceDefinition<unknown, unknown, unknown>): 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`)
}
}
@@ -0,0 +1,16 @@
import { z } from 'zod'
import { describe, expect, it } from 'vitest'
import { assertCrudResourceDefinition, type CrudResourceDefinition } from '../resourceDefinition'
const definition: CrudResourceDefinition<unknown, unknown, unknown> = {
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'))
})