Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/verify-rls-release-readiness.test.mjs
T

212 lines
8.0 KiB
JavaScript

import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { formatRlsReleaseReadiness, inspectRlsReleaseReadiness } from "./verify-rls-release-readiness.mjs";
const temporaryRoots = [];
function fixtureRoot() {
const root = mkdtempSync(path.join(os.tmpdir(), "hivegrid-rls-readiness-"));
temporaryRoots.push(root);
return root;
}
function write(root, relativePath, contents) {
const target = path.join(root, relativePath);
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, contents, "utf8");
}
function tenantModel(name = "TenantRow") {
return `model ${name} {
id String @id
tenantId String
}
`;
}
function directPolicy(table = "TenantRow") {
return `ALTER TABLE "${table}" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "${table}" FORCE ROW LEVEL SECURITY;
CREATE POLICY "${table}_tenant" ON "${table}"
USING ("tenantId" = current_setting('app.tenant_id', true))
WITH CHECK ("tenantId" = current_setting('app.tenant_id', true));
GRANT SELECT, INSERT, UPDATE, DELETE ON "${table}" TO hivegrid_app;
`;
}
function tenantRequiredManifest(...models) {
return Object.fromEntries(models.map((model) => [model, { scope: "tenant-required", enforcement: "tenant-policy" }]));
}
function inspectFixture(root, ...models) {
return inspectRlsReleaseReadiness(root, { manifest: tenantRequiredManifest(...models) });
}
afterEach(() => {
for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true });
});
describe("RLS release readiness verifier", () => {
it("passes only when artifact and active migration both cover every tenant model", () => {
const root = fixtureRoot();
write(root, "prisma/schema/core.prisma", tenantModel());
write(root, "prisma/rls/core-isolation.sql", directPolicy());
write(root, "prisma/migrations/20260101000000_rls/migration.sql", directPolicy());
const result = inspectFixture(root, "TenantRow");
expect(result.ok).toBe(true);
expect(result.failures).toEqual([]);
expect(formatRlsReleaseReadiness(result)).toContain("ENABLE 1/1, FORCE 1/1, POLICY 1/1");
});
it("recognizes schema-qualified pg_dump RLS statements in a baseline migration", () => {
const root = fixtureRoot();
write(root, "prisma/schema/core.prisma", tenantModel());
write(root, "prisma/rls/core-isolation.sql", directPolicy());
write(
root,
"prisma/migrations/00000000000000_baseline/migration.sql",
directPolicy().replaceAll('ALTER TABLE "', 'ALTER TABLE public."').replace(' ON "', ' ON public."')
);
const result = inspectFixture(root, "TenantRow");
expect(result.ok).toBe(true);
expect(result.migrationWithoutEnable).toEqual([]);
expect(result.migrationWithoutForce).toEqual([]);
expect(result.migrationWithoutPolicy).toEqual([]);
});
it("does not count commented rollout examples as deployed RLS", () => {
const root = fixtureRoot();
write(root, "prisma/schema/core.prisma", tenantModel());
write(root, "prisma/rls/core-isolation.sql", directPolicy());
write(root, "prisma/migrations/20260101000000_schema/migration.sql", `-- ${directPolicy().replaceAll("\n", "\n-- ")}`);
const result = inspectFixture(root, "TenantRow");
expect(result.ok).toBe(false);
expect(result.migrationWithoutEnable).toHaveLength(1);
expect(result.migrationWithoutForce).toHaveLength(1);
expect(result.migrationWithoutPolicy).toHaveLength(1);
});
it("reports schema-grouped artifact gaps instead of treating tenantId registry coverage as RLS", () => {
const root = fixtureRoot();
write(root, "prisma/schema/accounts.prisma", tenantModel("TenantMembership") + tenantModel("TenantInvite"));
const result = inspectFixture(root, "TenantMembership", "TenantInvite");
const output = formatRlsReleaseReadiness(result);
expect(result.missingArtifacts.map((entry) => entry.model)).toEqual(["TenantInvite", "TenantMembership"]);
expect(output).toContain("accounts.prisma: TenantInvite, TenantMembership");
expect(output).toContain("Release blocker: artifact SQL is not deployment");
});
it("includes FK-derived children without a direct tenantId in the RLS denominator", () => {
const root = fixtureRoot();
write(
root,
"prisma/schema/module.prisma",
`model TenantParent {
id String @id
tenantId String
children TenantChild[]
}
model TenantChild {
id String @id
parentId String
parent TenantParent @relation(fields: [parentId], references: [id])
}
`
);
const manifest = {
TenantParent: { scope: "tenant-required", enforcement: "tenant-policy" },
TenantChild: { scope: "derived-child", enforcement: "join-policy" },
};
const result = inspectRlsReleaseReadiness(root, { manifest });
expect(result.scopeValidation.errors).toEqual([]);
expect(result.models.map((entry) => entry.model)).toEqual(["TenantChild", "TenantParent"]);
expect(result.missingArtifacts).toHaveLength(2);
});
it("recognizes DO-loop rollout artifacts without mistaking them for migrations", () => {
const root = fixtureRoot();
write(root, "prisma/schema/module.prisma", tenantModel());
write(
root,
"prisma/rls/module-isolation.sql",
`DO $$
DECLARE tbl text;
BEGIN
FOREACH tbl IN ARRAY ARRAY['TenantRow'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', tbl);
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY;', tbl);
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', tbl);
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I TO hivegrid_app;', tbl);
END LOOP;
END $$;
`
);
const result = inspectFixture(root, "TenantRow");
expect(result.missingArtifacts).toEqual([]);
expect(result.artifactWithoutEnable).toEqual([]);
expect(result.artifactWithoutForce).toEqual([]);
expect(result.artifactWithoutPolicy).toEqual([]);
expect(result.artifactWithoutGrant).toEqual([]);
expect(result.ok).toBe(false);
expect(result.migrationWithoutPolicy).toHaveLength(1);
});
it("rejects a policy that uses the wrong tenant context name", () => {
const root = fixtureRoot();
write(root, "prisma/schema/core.prisma", tenantModel());
const wrongPolicy = directPolicy().replaceAll("app.tenant_id", "app.current_tenant_id");
write(root, "prisma/rls/core-isolation.sql", wrongPolicy);
write(root, "prisma/migrations/20260101000000_rls/migration.sql", wrongPolicy);
const result = inspectFixture(root, "TenantRow");
expect(result.artifactWithoutPolicy).toHaveLength(1);
expect(result.migrationWithoutPolicy).toHaveLength(1);
expect(result.ok).toBe(false);
});
it("recognizes one direct grant covering multiple quoted tables", () => {
const root = fixtureRoot();
write(root, "prisma/schema/core.prisma", tenantModel("FirstRow") + tenantModel("SecondRow"));
const policies =
directPolicy("FirstRow").replace(/GRANT[^;]+;/, "") +
directPolicy("SecondRow").replace(/GRANT[^;]+;/, "") +
'GRANT SELECT, INSERT, UPDATE, DELETE ON "FirstRow", "SecondRow" TO hivegrid_app;\n';
write(root, "prisma/rls/core-isolation.sql", policies);
const result = inspectFixture(root, "FirstRow", "SecondRow");
expect(result.artifactWithoutGrant).toEqual([]);
});
it("accepts intentionally partial DML grants for immutable app policies", () => {
const root = fixtureRoot();
write(root, "prisma/schema/core.prisma", tenantModel());
const policy = directPolicy().replace("GRANT SELECT, INSERT, UPDATE, DELETE", "GRANT SELECT, INSERT");
write(root, "prisma/rls/core-isolation.sql", policy);
write(root, "prisma/migrations/20260101000000_rls/migration.sql", policy);
const result = inspectFixture(root, "TenantRow");
expect(result.artifactWithoutGrant).toEqual([]);
expect(result.ok).toBe(true);
});
});