All checks were successful
CD / Build and push images (push) Successful in 3m3s
CI / Lint, typecheck, test (push) Successful in 2m21s
CI / Auth e2e pack (push) Successful in 2m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
The heart of the security model: one algorithm, implemented once, for API, collab, and UI (permissions.md — authoritative). - shared `permissions/`: pure resolution (`resolvePageCapability`) exactly per permissions.md — specificity page > label (incl. ancestor labels) > pond, deny wins within a level, default-closed, Site Admin bypass — plus the trash rule (`canAccessPage` / `canAccessTrashedPage`, ADR 0013). `grantValidationError` enforces the structural constraints. Documented, I/O-free signatures for API/collab reuse. - prisma: `RoleGrant` (+ grant enums) per data-model.md, unique on (pond, subject, role, scope); migration adds a CHECK backstop that a POND_ADMIN grant is pond-scope + user-subject. - api `grants/`: `GrantsService.createGrant` validates before insert (structural + no extra admin on a personal pond), rejects duplicates; `grantsForPond` returns the shared resolver model (what #52/#53 consume); enum mappers between the DB and the shared model. Interim "who may manage grants" stays until #52. - tests: exhaustive table-driven resolver suite — every worked example from permissions.md §Resolution, edge cases (multi-label deny-wins, ancestor inheritance, anonymous/public, most-specific-allow-beats-less-specific-deny, trash) and a property test (a less-specific grant never overrides a more-specific decision); validation unit tests; grants db test proving write-time rejection of invalid grants. - i18n: grant error codes (de + en). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { Grant } from './types';
|
|
import { grantValidationError } from './validate';
|
|
|
|
const base: Grant = {
|
|
subjectType: 'user',
|
|
subjectId: 'u1',
|
|
role: 'editor',
|
|
scopeType: 'pond',
|
|
scopeId: null,
|
|
effect: 'allow',
|
|
};
|
|
|
|
describe('grantValidationError (issue #51)', () => {
|
|
it('accepts a valid editor grant', () => {
|
|
expect(grantValidationError(base, { pondType: 'shared' })).toBeNull();
|
|
});
|
|
|
|
it('accepts a valid pond_admin grant on a shared pond', () => {
|
|
expect(
|
|
grantValidationError({ ...base, role: 'pond_admin' }, { pondType: 'shared' }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('rejects pond_admin at label scope', () => {
|
|
const g: Grant = { ...base, role: 'pond_admin', scopeType: 'label', scopeId: 'l1' };
|
|
expect(grantValidationError(g, { pondType: 'shared' })).toBe('grant_pond_admin_scope');
|
|
});
|
|
|
|
it('rejects pond_admin with a non-user subject', () => {
|
|
const g: Grant = { ...base, role: 'pond_admin', subjectType: 'authenticated', subjectId: null };
|
|
expect(grantValidationError(g, { pondType: 'shared' })).toBe('grant_pond_admin_scope');
|
|
});
|
|
|
|
it('rejects a second admin on a personal pond', () => {
|
|
const g: Grant = { ...base, role: 'pond_admin' };
|
|
expect(grantValidationError(g, { pondType: 'personal' })).toBe(
|
|
'grant_pond_admin_personal_pond',
|
|
);
|
|
});
|
|
|
|
it('rejects a user subject without an id, and a non-user subject with one', () => {
|
|
expect(grantValidationError({ ...base, subjectId: null }, { pondType: 'shared' })).toBe(
|
|
'grant_subject_id_mismatch',
|
|
);
|
|
expect(
|
|
grantValidationError(
|
|
{ ...base, subjectType: 'public', subjectId: 'x' },
|
|
{ pondType: 'shared' },
|
|
),
|
|
).toBe('grant_subject_id_mismatch');
|
|
});
|
|
|
|
it('rejects a scoped grant without an id, and a pond grant with one', () => {
|
|
expect(
|
|
grantValidationError({ ...base, scopeType: 'label', scopeId: null }, { pondType: 'shared' }),
|
|
).toBe('grant_scope_id_mismatch');
|
|
expect(grantValidationError({ ...base, scopeId: 'p1' }, { pondType: 'shared' })).toBe(
|
|
'grant_scope_id_mismatch',
|
|
);
|
|
});
|
|
});
|