Every credential provider says whether it still holds a consumer

holds() for postgres, mssql, mongodb, minio, lavinmq, mosquitto, mailu
and gitea, so the harness makes again a login the backend lost (hq issue
120). Each checks the mesh's password as the consumer presents it, or
compares it read-only, and returns false only when the backend says the
credential is absent or wrong; an unreachable backend throws.
This commit is contained in:
jochen
2026-09-26 01:09:52 +02:00
parent 3d271f72ea
commit 0cb0f814b4
24 changed files with 286 additions and 12 deletions
+24
View File
@@ -99,6 +99,30 @@ export class PostgresClient {
await this.query(`GRANT ALL PRIVILEGES ON DATABASE ${ident(database)} TO ${ident(role)}`);
}
/**
* Whether `role` can log in to `database` with exactly `password`: the consumer's own view of its
* credential, checked by connecting as it. Read-only. `false` only when the server says so (the
* role, the password or the database is wrong or gone); an unreachable server rejects instead,
* because being unable to ask is not evidence of loss (novox/hq issue 120).
*/
async canConnectAs(database: string, role: string, password: string): Promise<boolean> {
try {
await run(
"psql",
["-h", this.conn.host, "-p", String(this.conn.port), "-U", role, "-d", database,
"-v", "ON_ERROR_STOP=1", "--no-psqlrc", "-tAc", "SELECT 1"],
{ env: { ...process.env, PGPASSWORD: password, PGCONNECT_TIMEOUT: "10" }, timeout: 20_000 },
);
return true;
} catch (err) {
const text = `${(err as { stderr?: string }).stderr ?? ""}`;
if (/password authentication failed|role ".*" does not exist|database ".*" does not exist|not permitted to log in|permission denied for database/i.test(text)) {
return false;
}
throw err;
}
}
/** Drop a database and its owning role, idempotently, after evicting live connections. */
async dropDatabaseAndRole(database: string, role: string): Promise<void> {
await this.query(
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"private": true,
"dependencies": {
"@novox/mesh-sdk": "^0.1.0"
"@novox/mesh-sdk": "^0.1.1"
},
"devDependencies": {
"@types/node": "^22.0.0",
+5
View File
@@ -45,4 +45,9 @@ runProvisioner("postgres-database", {
await postgres.dropDatabaseAndRole(p.as, p.as);
await announce("module.postgres.database.deprovisioned", { database: p.as });
},
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
async holds(p: Provision): Promise<boolean> {
return postgres.canConnectAs(p.as, p.as, p.password);
},
});