Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/verify-migrator-role.mjs
T

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;
}
}