// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // What the migrate job applies, and the one grant list the global reader gets (decision AT-D20). import { readdirSync, readFileSync } from "node:fs" import { buildRegistry } from "@conusai-kit/core" import { entitiesOf } from "@conusai-kit/data" import { OUTBOX_READER_GRANT } from "@conusai-kit/data-kysely" import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss" import { describe, expect, it } from "vitest" import { CROSS_TENANT_READS, jobSchemaGrants, readerGrants, splitStatements } from "../apps/worker/src/migrations.js" import { appModules } from "../modules/index.js" const dir = new URL("../migrations/postgres/", import.meta.url) const files = readdirSync(dir) .filter((name) => name.endsWith(".sql")) .sort() const committed = files.map((name) => readFileSync(new URL(name, dir), "utf8")).join("\n") const registry = buildRegistry([...appModules]) describe("the committed migrations", () => { it("are numbered, forward only, and start at 0001", () => { expect(files.length).toBeGreaterThan(0) expect(files[0]).toMatch(/^0001_/) for (const name of files) expect(name).toMatch(/^\d{4}_[a-z0-9-]+\.sql$/) }) it("create a table for every entity this app's registry declares", () => { const declared = [...entitiesOf(registry, "postgres"), ...KIT_JOBS_ENTITIES] const inModules = new Set(registry.migrations.postgres.map(({ module }) => module)) for (const entity of declared) { // A module that ships its own migration folder is allowed to own its tables there. if (inModules.size > 0 && !committed.includes(`create table "${entity.table}"`)) continue expect(committed, `no migration creates ${entity.table}`).toContain(`create table "${entity.table}"`) } }) it("split into statements without losing a dollar-quoted body", () => { const statements = splitStatements('create table "a" ("x" int);\n\ndo $$ begin perform 1; end $$;\n') expect(statements).toHaveLength(2) expect(statements[1]).toContain("do $$ begin perform 1; end $$") }) }) describe("the global reader's grants", () => { const migration = readerGrants("postgresql://app_reader:secret@db:5432/app") const sql = migration.statements.join(";\n") const tables = ["outbox", "org_memberships", "projects", "auth_api_keys"] it("grant the cross-tenant reads the kit modules declare, the outbox, and no other table", () => { // The list is the modules' own declarations, so adding a read to a module adds the grant here // and a table nobody declared is never granted. expect(CROSS_TENANT_READS.map((read) => read.entity.table).sort()).toEqual([ "auth_api_keys", "org_memberships", "projects", ]) const granted = migration.statements.filter((statement) => statement.startsWith("grant select")) expect(granted).toHaveLength(tables.length) for (const table of tables) expect(sql).toContain(`on "${table}" to "app_reader"`) }) it("let the reader write nothing but the columns the relay keeps its books in", () => { // A grant on the whole row would let the reader rewrite an event's organization, name or // payload, which the relay then hands to a subscriber running as a system actor. const update = migration.statements.filter((statement) => statement.startsWith("grant update")) expect(update).toHaveLength(1) expect(update[0]).toContain('on "outbox" to "app_reader"') for (const column of OUTBOX_READER_GRANT.update ?? []) expect(update[0]).toContain(`"${column}"`) expect(update[0]).not.toContain('"payload"') expect(update[0]).not.toContain('"org_id"') expect(update[0]).not.toContain('"name"') expect(sql).not.toMatch(/grant [^;]*insert/) expect(sql).not.toMatch(/grant [^;]*delete/) }) it("give the reader a policy of its own, because row-level security binds it too", () => { // The reader owns nothing and has NOBYPASSRLS (data-services DS21), so without a policy // naming it the forced policy on app.org_id would answer zero rows for every read. for (const table of tables) { expect(sql).toContain( `create policy "${table}_global_reader" on "${table}" for select to "app_reader" using (true)`, ) } expect(sql).toContain('create policy "outbox_global_reader_update"') }) it("name the role from the reader's own URL, quoted", () => { expect(readerGrants("postgresql://other_reader:s@db:5432/app").statements[0]).toContain('"other_reader"') }) it("grant the runtime role the job schema a transactional enqueue needs", () => { const jobs = jobSchemaGrants("postgresql://app_app:secret@db:5432/app").statements.join(";\n") expect(jobs).toContain('grant usage on schema "pgboss" to "app_app"') expect(jobs).toContain('alter default privileges in schema "pgboss"') expect(jobs).not.toContain("app_reader") }) })