Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions charts/d2e-services/templates/dataflow-worker-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,34 @@ spec:
secretKeyRef:
name: "{{ .Release.Name }}-{{ .Chart.Name }}"
key: POSTGRES_CONFIG_DB
# supabase-storage connects as POSTGRES_SUPERUSER (see
# STORAGE__JDBC__URL), so on a fresh install it owns storage.objects
# and only that role can disable RLS on it. Connecting as the config
# admin user failed with "must be owner of table objects" on every
# greenfield deploy; it worked on older installs only because their
# storage tables happened to have been created by the admin user.
- name: PGUSER
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-{{ .Chart.Name }}"
key: POSTGRES_CONFIG_ADMIN_USER
key: POSTGRES_SUPERUSER
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-{{ .Chart.Name }}"
key: POSTGRES_CONFIG_ADMIN_PASSWORD
key: POSTGRES_SUPERUSER_PASSWORD
- name: POSTGRES_CONFIG_ADMIN_USER
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-{{ .Chart.Name }}"
key: POSTGRES_CONFIG_ADMIN_USER
command: ["/bin/sh"]
args:
[
"-c",
"echo 'Updating public.objects view to include user_metadata column...' &&
{ psql -c \"GRANT ${POSTGRES_CONFIG_ADMIN_USER} TO CURRENT_USER;\" ||
echo 'Could not join the config admin role - relying on existing membership'; } &&
psql -c 'ALTER TABLE IF EXISTS storage.objects DISABLE ROW LEVEL SECURITY;' &&
psql -c 'DROP VIEW IF EXISTS public.objects CASCADE;' &&
psql -c 'CREATE VIEW public.objects AS SELECT * FROM storage.objects;' &&
Expand Down
41 changes: 40 additions & 1 deletion services/trex/provision/d2e-bootstrap/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,46 @@ Deno.test("creates the three supabase roles with the documented attributes", ()
const stmts = buildBootstrapStatements(CFG).join("\n");
assertEquals(stmts.includes("CREATE ROLE anon NOLOGIN INHERIT"), true);
assertEquals(stmts.includes("CREATE ROLE authenticated NOLOGIN INHERIT"), true);
assertEquals(stmts.includes("CREATE ROLE service_role NOLOGIN INHERIT BYPASSRLS"), true);
// Without BYPASSRLS: it requires superuser, which managed Postgres does not
// grant, so requesting it leaves service_role uncreated.
assertEquals(stmts.includes("CREATE ROLE service_role NOLOGIN INHERIT"), true);
assertEquals(stmts.includes("BYPASSRLS"), false);
});

Deno.test("creates supabase_admin without REPLICATION so trex's V1 can be applied", () => {
const stmts = buildBootstrapStatements(CFG).join("\n");
// V1__initial_schema requests REPLICATION, which is superuser-only on managed
// Postgres; pre-creating the role makes V1's IF NOT EXISTS guard skip it.
assertEquals(stmts.includes("CREATE ROLE supabase_admin NOLOGIN"), true);
assertEquals(stmts.includes("REPLICATION"), false);
// V1 also creates the _realtime schema AUTHORIZATION supabase_admin, which
// needs membership -- for the manager and for the superuser running V1.
assertEquals(stmts.includes("GRANT supabase_admin TO CURRENT_USER"), true);
assertEquals(stmts.includes('GRANT supabase_admin TO "alp_pg_admin_user"'), true);
});

Deno.test("grants CREATE on schema public to the roles that create objects there", () => {
const stmts = buildBootstrapStatements(CFG);
const joined = stmts.join("\n");
// logto's roles.sql creates public.check_role_type, hardcoded to public.
assertEquals(
stmts.includes('GRANT USAGE, CREATE ON SCHEMA public TO "logto_postgres"'),
true,
);
assertEquals(
stmts.includes('GRANT USAGE, CREATE ON SCHEMA public TO "alp_pg_admin_user"'),
true,
);
// Readers of public.objects need schema USAGE, not CREATE.
assertEquals(stmts.includes("GRANT USAGE ON SCHEMA public TO service_role"), true);
assertEquals(
stmts.includes('GRANT USAGE ON SCHEMA public TO "alp_pg_write_user"'),
true,
);
// The grant is a silent no-op unless the bootstrap user owns public, so the
// result has to be checked rather than assumed.
assertStringIncludes(joined, "has_schema_privilege('logto_postgres', 'public', 'CREATE')");
assertStringIncludes(joined, "RAISE WARNING");
});

