Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,89 @@
-- AK-P4a · P4 Files — RLS ROLLOUT ARTIFACT (deferred, NOT in the auto-migration)
-- =============================================================================
-- Companion to R-P2b's prisma/rls/tenant-isolation.sql. The Files tables + enums
-- + indexes + the pg_trgm display-name index ship in the tracked migration
-- prisma/migrations/20260623160000_p4a_files_platform. What lives in THIS file is
-- the Row-Level-Security block, DELIBERATELY kept out of the auto-migration path
-- (same stance as R-P2b): turning RLS on before every access sets the GUC would
-- break the feature, so activation is a coordinated cutover, not a schema diff.
--
-- WHY DEFERRED: enabling RLS + FORCE makes every File query WITHOUT
-- `app.tenant_id` return ZERO rows. Per R-P2b's rule a table may only be
-- RLS-enabled once ALL its accesses set the GUC. The app-level scoping
-- (where: { tenantId }) carries isolation meanwhile.
--
-- CODE-SIDE PRECONDITIONS — DONE (AK-P4a "Teil A", 2026-06-24):
-- 1. [x] File web loaders/actions run inside withTenantRLS()
-- (apps/web/app/utils/integrations/fileService.server.ts — all DB ops).
-- 2. [x] Worker write-backs set the tenant GUC before file.update/updateMany
-- (jobs/file-preview.ts, jobs/file-scan-status.ts via
-- apps/worker/src/db/tenantRls.ts).
-- Proven by apps/web/app/utils/db/.server/rls-file-isolation.integration.test.ts
-- (7/7 against real Postgres: own-tenant only, 0 cross-tenant, 0 without context,
-- WITH CHECK blocks foreign insert; File + FileEntityLink).
--
-- 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 already
-- cover that role.
-- 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 the four Files tables --------------------------------
-- Pattern: tenant_id = 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), and server-side scoping
-- stays the hard path until the cutover.
--
-- 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['File', 'FileEntityLink', 'FileUpload', 'FileUploadChunk']
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. Mirrors the GRANT in
-- prisma/rls/tenant-isolation.sql for "TenantUser".
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,
-- no order dependency on Raffael's P2 migration).
-- 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.
-- =============================================================================
@@ -0,0 +1,101 @@
-- AK-P4b · P4 Tasks + EntityLink — RLS ROLLOUT ARTIFACT (deferred, NOT in the auto-migration)
-- =============================================================================
-- Companion to R-P2b's prisma/rls/tenant-isolation.sql and AK-P4a's
-- prisma/rls/file-isolation.sql. The Task / TaskWatcher / EntityLink tables +
-- enums + indexes + the single-assignee CHECK + the pg_trgm title index ship in
-- the tracked migration prisma/migrations/<ts>_p4b_tasks_entitylinks. What lives
-- in THIS file is the Row-Level-Security block, DELIBERATELY kept out of the
-- auto-migration path (same stance as file-isolation.sql): turning RLS on before
-- every access sets the GUC would break the feature, so activation is a
-- coordinated cutover, not a schema diff.
--
-- WHY DEFERRED: enabling RLS + FORCE makes every Task/TaskWatcher/EntityLink query
-- WITHOUT `app.tenant_id` return ZERO rows. Per R-P2b's rule a table may only be
-- RLS-enabled once ALL its accesses set the GUC. The app-level scoping
-- (where: { tenantId }) carries isolation meanwhile.
--
-- CODE-SIDE PRECONDITIONS — DONE (AK-P4b "Teil A", 2026-06-24):
-- 1. [x] Task/EntityLink web services run every DB op inside withTenantRLS()
-- (apps/web/app/utils/integrations/taskService.server.ts,
-- apps/web/app/utils/integrations/entityLinkService.server.ts).
-- The only unwrapped access is the GLOBAL User lookup (resolveUserLabels);
-- User is not tenant-scoped, so it intentionally stays on the db singleton.
-- 2. [x] Worker maintenance crons set the tenant GUC per tenant: the archive +
-- reminder sweeps enumerate active tenants from the global Tenant
-- registry and run the Task/TaskWatcher reads/writes inside
-- withTenantRLS() (apps/worker/src/handlers.ts via
-- apps/worker/src/db/tenantRls.ts). notify() runs outside the tx.
-- Proven by apps/web/app/utils/db/.server/rls-task-isolation.integration.test.ts
-- (own-tenant only, 0 cross-tenant, 0 without context, WITH CHECK blocks foreign
-- insert; Task + TaskWatcher + EntityLink).
--
-- 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 already
-- cover that role for the three Task tables.
-- c. MAINTENANCE-WORKER NOTE: the cross-tenant archive/reminder sweeps read the
-- global "Tenant" registry to enumerate tenants. Under hivegrid_app that
-- needs `GRANT SELECT ON "Tenant" TO hivegrid_app` (the registry holds no
-- per-tenant payload, only tenant ids/slugs) OR the maintenance worker runs
-- under a privileged connection. Decide with R-P2b at cutover.
-- d. 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 the three Tasks/EntityLink tables ---------------------
-- Pattern: tenant_id = 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), and server-side scoping
-- stays the hard path until the cutover.
--
-- 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['Task', 'TaskWatcher', 'EntityLink']
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. Mirrors the GRANT in
-- prisma/rls/tenant-isolation.sql for "TenantUser".
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,
-- no order dependency on Raffael's P2 migration).
-- 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.
-- =============================================================================
@@ -0,0 +1,45 @@
-- R-P2b — RLS-Gate-Beweis (AD-1): mandantenuebergreifender Zugriff DB-seitig verhindert.
--
-- ⚠️ DELIBERATES ROLLOUT-ARTEFAKT, NICHT im Auto-Migrations-Pfad (AD-21):
-- RLS auf einer Tabelle scharfzuschalten macht JEDE Query ohne gesetzten app.tenant_id
-- ergebnislos. Eine Tabelle darf daher erst RLS-aktiviert werden, wenn ALLE ihre Zugriffe
-- ueber withTenantRLS() laufen (Tenant-Kontext gesetzt). Dieses Skript etabliert die
-- bewiesene Mechanik + die restricted App-Rolle und schaltet RLS exemplarisch auf
-- "TenantUser" (Gate-Beweis). Module ziehen das Muster beim Bau pro Tabelle nach.
--
-- Idempotent. Inverse-SQL (DoD 28) am Dateiende.
-- 1) Restricted Application-Rolle: NICHT Superuser, NICHT BYPASSRLS, NICHT Tabellen-Owner
-- -> RLS-Policies greifen fuer diese Rolle (Superuser/Owner wuerden sonst bypassen).
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hivegrid_app') THEN
CREATE ROLE hivegrid_app LOGIN PASSWORD 'hivegrid_app_pw' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;
ELSE
ALTER ROLE hivegrid_app NOSUPERUSER NOBYPASSRLS;
END IF;
END$$;
GRANT USAGE ON SCHEMA public TO hivegrid_app;
-- 2) Tenant-Isolation per Row-Level-Security auf "TenantUser" (tenantId NOT NULL).
-- FORCE: auch der Tabellen-Owner ist der Policy unterworfen (Defense-in-Depth).
-- current_setting('app.tenant_id', true): liefert NULL, wenn der Kontext nicht gesetzt ist
-- -> "tenantId = NULL" ist niemals wahr -> ohne Kontext sieht die App-Rolle KEINE Zeile.
ALTER TABLE "TenantUser" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "TenantUser" FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "TenantUser";
CREATE POLICY tenant_isolation ON "TenantUser"
USING ("tenantId" = current_setting('app.tenant_id', true))
WITH CHECK ("tenantId" = current_setting('app.tenant_id', true));
GRANT SELECT, INSERT, UPDATE, DELETE ON "TenantUser" TO hivegrid_app;
-- Inverse (Rollback):
-- REVOKE SELECT, INSERT, UPDATE, DELETE ON "TenantUser" FROM hivegrid_app;
-- DROP POLICY IF EXISTS tenant_isolation ON "TenantUser";
-- ALTER TABLE "TenantUser" NO FORCE ROW LEVEL SECURITY;
-- ALTER TABLE "TenantUser" DISABLE ROW LEVEL SECURITY;
-- REVOKE USAGE ON SCHEMA public FROM hivegrid_app;
-- DROP ROLE IF EXISTS hivegrid_app;