Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/prisma/rls/calendar-isolation.sql
T
2026-07-08 16:15:37 +02:00

94 lines
4.5 KiB
SQL

-- AK-P16a · P16 Calendar-Sync (Google) — RLS ROLLOUT ARTIFACT (deferred, NOT in the auto-migration)
-- =============================================================================
-- Companion to email-isolation.sql / apps-engine-isolation.sql / hr-isolation.sql.
-- The AK-P16a tables + enums + indexes ship in the tracked merge migration
-- (prisma/migrations/<ts>_p16a_calendar, created at main-merge per AD-21). What
-- lives in THIS file is the Row-Level-Security block, DELIBERATELY kept out of the
-- auto-migration path: enabling RLS + FORCE before every access sets the GUC would
-- make queries return ZERO rows, so activation is a coordinated cutover (R-P2b's
-- org-wide P5 RLS cutover), not a schema diff.
--
-- SCOPE: every AK-P16a table is ALWAYS tenant-scoped, so all get policies here:
-- CalendarConnection, CalendarEvent, CalendarEventMapping, CalendarSyncCursor,
-- CalendarWebhookEvent, CalendarConflict.
-- NOTE on per-user leak-protection (DoD 3): RLS isolates by TENANT. The stronger
-- per-USER ownership rule (external event contents are owner-only) is enforced in
-- the service layer (calendarAccess.server.ts owner-check + where: { userId }).
-- CalendarEventMapping has no userId column; its isolation is tenant-level here
-- and owner-level via its parent CalendarConnection in code.
--
-- CODE-SIDE PRECONDITIONS — DONE (AK-P16a):
-- 1. [x] Every AK-P16a web/worker service runs each DB op for these tables
-- inside withTenantRLS() (calendar sync/write/conflict paths).
-- The app-level scoping (where: { tenantId, userId }) carries isolation until
-- cutover.
--
-- WHAT REMAINS — "Teil B" = the org-wide P5 RLS cutover (Raffael / R-P2b):
-- a. Ship this block as its own forward-only migration.
-- b. Switch the runtime DB connection to the restricted role `hivegrid_app`
-- (NOSUPERUSER, NOBYPASSRLS); otherwise FORCE is a no-op for a superuser and
-- an empty-read trap for a non-superuser owner. The GRANTs below cover it.
-- c. Staging cutover + live cross-tenant 404 verification.
--
-- Forward-only (DoD 28). Inverse SQL as comment per block. Order-dependent DDL is
-- DO-guarded (P3018 protection: reproducible from an empty DB). Idempotent.
-- =============================================================================
-- Row-level security on all AK-P16a tenant-scoped calendar tables --------------
-- Pattern: "tenantId" = current_setting('app.tenant_id', true). The second
-- argument `true` returns NULL instead of erroring when the GUC is not (yet) set
-- — the policy then matches nothing (fail-closed).
--
-- Inverse per table:
-- REVOKE SELECT, INSERT, UPDATE, DELETE ON "<tbl>" FROM hivegrid_app;
-- DROP POLICY IF EXISTS "<tbl>_tenant_isolation" ON "<tbl>";
-- ALTER TABLE "<tbl>" NO FORCE ROW LEVEL SECURITY;
-- ALTER TABLE "<tbl>" DISABLE ROW LEVEL SECURITY;
DO $$
DECLARE
tbl text;
BEGIN
FOREACH tbl IN ARRAY ARRAY[
'CalendarConnection',
'CalendarEvent',
'CalendarEventMapping',
'CalendarSyncCursor',
'CalendarWebhookEvent',
'CalendarConflict'
]
LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', tbl);
-- FORCE so the table owner is subject to the policies too (otherwise the
-- owner silently bypasses RLS).
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY;', tbl);
IF NOT EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = current_schema()
AND tablename = tbl
AND policyname = tbl || '_tenant_isolation'
) THEN
EXECUTE format(
'CREATE POLICY %I ON %I USING ("tenantId" = current_setting(''app.tenant_id'', true)) WITH CHECK ("tenantId" = current_setting(''app.tenant_id'', true));',
tbl || '_tenant_isolation', tbl
);
END IF;
-- The restricted app role (R-P2b's hivegrid_app, NOBYPASSRLS) needs table
-- privileges to reach the rows the policy then filters.
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I TO hivegrid_app;', tbl);
END LOOP;
END
$$;
-- =============================================================================
-- Verify green before the cutover migration (AGENTS.md §Prisma):
-- pnpm prisma validate
-- pnpm prisma migrate status
-- CI: `prisma migrate deploy` against an EMPTY DB (reproducible from scratch).
-- Coordination: align ordering with R-P2b — P2 ships withTenantRLS()
-- (SET LOCAL app.tenant_id); without a tenant context the policies match nothing
-- (no data leak, just empty results) until the runtime role is hivegrid_app.
-- =============================================================================