Skip to content
Merged
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
123 changes: 90 additions & 33 deletions scripts/data-pipeline/candidate-restore.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1587,14 +1587,86 @@ function partitionClosure(closure, label) {
});
}

async function assertRestoreRolePosture(sql) {
export function assertRestoreRolePostureEvidence(
identity,
roles,
memberships,
{ supabaseHosted = true } = {},
) {
const postgresRole = roles.find(({ rolname }) => rolname === "postgres");
const migrator = roles.find(({ rolname }) => rolname === "programmable_migrator");
const operatorMemberships = memberships.filter(
({
member_role: memberRole,
granted_role: grantedRole,
grantor_role: grantorRole,
inherit_option: inheritOption,
set_option: setOption,
admin_option: adminOption,
}) =>
memberRole === "postgres" &&
grantedRole === "programmable_migrator" &&
grantorRole === "postgres" &&
inheritOption === false &&
setOption === true &&
adminOption === false,
);
const supabaseMemberships = memberships.filter(
({
member_role: memberRole,
granted_role: grantedRole,
grantor_role: grantorRole,
inherit_option: inheritOption,
set_option: setOption,
admin_option: adminOption,
}) =>
memberRole === "postgres" &&
grantedRole === "programmable_migrator" &&
grantorRole === "supabase_admin" &&
inheritOption === false &&
setOption === false &&
adminOption === true,
);
if (
!["postgres", "cli_login_postgres"].includes(identity?.session_user) ||
identity?.current_user !== "postgres" ||
identity?.current_role !== "postgres" ||
identity?.can_set_migrator !== true ||
typeof identity?.supabase_admin_exists !== "boolean" ||
typeof supabaseHosted !== "boolean" ||
identity.supabase_admin_exists !== supabaseHosted ||
roles.length !== 2 ||
postgresRole?.rolsuper !== false ||
migrator?.rolsuper !== false ||
migrator?.rolinherit !== false ||
migrator?.rolcreaterole !== false ||
migrator?.rolcreatedb !== false ||
migrator?.rolcanlogin !== false ||
migrator?.rolreplication !== false ||
migrator?.rolbypassrls !== false ||
operatorMemberships.length !== 1 ||
supabaseMemberships.length !==
(supabaseHosted ? 1 : 0) ||
memberships.length !==
operatorMemberships.length + supabaseMemberships.length
) {
throw new Error("Candidate restore role posture is not exact");
}
}

async function assertRestoreRolePosture(sql, posture) {
const [identity] = await sql.unsafe(`
select session_user::text as session_user,
current_user::text as current_user,
current_role::text as current_role,
pg_catalog.pg_has_role(
current_user, 'programmable_migrator', 'SET'
) as can_set_migrator
) as can_set_migrator,
exists (
select 1
from pg_catalog.pg_roles
where rolname = 'supabase_admin'
) as supabase_admin_exists
`);
const roles = await sql.unsafe(`
select rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb,
Expand All @@ -1606,6 +1678,7 @@ async function assertRestoreRolePosture(sql) {
const memberships = await sql.unsafe(`
select member_role.rolname as member_role,
granted_role.rolname as granted_role,
grantor_role.rolname as grantor_role,
membership.inherit_option,
membership.set_option,
membership.admin_option
Expand All @@ -1614,34 +1687,13 @@ async function assertRestoreRolePosture(sql) {
on member_role.oid = membership.member
join pg_catalog.pg_roles as granted_role
on granted_role.oid = membership.roleid
join pg_catalog.pg_roles as grantor_role
on grantor_role.oid = membership.grantor
where member_role.rolname = 'postgres'
and granted_role.rolname = 'programmable_migrator'
order by grantor_role.rolname
`);
const postgresRole = roles.find(({ rolname }) => rolname === "postgres");
const migrator = roles.find(({ rolname }) => rolname === "programmable_migrator");
if (
!["postgres", "cli_login_postgres"].includes(identity?.session_user) ||
identity?.current_user !== "postgres" ||
identity?.current_role !== "postgres" ||
identity?.can_set_migrator !== true ||
roles.length !== 2 ||
postgresRole?.rolsuper !== false ||
migrator?.rolsuper !== false ||
migrator?.rolinherit !== false ||
migrator?.rolcreaterole !== false ||
migrator?.rolcreatedb !== false ||
migrator?.rolcanlogin !== false ||
migrator?.rolreplication !== false ||
migrator?.rolbypassrls !== false ||
memberships.length !== 1 ||
memberships[0]?.member_role !== "postgres" ||
memberships[0]?.granted_role !== "programmable_migrator" ||
memberships[0]?.inherit_option !== false ||
memberships[0]?.set_option !== true ||
memberships[0]?.admin_option !== false
) {
throw new Error("Candidate restore role posture is not exact");
}
assertRestoreRolePostureEvidence(identity, roles, memberships, posture);
}

async function assertRestoreSchemasAbsent(sql) {
Expand Down Expand Up @@ -1686,9 +1738,9 @@ export async function assertCandidateSchemaStage(sql, expectedSchemas) {
}
}

export async function cleanupCandidateSchemas(sql, closure) {
export async function cleanupCandidateSchemas(sql, closure, posture) {
const statements = partitionClosure(closure, "Candidate cleanup closure");
await assertRestoreRolePosture(sql);
await assertRestoreRolePosture(sql, posture);
await sql.begin(async (transaction) => {
await transaction.unsafe("set local role programmable_migrator").simple();
await transaction.unsafe(LATER_ONLY_RESTRICT_CLEANUP_SQL).simple();
Expand All @@ -1705,18 +1757,23 @@ export async function cleanupCandidateSchemas(sql, closure) {
}
});
await assertRestoreSchemasAbsent(sql);
await assertRestoreRolePosture(sql);
await assertRestoreRolePosture(sql, posture);
}

export async function applyOwnerAndSecurityClosure(sql, owners, security) {
export async function applyOwnerAndSecurityClosure(
sql,
owners,
security,
posture,
) {
const ownerLines = owners.sql.trimEnd().split("\n");
const objectOwners = ownerLines.filter((line) => !line.startsWith("ALTER SCHEMA "));
const schemaOwners = ownerLines.filter((line) => line.startsWith("ALTER SCHEMA "));
if (schemaOwners.length !== CANDIDATE_RESTORE_SCHEMAS.length) {
throw new Error("Candidate schema owner closure is incomplete");
}
const acl = partitionClosure(security, "Candidate security closure");
await assertRestoreRolePosture(sql);
await assertRestoreRolePosture(sql, posture);
await sql.begin(async (transaction) => {
await transaction.unsafe(`
grant create on schema programmable_private,
Expand All @@ -1729,7 +1786,7 @@ export async function applyOwnerAndSecurityClosure(sql, owners, security) {
await transaction.unsafe("set local role postgres").simple();
if (acl.postgresOwned) await transaction.unsafe(acl.postgresOwned).simple();
});
await assertRestoreRolePosture(sql);
await assertRestoreRolePosture(sql, posture);
}

export async function preparePinnedRestoreClosures({
Expand Down
5 changes: 4 additions & 1 deletion scripts/data-pipeline/candidate-restore.pg17.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,9 @@ test(
environment: Object.freeze({ LANG: "C", LC_ALL: "C" }),
secrets: [],
});
await cleanupCandidateSchemas(sql, closures.cleanup);
await cleanupCandidateSchemas(sql, closures.cleanup, {
supabaseHosted: false,
});
await runTool(config.pgRestore, [
...CANDIDATE_SAFETY_RECOVERY_FLAGS,
"--host",
Expand All @@ -294,6 +296,7 @@ test(
sql,
closures.owners,
closures.security,
{ supabaseHosted: false },
);
await assertCandidateSchemaStage(sql, CANDIDATE_RESTORE_SCHEMAS);
const recovered = await captureDatabaseManifest(sql);
Expand Down
109 changes: 109 additions & 0 deletions scripts/data-pipeline/candidate-restore.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
applyCandidateRestore,
applyCandidateRuntimeEnable,
applyCandidateSafetyRecovery,
assertRestoreRolePostureEvidence,
buildCandidateSafetyBackupEvidence,
createCandidateRestorePlan,
createCandidateRuntimeEnablePlan,
Expand Down Expand Up @@ -69,6 +70,114 @@ const SAFETY_MANIFEST = `0x${"e".repeat(64)}`;
const SAFETY_STRUCTURAL_MANIFEST = `0x${"f".repeat(64)}`;
const SAFETY_PORTABLE_STRUCTURAL_MANIFEST = `0x${"d".repeat(64)}`;

const RESTORE_ROLE_IDENTITY = Object.freeze({
session_user: "cli_login_postgres",
current_user: "postgres",
current_role: "postgres",
can_set_migrator: true,
supabase_admin_exists: true,
});
const RESTORE_ROLES = Object.freeze([
Object.freeze({ rolname: "postgres", rolsuper: false }),
Object.freeze({
rolname: "programmable_migrator",
rolsuper: false,
rolinherit: false,
rolcreaterole: false,
rolcreatedb: false,
rolcanlogin: false,
rolreplication: false,
rolbypassrls: false,
}),
]);
const RESTORE_OPERATOR_MEMBERSHIP = Object.freeze({
member_role: "postgres",
granted_role: "programmable_migrator",
grantor_role: "postgres",
inherit_option: false,
set_option: true,
admin_option: false,
});
const RESTORE_SUPABASE_MEMBERSHIP = Object.freeze({
member_role: "postgres",
granted_role: "programmable_migrator",
grantor_role: "supabase_admin",
inherit_option: false,
set_option: false,
admin_option: true,
});

test("restore posture accepts the exact Supabase admin and operator grants", () => {
assert.doesNotThrow(() =>
assertRestoreRolePostureEvidence(
RESTORE_ROLE_IDENTITY,
RESTORE_ROLES,
[RESTORE_SUPABASE_MEMBERSHIP, RESTORE_OPERATOR_MEMBERSHIP],
{ supabaseHosted: true },
),
);
});

test("restore posture accepts the exact grants for a hosted postgres login", () => {
assert.doesNotThrow(() =>
assertRestoreRolePostureEvidence(
{ ...RESTORE_ROLE_IDENTITY, session_user: "postgres" },
RESTORE_ROLES,
[RESTORE_SUPABASE_MEMBERSHIP, RESTORE_OPERATOR_MEMBERSHIP],
{ supabaseHosted: true },
),
);
});

test("restore posture accepts an isolated postgres operator grant", () => {
assert.doesNotThrow(() =>
assertRestoreRolePostureEvidence(
{
...RESTORE_ROLE_IDENTITY,
session_user: "postgres",
supabase_admin_exists: false,
},
RESTORE_ROLES,
[RESTORE_OPERATOR_MEMBERSHIP],
{ supabaseHosted: false },
),
);
});

test("restore posture rejects unknown or duplicated memberships", () => {
const invalid = Object.freeze({
...RESTORE_SUPABASE_MEMBERSHIP,
grantor_role: "unknown_admin",
});
for (const memberships of [
[RESTORE_SUPABASE_MEMBERSHIP],
[RESTORE_OPERATOR_MEMBERSHIP],
[RESTORE_OPERATOR_MEMBERSHIP, RESTORE_OPERATOR_MEMBERSHIP],
[RESTORE_OPERATOR_MEMBERSHIP, invalid],
]) {
assert.throws(
() =>
assertRestoreRolePostureEvidence(
RESTORE_ROLE_IDENTITY,
RESTORE_ROLES,
memberships,
{ supabaseHosted: true },
),
/Candidate restore role posture is not exact/u,
);
}
assert.throws(
() =>
assertRestoreRolePostureEvidence(
{ ...RESTORE_ROLE_IDENTITY, supabase_admin_exists: false },
RESTORE_ROLES,
[RESTORE_SUPABASE_MEMBERSHIP, RESTORE_OPERATOR_MEMBERSHIP],
{ supabaseHosted: false },
),
/Candidate restore role posture is not exact/u,
);
});

const PINNED_SNAPSHOT_EVIDENCE = Object.freeze({
kind: "programmable-database-backup-restore-evidence",
schemaVersion: 1,
Expand Down