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
46 lines
1.6 KiB
SQL
46 lines
1.6 KiB
SQL
-- CreateEnum
|
|
CREATE TYPE "GrantSubjectType" AS ENUM ('USER', 'AUTHENTICATED', 'PUBLIC');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "GrantRole" AS ENUM ('POND_ADMIN', 'EDITOR', 'READER');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "GrantScopeType" AS ENUM ('POND', 'LABEL', 'PAGE');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "GrantEffect" AS ENUM ('ALLOW', 'DENY');
|
|
|
|
-- DropIndex
|
|
DROP INDEX "page_content_cache_search_vector_idx";
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "role_grants" (
|
|
"id" TEXT NOT NULL,
|
|
"pond_id" TEXT NOT NULL,
|
|
"subject_type" "GrantSubjectType" NOT NULL,
|
|
"subject_id" TEXT,
|
|
"role" "GrantRole" NOT NULL,
|
|
"scope_type" "GrantScopeType" NOT NULL,
|
|
"scope_id" TEXT,
|
|
"effect" "GrantEffect" NOT NULL,
|
|
"created_by" TEXT NOT NULL,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "role_grants_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "role_grants_pond_id_idx" ON "role_grants"("pond_id");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "role_grants_pond_id_subject_type_subject_id_role_scope_type_key" ON "role_grants"("pond_id", "subject_type", "subject_id", "role", "scope_type", "scope_id");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "role_grants" ADD CONSTRAINT "role_grants_pond_id_fkey" FOREIGN KEY ("pond_id") REFERENCES "ponds"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- Structural backstop (permissions.md, issue #51): a POND_ADMIN grant is only
|
|
-- valid at pond scope for a specific user. The personal-pond single-admin rule
|
|
-- needs the pond type and is enforced in the service, not here.
|
|
ALTER TABLE "role_grants" ADD CONSTRAINT "role_grants_pond_admin_scope_check"
|
|
CHECK ("role" <> 'POND_ADMIN' OR ("scope_type" = 'POND' AND "subject_type" = 'USER'));
|