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:
@@ -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'))
|
||||
})
|
||||
Reference in New Issue
Block a user