115 lines
3.5 KiB
JavaScript
115 lines
3.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "vitest";
|
|
|
|
import { MIGRATOR_DATABASE_URL_ENV, MigratorRoleVerificationError, verifyMigratorRole, verifyMigratorRoleState } from "./verify-migrator-role.mjs";
|
|
|
|
const SECRET_URL = "postgresql://hivegrid_migrator:migrator-secret@db.example.test:5432/hivegrid";
|
|
|
|
function validState() {
|
|
return {
|
|
session: { sessionRole: "hivegrid_migrator", currentRole: "hivegrid_migrator" },
|
|
roles: [
|
|
{
|
|
roleName: "hivegrid_migrator",
|
|
canLogin: true,
|
|
isSuperuser: false,
|
|
canCreateDb: false,
|
|
canCreateRole: false,
|
|
inherits: false,
|
|
replicates: false,
|
|
bypassesRls: false,
|
|
},
|
|
],
|
|
memberships: [
|
|
{
|
|
grantedRole: "hivegrid_rls_definer",
|
|
memberRole: "hivegrid_migrator",
|
|
adminOption: false,
|
|
inheritOption: false,
|
|
setOption: true,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
class FakeClient {
|
|
constructor({ state = validState(), queryError } = {}) {
|
|
this.state = state;
|
|
this.queryError = queryError;
|
|
this.queryIndex = 0;
|
|
this.connected = false;
|
|
this.ended = false;
|
|
}
|
|
|
|
async connect() {
|
|
this.connected = true;
|
|
}
|
|
|
|
async query() {
|
|
if (this.queryError) {
|
|
throw this.queryError;
|
|
}
|
|
const rows = [this.state.session, this.state.roles, this.state.memberships][this.queryIndex++];
|
|
return { rows: Array.isArray(rows) ? rows : [rows] };
|
|
}
|
|
|
|
async end() {
|
|
this.ended = true;
|
|
}
|
|
}
|
|
|
|
describe("verify-migrator-role", () => {
|
|
it("requires DIRECT_URL", async () => {
|
|
await assert.rejects(verifyMigratorRole({ env: {} }), new RegExp(MIGRATOR_DATABASE_URL_ENV));
|
|
});
|
|
|
|
it("accepts only the exact non-superuser migrator contract", async () => {
|
|
const client = new FakeClient();
|
|
let output = "";
|
|
|
|
await verifyMigratorRole({
|
|
env: { [MIGRATOR_DATABASE_URL_ENV]: SECRET_URL },
|
|
clientFactory: () => client,
|
|
stdout: { write: (value) => (output += value) },
|
|
});
|
|
|
|
assert.equal(client.connected, true);
|
|
assert.equal(client.ended, true);
|
|
assert.equal(output, "PASS HiveGrid migrator role verified\n");
|
|
assert.doesNotMatch(output, /migrator-secret|db\.example\.test/);
|
|
});
|
|
|
|
it("rejects superuser, role switching, and membership drift", () => {
|
|
const superuser = validState();
|
|
superuser.roles[0].isSuperuser = true;
|
|
assert.throws(() => verifyMigratorRoleState(superuser), /non-superuser security contract/);
|
|
|
|
const switched = validState();
|
|
switched.session.sessionRole = "postgres";
|
|
assert.throws(() => verifyMigratorRoleState(switched), /authenticate directly as hivegrid_migrator/);
|
|
|
|
const extraMembership = validState();
|
|
extraMembership.memberships.push({ grantedRole: "admin", memberRole: "hivegrid_migrator" });
|
|
assert.throws(() => verifyMigratorRoleState(extraMembership), /unexpected PostgreSQL role membership/);
|
|
});
|
|
|
|
it("suppresses database errors and connection details", async () => {
|
|
const client = new FakeClient({ queryError: new Error(`database rejected ${SECRET_URL}`) });
|
|
let error;
|
|
|
|
try {
|
|
await verifyMigratorRole({
|
|
env: { [MIGRATOR_DATABASE_URL_ENV]: SECRET_URL },
|
|
clientFactory: () => client,
|
|
});
|
|
} catch (caught) {
|
|
error = caught;
|
|
}
|
|
|
|
assert.ok(error instanceof MigratorRoleVerificationError);
|
|
assert.match(error.message, /inspect restricted database logs/);
|
|
assert.doesNotMatch(error.message, /migrator-secret|db\.example\.test/);
|
|
assert.equal(client.ended, true);
|
|
});
|
|
});
|