125 lines
4.2 KiB
JavaScript
125 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import pg from "pg";
|
|
|
|
export const MIGRATOR_DATABASE_URL_ENV = "DIRECT_URL";
|
|
export const REQUIRED_MIGRATOR_ROLE = "hivegrid_migrator";
|
|
|
|
export class MigratorRoleVerificationError extends Error {
|
|
constructor(message, options) {
|
|
super(message, options);
|
|
this.name = "MigratorRoleVerificationError";
|
|
}
|
|
}
|
|
|
|
const SESSION_ROLE_QUERY = `
|
|
SELECT session_user AS "sessionRole", current_user AS "currentRole";
|
|
`;
|
|
|
|
const ROLE_STATE_QUERY = `
|
|
SELECT
|
|
rolname AS "roleName",
|
|
rolcanlogin AS "canLogin",
|
|
rolsuper AS "isSuperuser",
|
|
rolcreatedb AS "canCreateDb",
|
|
rolcreaterole AS "canCreateRole",
|
|
rolinherit AS "inherits",
|
|
rolreplication AS "replicates",
|
|
rolbypassrls AS "bypassesRls"
|
|
FROM pg_roles
|
|
WHERE rolname = $1;
|
|
`;
|
|
|
|
const MEMBERSHIP_STATE_QUERY = `
|
|
SELECT
|
|
granted.rolname AS "grantedRole",
|
|
member.rolname AS "memberRole",
|
|
membership.admin_option AS "adminOption",
|
|
membership.inherit_option AS "inheritOption",
|
|
membership.set_option AS "setOption"
|
|
FROM pg_auth_members AS membership
|
|
JOIN pg_roles AS granted ON granted.oid = membership.roleid
|
|
JOIN pg_roles AS member ON member.oid = membership.member
|
|
WHERE granted.rolname = $1 OR member.rolname = $1
|
|
ORDER BY granted.rolname, member.rolname;
|
|
`;
|
|
|
|
export function verifyMigratorRoleState({ session, roles, memberships }) {
|
|
if (!session || session.sessionRole !== REQUIRED_MIGRATOR_ROLE || session.currentRole !== REQUIRED_MIGRATOR_ROLE) {
|
|
throw new MigratorRoleVerificationError(`${MIGRATOR_DATABASE_URL_ENV} must authenticate directly as ${REQUIRED_MIGRATOR_ROLE}`);
|
|
}
|
|
|
|
if (
|
|
roles.length !== 1 ||
|
|
roles[0].roleName !== REQUIRED_MIGRATOR_ROLE ||
|
|
roles[0].canLogin !== true ||
|
|
roles[0].isSuperuser !== false ||
|
|
roles[0].canCreateDb !== false ||
|
|
roles[0].canCreateRole !== false ||
|
|
roles[0].inherits !== false ||
|
|
roles[0].replicates !== false ||
|
|
roles[0].bypassesRls !== false
|
|
) {
|
|
throw new MigratorRoleVerificationError(`${REQUIRED_MIGRATOR_ROLE} does not match the required non-superuser security contract`);
|
|
}
|
|
|
|
if (
|
|
memberships.length !== 1 ||
|
|
memberships[0].grantedRole !== "hivegrid_rls_definer" ||
|
|
memberships[0].memberRole !== REQUIRED_MIGRATOR_ROLE ||
|
|
memberships[0].adminOption !== false ||
|
|
memberships[0].inheritOption !== false ||
|
|
memberships[0].setOption !== true
|
|
) {
|
|
throw new MigratorRoleVerificationError(`${REQUIRED_MIGRATOR_ROLE} has an unexpected PostgreSQL role membership`);
|
|
}
|
|
}
|
|
|
|
export async function verifyMigratorRole({
|
|
env = process.env,
|
|
clientFactory = (connectionString) => new pg.Client({ connectionString, application_name: "hivegrid-migrator-role-verifier" }),
|
|
stdout = process.stdout,
|
|
} = {}) {
|
|
const connectionString = env[MIGRATOR_DATABASE_URL_ENV]?.trim();
|
|
if (!connectionString) {
|
|
throw new MigratorRoleVerificationError(`${MIGRATOR_DATABASE_URL_ENV} is required for migrator role verification`);
|
|
}
|
|
|
|
const client = clientFactory(connectionString);
|
|
try {
|
|
await client.connect();
|
|
const sessionState = await client.query(SESSION_ROLE_QUERY);
|
|
const roleState = await client.query(ROLE_STATE_QUERY, [REQUIRED_MIGRATOR_ROLE]);
|
|
const membershipState = await client.query(MEMBERSHIP_STATE_QUERY, [REQUIRED_MIGRATOR_ROLE]);
|
|
verifyMigratorRoleState({
|
|
session: sessionState.rows[0],
|
|
roles: roleState.rows,
|
|
memberships: membershipState.rows,
|
|
});
|
|
stdout.write("PASS HiveGrid migrator role verified\n");
|
|
} catch (error) {
|
|
if (error instanceof MigratorRoleVerificationError) {
|
|
throw error;
|
|
}
|
|
throw new MigratorRoleVerificationError("PostgreSQL migrator role verification failed; inspect restricted database logs without copying credentials", {
|
|
cause: error,
|
|
});
|
|
} finally {
|
|
await client.end().catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
|
|
if (isCli) {
|
|
try {
|
|
await verifyMigratorRole();
|
|
} catch (error) {
|
|
const message = error instanceof MigratorRoleVerificationError ? error.message : "PostgreSQL migrator role verification failed without a safe diagnostic";
|
|
process.stderr.write(`FAIL ${message}\n`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|