Every credential provider says whether it still holds a consumer (hq issue 120) #84

Merged
jschoubben merged 4 commits from fix/120-redis-says-what-it-holds into main 2026-09-25 23:31:29 +00:00
3 changed files with 30 additions and 2 deletions
Showing only changes of commit 3d271f72ea - Show all commits
+22 -1
View File
@@ -8,7 +8,7 @@
// order requests were sent, which is what the queue below relies on.
import { createConnection, type Socket } from "node:net";
import { randomBytes } from "node:crypto";
import { createHash, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";
/** A parsed RESP value. Errors are surfaced as rejected commands, not as this type. */
@@ -113,6 +113,27 @@ export class RedisClient {
await this.command("ACL", "DELUSER", username);
}
/**
* Whether an ACL user exists, is enabled, and accepts exactly this password. Read-only: it asks
* `ACL GETUSER`, which answers nil for an unknown user and otherwise a flat list of fields, among
* them `flags` and `passwords`, the latter as SHA-256 hex. This server keeps no ACL file, so its
* users live in memory and a restart forgets them. This is how the provisioner notices
* (novox/hq issue 120).
*/
async holdsAclUser(username: string, password: string): Promise<boolean> {
const reply = await this.command("ACL", "GETUSER", username);
if (!Array.isArray(reply)) return false;
const field = (name: string): RespValue | undefined => {
const i = reply.indexOf(name);
return i >= 0 ? reply[i + 1] : undefined;
};
const flags = field("flags");
const passwords = field("passwords");
if (!Array.isArray(flags) || !flags.includes("on")) return false;
if (!Array.isArray(passwords)) return false;
return passwords.includes(createHash("sha256").update(password).digest("hex"));
}
close(): void {
if (this.socket) {
this.socket.destroy();
+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",
+7
View File
@@ -43,4 +43,11 @@ runProvisioner("redis-cache", {
await redis.deleteAclUser(p.as);
await announce("module.redis.cache.deprovisioned", { username: p.as });
},
// This server keeps its ACL users in memory only, so a restart of it forgets every consumer while
// this provisioner keeps running. Asked every minute, so a forgotten user is made again instead
// of every consumer failing to authenticate in silence (novox/hq issue 120).
async holds(p: Provision): Promise<boolean> {
return redis.holdsAclUser(p.as, p.password);
},
});