redis: say whether it still holds a consumer's ACL user
The server keeps ACL users in memory only, so a restart forgets every consumer while the provisioner keeps running (hq issue 120). holds() checks ACL GETUSER for the user, enabled, with the mesh's password, so the harness makes a forgotten user again. Needs mesh-sdk 0.1.1.
This commit is contained in:
+22
-1
@@ -8,7 +8,7 @@
|
|||||||
// order requests were sent, which is what the queue below relies on.
|
// order requests were sent, which is what the queue below relies on.
|
||||||
|
|
||||||
import { createConnection, type Socket } from "node:net";
|
import { createConnection, type Socket } from "node:net";
|
||||||
import { randomBytes } from "node:crypto";
|
import { createHash, randomBytes } from "node:crypto";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
/** A parsed RESP value. Errors are surfaced as rejected commands, not as this type. */
|
/** 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);
|
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 {
|
close(): void {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.destroy();
|
this.socket.destroy();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@novox/mesh-sdk": "^0.1.0"
|
"@novox/mesh-sdk": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|||||||
@@ -43,4 +43,11 @@ runProvisioner("redis-cache", {
|
|||||||
await redis.deleteAclUser(p.as);
|
await redis.deleteAclUser(p.as);
|
||||||
await announce("module.redis.cache.deprovisioned", { username: 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);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user