500 lines
18 KiB
JavaScript
500 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import pg from "pg";
|
|
|
|
export const PROVISION_DATABASE_URL_ENV = "HIVEGRID_RLS_PROVISION_DATABASE_URL";
|
|
export const ROLE_PASSWORD_ENV = Object.freeze({
|
|
hivegrid_migrator: "HIVEGRID_RLS_MIGRATOR_PASSWORD",
|
|
hivegrid_app: "HIVEGRID_RLS_APP_PASSWORD",
|
|
hivegrid_portal: "HIVEGRID_RLS_PORTAL_PASSWORD",
|
|
hivegrid_worker: "HIVEGRID_RLS_WORKER_PASSWORD",
|
|
hivegrid_authentik_jit: "HIVEGRID_RLS_AUTHENTIK_JIT_PASSWORD",
|
|
});
|
|
|
|
export const MANAGED_ROLES = Object.freeze([
|
|
"hivegrid_migrator",
|
|
"hivegrid_app",
|
|
"hivegrid_portal",
|
|
"hivegrid_worker",
|
|
"hivegrid_authentik_jit",
|
|
"hivegrid_rls_definer",
|
|
]);
|
|
|
|
const MINIMUM_PASSWORD_LENGTH = 32;
|
|
|
|
const EXPECTED_ROLE_ATTRIBUTES = Object.freeze({
|
|
hivegrid_migrator: {
|
|
canLogin: true,
|
|
isSuperuser: false,
|
|
canCreateDb: false,
|
|
canCreateRole: false,
|
|
inherits: false,
|
|
replicates: false,
|
|
bypassesRls: false,
|
|
passwordStateValid: true,
|
|
},
|
|
hivegrid_app: {
|
|
canLogin: true,
|
|
isSuperuser: false,
|
|
canCreateDb: false,
|
|
canCreateRole: false,
|
|
inherits: false,
|
|
replicates: false,
|
|
bypassesRls: false,
|
|
passwordStateValid: true,
|
|
},
|
|
hivegrid_portal: {
|
|
canLogin: true,
|
|
isSuperuser: false,
|
|
canCreateDb: false,
|
|
canCreateRole: false,
|
|
inherits: false,
|
|
replicates: false,
|
|
bypassesRls: false,
|
|
passwordStateValid: true,
|
|
},
|
|
hivegrid_worker: {
|
|
canLogin: true,
|
|
isSuperuser: false,
|
|
canCreateDb: false,
|
|
canCreateRole: false,
|
|
inherits: false,
|
|
replicates: false,
|
|
bypassesRls: false,
|
|
passwordStateValid: true,
|
|
},
|
|
hivegrid_authentik_jit: {
|
|
canLogin: true,
|
|
isSuperuser: false,
|
|
canCreateDb: false,
|
|
canCreateRole: false,
|
|
inherits: false,
|
|
replicates: false,
|
|
bypassesRls: false,
|
|
passwordStateValid: true,
|
|
},
|
|
hivegrid_rls_definer: {
|
|
canLogin: false,
|
|
isSuperuser: false,
|
|
canCreateDb: false,
|
|
canCreateRole: false,
|
|
inherits: false,
|
|
replicates: false,
|
|
bypassesRls: true,
|
|
passwordStateValid: true,
|
|
},
|
|
});
|
|
|
|
export const ROLE_PROVISIONING_SQL = String.raw`
|
|
DO $hivegrid_role_provisioning$
|
|
DECLARE
|
|
membership_edge record;
|
|
relation_object record;
|
|
routine_object record;
|
|
type_object record;
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hivegrid_migrator') THEN
|
|
CREATE ROLE hivegrid_migrator;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hivegrid_app') THEN
|
|
CREATE ROLE hivegrid_app;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hivegrid_portal') THEN
|
|
CREATE ROLE hivegrid_portal;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hivegrid_worker') THEN
|
|
CREATE ROLE hivegrid_worker;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hivegrid_authentik_jit') THEN
|
|
CREATE ROLE hivegrid_authentik_jit;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hivegrid_rls_definer') THEN
|
|
CREATE ROLE hivegrid_rls_definer;
|
|
END IF;
|
|
|
|
ALTER ROLE hivegrid_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS VALID UNTIL 'infinity';
|
|
ALTER ROLE hivegrid_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS VALID UNTIL 'infinity';
|
|
ALTER ROLE hivegrid_portal LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS VALID UNTIL 'infinity';
|
|
ALTER ROLE hivegrid_worker LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS VALID UNTIL 'infinity';
|
|
ALTER ROLE hivegrid_authentik_jit LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS VALID UNTIL 'infinity';
|
|
ALTER ROLE hivegrid_rls_definer NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION BYPASSRLS PASSWORD NULL;
|
|
|
|
EXECUTE format(
|
|
'ALTER ROLE hivegrid_migrator PASSWORD %L',
|
|
current_setting('hivegrid.provision.migrator_password')
|
|
);
|
|
EXECUTE format(
|
|
'ALTER ROLE hivegrid_app PASSWORD %L',
|
|
current_setting('hivegrid.provision.app_password')
|
|
);
|
|
EXECUTE format(
|
|
'ALTER ROLE hivegrid_portal PASSWORD %L',
|
|
current_setting('hivegrid.provision.portal_password')
|
|
);
|
|
EXECUTE format(
|
|
'ALTER ROLE hivegrid_worker PASSWORD %L',
|
|
current_setting('hivegrid.provision.worker_password')
|
|
);
|
|
EXECUTE format(
|
|
'ALTER ROLE hivegrid_authentik_jit PASSWORD %L',
|
|
current_setting('hivegrid.provision.authentik_jit_password')
|
|
);
|
|
|
|
-- Remove every membership edge involving a managed role before restoring
|
|
-- the single reviewed edge. This also removes external SET ROLE access to a
|
|
-- runtime or definer role.
|
|
FOR membership_edge IN
|
|
SELECT granted.rolname AS granted_role, member.rolname AS member_role
|
|
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 = ANY (ARRAY['hivegrid_migrator', 'hivegrid_app', 'hivegrid_portal', 'hivegrid_worker', 'hivegrid_authentik_jit', 'hivegrid_rls_definer'])
|
|
OR member.rolname = ANY (ARRAY['hivegrid_migrator', 'hivegrid_app', 'hivegrid_portal', 'hivegrid_worker', 'hivegrid_authentik_jit', 'hivegrid_rls_definer'])
|
|
LOOP
|
|
EXECUTE format('REVOKE %I FROM %I', membership_edge.granted_role, membership_edge.member_role);
|
|
END LOOP;
|
|
|
|
GRANT hivegrid_rls_definer TO hivegrid_migrator
|
|
WITH ADMIN FALSE, INHERIT FALSE, SET TRUE;
|
|
|
|
-- The migration principal owns the database namespace and all non-extension
|
|
-- application objects. Extension-owned objects must retain their extension
|
|
-- owner so upgrades remain valid.
|
|
EXECUTE format('ALTER DATABASE %I OWNER TO hivegrid_migrator', current_database());
|
|
ALTER SCHEMA public OWNER TO hivegrid_migrator;
|
|
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
|
GRANT USAGE, CREATE ON SCHEMA public TO hivegrid_migrator;
|
|
|
|
FOR relation_object IN
|
|
SELECT namespace.nspname AS schema_name, relation.relname AS object_name, relation.relkind
|
|
FROM pg_class AS relation
|
|
JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace
|
|
WHERE namespace.nspname = 'public'
|
|
AND relation.relkind IN ('r', 'p', 'S', 'v', 'm', 'f')
|
|
AND relation.relowner NOT IN (
|
|
SELECT oid FROM pg_roles WHERE rolname IN ('hivegrid_migrator', 'hivegrid_rls_definer')
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_depend AS dependency
|
|
WHERE dependency.classid = 'pg_class'::regclass
|
|
AND dependency.objid = relation.oid
|
|
AND dependency.deptype = 'e'
|
|
)
|
|
ORDER BY CASE relation.relkind WHEN 'r' THEN 0 WHEN 'p' THEN 0 ELSE 1 END, relation.oid
|
|
LOOP
|
|
EXECUTE format(
|
|
CASE relation_object.relkind
|
|
WHEN 'S' THEN 'ALTER SEQUENCE %I.%I OWNER TO hivegrid_migrator'
|
|
WHEN 'v' THEN 'ALTER VIEW %I.%I OWNER TO hivegrid_migrator'
|
|
WHEN 'm' THEN 'ALTER MATERIALIZED VIEW %I.%I OWNER TO hivegrid_migrator'
|
|
WHEN 'f' THEN 'ALTER FOREIGN TABLE %I.%I OWNER TO hivegrid_migrator'
|
|
ELSE 'ALTER TABLE %I.%I OWNER TO hivegrid_migrator'
|
|
END,
|
|
relation_object.schema_name,
|
|
relation_object.object_name
|
|
);
|
|
END LOOP;
|
|
|
|
FOR routine_object IN
|
|
SELECT
|
|
namespace.nspname AS schema_name,
|
|
routine.proname AS object_name,
|
|
routine.prokind,
|
|
pg_get_function_identity_arguments(routine.oid) AS identity_arguments
|
|
FROM pg_proc AS routine
|
|
JOIN pg_namespace AS namespace ON namespace.oid = routine.pronamespace
|
|
WHERE namespace.nspname = 'public'
|
|
AND routine.proowner NOT IN (
|
|
SELECT oid FROM pg_roles WHERE rolname IN ('hivegrid_migrator', 'hivegrid_rls_definer')
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_depend AS dependency
|
|
WHERE dependency.classid = 'pg_proc'::regclass
|
|
AND dependency.objid = routine.oid
|
|
AND dependency.deptype = 'e'
|
|
)
|
|
LOOP
|
|
EXECUTE format(
|
|
CASE routine_object.prokind
|
|
WHEN 'p' THEN 'ALTER PROCEDURE %I.%I(%s) OWNER TO hivegrid_migrator'
|
|
WHEN 'a' THEN 'ALTER AGGREGATE %I.%I(%s) OWNER TO hivegrid_migrator'
|
|
ELSE 'ALTER FUNCTION %I.%I(%s) OWNER TO hivegrid_migrator'
|
|
END,
|
|
routine_object.schema_name,
|
|
routine_object.object_name,
|
|
routine_object.identity_arguments
|
|
);
|
|
END LOOP;
|
|
|
|
FOR type_object IN
|
|
SELECT namespace.nspname AS schema_name, type_definition.typname AS object_name, type_definition.typtype
|
|
FROM pg_type AS type_definition
|
|
JOIN pg_namespace AS namespace ON namespace.oid = type_definition.typnamespace
|
|
WHERE namespace.nspname = 'public'
|
|
AND type_definition.typtype IN ('c', 'd', 'e')
|
|
AND type_definition.typrelid = 0
|
|
AND type_definition.typowner NOT IN (
|
|
SELECT oid FROM pg_roles WHERE rolname IN ('hivegrid_migrator', 'hivegrid_rls_definer')
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_depend AS dependency
|
|
WHERE dependency.classid = 'pg_type'::regclass
|
|
AND dependency.objid = type_definition.oid
|
|
AND dependency.deptype = 'e'
|
|
)
|
|
LOOP
|
|
EXECUTE format(
|
|
CASE type_object.typtype
|
|
WHEN 'd' THEN 'ALTER DOMAIN %I.%I OWNER TO hivegrid_migrator'
|
|
ELSE 'ALTER TYPE %I.%I OWNER TO hivegrid_migrator'
|
|
END,
|
|
type_object.schema_name,
|
|
type_object.object_name
|
|
);
|
|
END LOOP;
|
|
|
|
EXECUTE format('REVOKE CONNECT, CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC', current_database());
|
|
EXECUTE format('REVOKE ALL PRIVILEGES ON DATABASE %I FROM hivegrid_app, hivegrid_portal, hivegrid_worker, hivegrid_authentik_jit, hivegrid_rls_definer', current_database());
|
|
EXECUTE format('GRANT CONNECT, TEMPORARY ON DATABASE %I TO hivegrid_migrator', current_database());
|
|
EXECUTE format('GRANT CONNECT ON DATABASE %I TO hivegrid_app, hivegrid_portal, hivegrid_worker, hivegrid_authentik_jit', current_database());
|
|
END
|
|
$hivegrid_role_provisioning$;
|
|
`;
|
|
|
|
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",
|
|
CASE
|
|
WHEN rolname = 'hivegrid_rls_definer' THEN rolpassword IS NULL
|
|
ELSE rolpassword LIKE 'SCRAM-SHA-256$%'
|
|
END AS "passwordStateValid"
|
|
FROM pg_authid
|
|
WHERE rolname = ANY ($1::text[])
|
|
ORDER BY rolname;
|
|
`;
|
|
|
|
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 = ANY ($1::text[])
|
|
OR member.rolname = ANY ($1::text[])
|
|
ORDER BY granted.rolname, member.rolname;
|
|
`;
|
|
|
|
const OWNERSHIP_STATE_QUERY = `
|
|
WITH unexpected_owner AS (
|
|
SELECT relation.relowner AS owner_id
|
|
FROM pg_class AS relation
|
|
JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace
|
|
WHERE namespace.nspname = 'public'
|
|
AND relation.relkind IN ('r', 'p', 'S', 'v', 'm', 'f')
|
|
AND relation.relowner NOT IN (
|
|
SELECT oid FROM pg_roles WHERE rolname IN ('hivegrid_migrator', 'hivegrid_rls_definer')
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM pg_depend AS dependency
|
|
WHERE dependency.classid = 'pg_class'::regclass
|
|
AND dependency.objid = relation.oid
|
|
AND dependency.deptype = 'e'
|
|
)
|
|
UNION ALL
|
|
SELECT routine.proowner
|
|
FROM pg_proc AS routine
|
|
JOIN pg_namespace AS namespace ON namespace.oid = routine.pronamespace
|
|
WHERE namespace.nspname = 'public'
|
|
AND routine.proowner NOT IN (
|
|
SELECT oid FROM pg_roles WHERE rolname IN ('hivegrid_migrator', 'hivegrid_rls_definer')
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM pg_depend AS dependency
|
|
WHERE dependency.classid = 'pg_proc'::regclass
|
|
AND dependency.objid = routine.oid
|
|
AND dependency.deptype = 'e'
|
|
)
|
|
)
|
|
SELECT
|
|
pg_get_userbyid(database_definition.datdba) AS "databaseOwner",
|
|
pg_get_userbyid(schema_definition.nspowner) AS "schemaOwner",
|
|
(SELECT count(*)::integer FROM unexpected_owner) AS "unexpectedOwnerCount"
|
|
FROM pg_database AS database_definition
|
|
CROSS JOIN pg_namespace AS schema_definition
|
|
WHERE database_definition.datname = current_database()
|
|
AND schema_definition.nspname = 'public';
|
|
`;
|
|
|
|
export class RlsRoleProvisioningError extends Error {
|
|
constructor(message, options) {
|
|
super(message, options);
|
|
this.name = "RlsRoleProvisioningError";
|
|
}
|
|
}
|
|
|
|
function requirePassword(env, roleName) {
|
|
const key = ROLE_PASSWORD_ENV[roleName];
|
|
const password = env[key];
|
|
|
|
if (typeof password !== "string" || password.length < MINIMUM_PASSWORD_LENGTH) {
|
|
throw new RlsRoleProvisioningError(`${key} must contain at least ${MINIMUM_PASSWORD_LENGTH} characters`);
|
|
}
|
|
if (/\0|\r|\n/.test(password)) {
|
|
throw new RlsRoleProvisioningError(`${key} must not contain NUL or newline characters`);
|
|
}
|
|
|
|
return password;
|
|
}
|
|
|
|
export function readProvisioningConfig(env = process.env) {
|
|
const connectionString = env[PROVISION_DATABASE_URL_ENV];
|
|
if (!connectionString) {
|
|
throw new RlsRoleProvisioningError(`${PROVISION_DATABASE_URL_ENV} is required`);
|
|
}
|
|
|
|
let parsedUrl;
|
|
try {
|
|
parsedUrl = new URL(connectionString);
|
|
} catch {
|
|
throw new RlsRoleProvisioningError(`${PROVISION_DATABASE_URL_ENV} must be a valid PostgreSQL URL`);
|
|
}
|
|
if (!new Set(["postgres:", "postgresql:"]).has(parsedUrl.protocol) || !parsedUrl.username || parsedUrl.pathname === "/") {
|
|
throw new RlsRoleProvisioningError(`${PROVISION_DATABASE_URL_ENV} must identify a PostgreSQL user and database`);
|
|
}
|
|
|
|
const passwords = {
|
|
hivegrid_migrator: requirePassword(env, "hivegrid_migrator"),
|
|
hivegrid_app: requirePassword(env, "hivegrid_app"),
|
|
hivegrid_portal: requirePassword(env, "hivegrid_portal"),
|
|
hivegrid_worker: requirePassword(env, "hivegrid_worker"),
|
|
hivegrid_authentik_jit: requirePassword(env, "hivegrid_authentik_jit"),
|
|
};
|
|
if (new Set(Object.values(passwords)).size !== Object.keys(passwords).length) {
|
|
throw new RlsRoleProvisioningError("Every managed login role must use a distinct password");
|
|
}
|
|
|
|
return { connectionString, passwords };
|
|
}
|
|
|
|
export function verifyProvisioningState({ roles, memberships, ownership }) {
|
|
if (roles.length !== MANAGED_ROLES.length) {
|
|
throw new RlsRoleProvisioningError("Managed PostgreSQL role verification returned an incomplete role set");
|
|
}
|
|
|
|
for (const roleName of MANAGED_ROLES) {
|
|
const actual = roles.find((role) => role.roleName === roleName);
|
|
const expected = EXPECTED_ROLE_ATTRIBUTES[roleName];
|
|
if (!actual || Object.entries(expected).some(([key, value]) => actual[key] !== value)) {
|
|
throw new RlsRoleProvisioningError(`Managed PostgreSQL role ${roleName} does not match the required security contract`);
|
|
}
|
|
}
|
|
|
|
if (
|
|
memberships.length !== 1 ||
|
|
memberships[0].grantedRole !== "hivegrid_rls_definer" ||
|
|
memberships[0].memberRole !== "hivegrid_migrator" ||
|
|
memberships[0].adminOption !== false ||
|
|
memberships[0].inheritOption !== false ||
|
|
memberships[0].setOption !== true
|
|
) {
|
|
throw new RlsRoleProvisioningError("Managed PostgreSQL roles have an unexpected membership edge");
|
|
}
|
|
|
|
if (!ownership || ownership.databaseOwner !== "hivegrid_migrator" || ownership.schemaOwner !== "hivegrid_migrator" || ownership.unexpectedOwnerCount !== 0) {
|
|
throw new RlsRoleProvisioningError("HiveGrid database objects are not exclusively owned by the migration contract");
|
|
}
|
|
}
|
|
|
|
export async function provisionRlsRoles({
|
|
env = process.env,
|
|
clientFactory = (connectionString) => new pg.Client({ connectionString, application_name: "hivegrid-rls-role-provisioner" }),
|
|
stdout = process.stdout,
|
|
} = {}) {
|
|
const config = readProvisioningConfig(env);
|
|
const client = clientFactory(config.connectionString);
|
|
let transactionStarted = false;
|
|
|
|
try {
|
|
await client.connect();
|
|
const preflight = await client.query(`
|
|
SELECT current_user AS "roleName", rolsuper AS "isSuperuser"
|
|
FROM pg_roles
|
|
WHERE rolname = current_user;
|
|
`);
|
|
if (preflight.rows.length !== 1 || preflight.rows[0].isSuperuser !== true) {
|
|
throw new RlsRoleProvisioningError(`${PROVISION_DATABASE_URL_ENV} must authenticate as a PostgreSQL superuser`);
|
|
}
|
|
|
|
await client.query("BEGIN");
|
|
transactionStarted = true;
|
|
await client.query("SET LOCAL password_encryption = 'scram-sha-256'");
|
|
await client.query(
|
|
`SELECT
|
|
set_config('hivegrid.provision.migrator_password', $1, true),
|
|
set_config('hivegrid.provision.app_password', $2, true),
|
|
set_config('hivegrid.provision.portal_password', $3, true),
|
|
set_config('hivegrid.provision.worker_password', $4, true),
|
|
set_config('hivegrid.provision.authentik_jit_password', $5, true);`,
|
|
[
|
|
config.passwords.hivegrid_migrator,
|
|
config.passwords.hivegrid_app,
|
|
config.passwords.hivegrid_portal,
|
|
config.passwords.hivegrid_worker,
|
|
config.passwords.hivegrid_authentik_jit,
|
|
]
|
|
);
|
|
await client.query(ROLE_PROVISIONING_SQL);
|
|
|
|
const roleState = await client.query(ROLE_STATE_QUERY, [MANAGED_ROLES]);
|
|
const membershipState = await client.query(MEMBERSHIP_STATE_QUERY, [MANAGED_ROLES]);
|
|
const ownershipState = await client.query(OWNERSHIP_STATE_QUERY);
|
|
verifyProvisioningState({
|
|
roles: roleState.rows,
|
|
memberships: membershipState.rows,
|
|
ownership: ownershipState.rows[0],
|
|
});
|
|
|
|
await client.query("COMMIT");
|
|
transactionStarted = false;
|
|
stdout.write("PASS HiveGrid PostgreSQL roles provisioned and verified\n");
|
|
} catch (error) {
|
|
if (transactionStarted) {
|
|
await client.query("ROLLBACK").catch(() => undefined);
|
|
}
|
|
if (error instanceof RlsRoleProvisioningError) {
|
|
throw error;
|
|
}
|
|
throw new RlsRoleProvisioningError("PostgreSQL role provisioning 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 provisionRlsRoles();
|
|
} catch (error) {
|
|
const message = error instanceof RlsRoleProvisioningError ? error.message : "PostgreSQL role provisioning failed without a safe diagnostic";
|
|
process.stderr.write(`FAIL ${message}\n`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|