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
+17 -4
View File
@@ -125,6 +125,18 @@ export class MinioClient {
throw new Error(`minio bucketExists ${bucket}: ${status}`);
}
/**
* Whether a consumer's access key, with exactly this secret, reaches its bucket: a HEAD of the
* bucket signed as the consumer, the way it signs. Read-only. `false` when the key is unknown, the
* secret wrong, access denied or the bucket gone; any other answer rejects (novox/hq issue 120).
*/
async canReachAs(bucket: string, accessKey: string, secretKey: string): Promise<boolean> {
const { status } = await this.request("HEAD", `/${bucket}`, {}, { accessKey, secretKey });
if (status === 200) return true;
if (status === 403 || status === 404) return false;
throw new Error(`minio HEAD ${bucket} as ${accessKey}: ${status}`);
}
async createBucket(bucket: string): Promise<void> {
const { status, text } = await this.request("PUT", `/${bucket}`);
// 200 created; 409 BucketAlreadyOwnedByYou — idempotent, a re-provision must not fail.
@@ -251,6 +263,7 @@ export class MinioClient {
method: string,
path: string,
query: Record<string, string> = {},
as: { accessKey: string; secretKey: string } = { accessKey: this.rootUser, secretKey: this.rootPassword },
): Promise<{ status: number; headers: Headers; text: string }> {
const { amzDate, dateStamp } = this.stamp();
const host = new URL(this.baseUrl).host;
@@ -262,8 +275,8 @@ export class MinioClient {
const canonicalRequest = [method, encodedPath, canonicalQuery, canonicalHeaders, signedHeaders, payloadHash].join("\n");
const scope = `${dateStamp}/${this.region}/s3/aws4_request`;
const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, sha256hex(canonicalRequest)].join("\n");
const signature = hmac(this.signingKey(dateStamp), stringToSign).toString("hex");
const authorization = `AWS4-HMAC-SHA256 Credential=${this.rootUser}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
const signature = hmac(this.signingKey(dateStamp, as.secretKey), stringToSign).toString("hex");
const authorization = `AWS4-HMAC-SHA256 Credential=${as.accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
const url = `${this.baseUrl}${encodedPath}${canonicalQuery ? `?${canonicalQuery}` : ""}`;
const res = await fetch(url, {
@@ -275,8 +288,8 @@ export class MinioClient {
return { status: res.status, headers: res.headers, text };
}
private signingKey(dateStamp: string): Buffer {
const kDate = hmac(`AWS4${this.rootPassword}`, dateStamp);
private signingKey(dateStamp: string, secretKey: string = this.rootPassword): Buffer {
const kDate = hmac(`AWS4${secretKey}`, dateStamp);
const kRegion = hmac(kDate, this.region);
const kService = hmac(kRegion, "s3");
return hmac(kService, "aws4_request");
+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",
+6
View File
@@ -53,6 +53,12 @@ runProvisioner("s3-bucket", {
await announce("module.minio.bucket.removed", { bucket, accessKey: 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 minio.canReachAs(bucketFor(p.as), p.as, p.password);
},
});
/** Emit best-effort: a broker hiccup is logged and dropped, never allowed to throw back and fail a