Deno.test("grants per-schema privileges and default privileges to reader and writer", () => {
Expand Down
58 changes: 57 additions & 1 deletion services/trex/provision/d2e-bootstrap/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,29 @@ export function buildBootstrapStatements(cfg: BootstrapConfig): string[] {
// ── Supabase roles (PostGraphile connects as authenticator and SET ROLEs) ──
out.push(createGroupRole("anon", "NOLOGIN INHERIT"));
out.push(createGroupRole("authenticated", "NOLOGIN INHERIT"));
out.push(createGroupRole("service_role", "NOLOGIN INHERIT BYPASSRLS"));
// No BYPASSRLS: setting that attribute requires superuser, which managed
// Postgres (Azure Flexible Server included) never grants -- even to a role
// that already holds it. Requesting it fails the statement outright with
// "must be superuser to change bypassrls attribute", leaving service_role
// absent on every greenfield install. Reachability of storage.buckets is
// provided by the service_role buckets policy migration instead.
out.push(createGroupRole("service_role", "NOLOGIN INHERIT"));
// trex's V1__initial_schema creates supabase_admin WITH ... REPLICATION, which
// is superuser-only on managed Postgres, so V1 aborts and the whole trexdb
// schema is never created. V1 is checksum-verified and already applied in
// existing deployments, so it cannot be edited; pre-creating the role here
// makes V1's own IF NOT EXISTS guard skip the failing statement. No
// REPLICATION: V5__drop_realtime_admin drops this role and the _realtime
// schema a few migrations later, so nothing ever replicates as it.
out.push(createGroupRole("supabase_admin", "NOLOGIN"));
// Postgres 15 does not give a CREATEROLE creator membership in the role it
// just created, and bootstrap runs as the superuser that also runs V1.
out.push(`GRANT supabase_admin TO CURRENT_USER`);
// The storage post-init grants these roles access to public.objects, which is
// only usable with USAGE on the schema itself.
for (const role of ["anon", "authenticated", "service_role"]) {
out.push(`GRANT USAGE ON SCHEMA public TO ${role}`);
}

for (const dbKey of Object.keys(cfg.manageConfig.databases)) {
if (!dbKey.startsWith("+")) continue; // only creation scenarios
Expand All @@ -184,9 +206,43 @@ export function buildBootstrapStatements(cfg: BootstrapConfig): string[] {

// ── Role membership: manager gets service_role, reader anon, writer authenticated ──
if (users.manager) out.push(`GRANT service_role TO ${quoteIdent(users.manager)}`);
// V1 creates the _realtime schema AUTHORIZATION supabase_admin, which needs
// membership in that role rather than mere CREATEROLE.
if (users.manager) out.push(`GRANT supabase_admin TO ${quoteIdent(users.manager)}`);
if (users.reader) out.push(`GRANT anon TO ${quoteIdent(users.reader)}`);
if (users.writer) out.push(`GRANT authenticated TO ${quoteIdent(users.writer)}`);

// ── CREATE on schema public ──────────────────────────────────────────────
// Postgres 15 stopped granting CREATE on public to PUBLIC. logto's
// roles.sql creates public.check_role_type -- hardcoded to public, not to
// its own schema -- so on a greenfield database logto's seed dies with
// "permission denied for schema public".
//
// public is owned by the platform admin role (azure_pg_admin on Azure), so
// these only take effect when the bootstrap superuser is a member of it. A
// non-member gets "WARNING: no privileges were granted for public" and
// Postgres still reports success, hence the explicit check below: the
// failure otherwise surfaces much later as an unrelated error.
const publicCreators = [users.manager, users.logtoManager].filter(
(u): u is string => !!u,
);
for (const user of publicCreators) {
out.push(`GRANT USAGE, CREATE ON SCHEMA public TO ${quoteIdent(user)}`);
}
if (users.writer) out.push(`GRANT USAGE ON SCHEMA public TO ${quoteIdent(users.writer)}`);
for (const user of publicCreators) {
out.push(
doBlock(
`BEGIN IF NOT has_schema_privilege(${quoteLiteral(user)}, 'public', 'CREATE') THEN ` +
`RAISE WARNING ${quoteLiteral(
`no CREATE on schema public for ${user}; schema public is owned by the platform ` +
"admin role, so the bootstrap user must be a member of it (on Azure: GRANT " +
"azure_pg_admin TO <superuser>). logto seeding will fail without it.",
)}; END IF; END`,
),
);
}

// ── Database-level CREATE ────────────────────────────────────────────────
// `CREATE SCHEMA IF NOT EXISTS` checks CREATE on the database before it
// checks whether the schema exists, so a service that opens with that
Expand Down
Loading