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
80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
import { Grant, GrantEffect, GrantRole, GrantScopeType, GrantSubjectType } from '@dorfteich/shared';
|
|
import {
|
|
GrantEffect as PrismaEffect,
|
|
GrantRole as PrismaRole,
|
|
GrantScopeType as PrismaScopeType,
|
|
GrantSubjectType as PrismaSubjectType,
|
|
RoleGrant,
|
|
} from '@prisma/client';
|
|
|
|
/**
|
|
* Maps between the database's uppercase grant enums and the shared model's
|
|
* lowercase strings (issue #51), so the resolution algorithm in
|
|
* `@dorfteich/shared` sees one canonical shape regardless of storage.
|
|
*/
|
|
|
|
const SUBJECT_TO_DB: Record<GrantSubjectType, PrismaSubjectType> = {
|
|
user: 'USER',
|
|
authenticated: 'AUTHENTICATED',
|
|
public: 'PUBLIC',
|
|
};
|
|
const ROLE_TO_DB: Record<GrantRole, PrismaRole> = {
|
|
pond_admin: 'POND_ADMIN',
|
|
editor: 'EDITOR',
|
|
reader: 'READER',
|
|
};
|
|
const SCOPE_TO_DB: Record<GrantScopeType, PrismaScopeType> = {
|
|
pond: 'POND',
|
|
label: 'LABEL',
|
|
page: 'PAGE',
|
|
};
|
|
const EFFECT_TO_DB: Record<GrantEffect, PrismaEffect> = { allow: 'ALLOW', deny: 'DENY' };
|
|
|
|
const SUBJECT_FROM_DB: Record<PrismaSubjectType, GrantSubjectType> = {
|
|
USER: 'user',
|
|
AUTHENTICATED: 'authenticated',
|
|
PUBLIC: 'public',
|
|
};
|
|
const ROLE_FROM_DB: Record<PrismaRole, GrantRole> = {
|
|
POND_ADMIN: 'pond_admin',
|
|
EDITOR: 'editor',
|
|
READER: 'reader',
|
|
};
|
|
const SCOPE_FROM_DB: Record<PrismaScopeType, GrantScopeType> = {
|
|
POND: 'pond',
|
|
LABEL: 'label',
|
|
PAGE: 'page',
|
|
};
|
|
const EFFECT_FROM_DB: Record<PrismaEffect, GrantEffect> = { ALLOW: 'allow', DENY: 'deny' };
|
|
|
|
/** A stored grant row → the shared model shape. */
|
|
export function toGrant(row: RoleGrant): Grant {
|
|
return {
|
|
subjectType: SUBJECT_FROM_DB[row.subjectType],
|
|
subjectId: row.subjectId,
|
|
role: ROLE_FROM_DB[row.role],
|
|
scopeType: SCOPE_FROM_DB[row.scopeType],
|
|
scopeId: row.scopeId,
|
|
effect: EFFECT_FROM_DB[row.effect],
|
|
};
|
|
}
|
|
|
|
/** The shared grant shape → the Prisma enum columns (for writes). */
|
|
export function toGrantColumns(grant: Grant): {
|
|
subjectType: PrismaSubjectType;
|
|
subjectId: string | null;
|
|
role: PrismaRole;
|
|
scopeType: PrismaScopeType;
|
|
scopeId: string | null;
|
|
effect: PrismaEffect;
|
|
} {
|
|
return {
|
|
subjectType: SUBJECT_TO_DB[grant.subjectType],
|
|
subjectId: grant.subjectId,
|
|
role: ROLE_TO_DB[grant.role],
|
|
scopeType: SCOPE_TO_DB[grant.scopeType],
|
|
scopeId: grant.scopeId,
|
|
effect: EFFECT_TO_DB[grant.effect],
|
|
};
|
|
}
|