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:
@@ -134,6 +134,35 @@ export class MailuClient {
|
||||
await this.api("DELETE", `/user/${encodeURIComponent(email)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a mailbox exists, is enabled, and accepts exactly this password. Read-only. Existence
|
||||
* from the admin API; the password from `doveadm auth test` in the imap container, which is how
|
||||
* the mail server itself authenticates, and which exits 77 for a refused login. The password
|
||||
* reaches doveadm through the exec's environment, never the host's argv. An unreachable API or
|
||||
* container rejects (novox/hq issue 120).
|
||||
*/
|
||||
async holdsUser(email: string, password: string): Promise<boolean> {
|
||||
const res = await fetch(`${this.baseUrl}/user/${encodeURIComponent(email)}`, {
|
||||
headers: { Authorization: this.apiKey, Accept: "application/json" },
|
||||
});
|
||||
if (res.status === 404) return false;
|
||||
if (!res.ok) throw new Error(`Mailu API GET /user/${email}: ${res.status} ${await res.text()}`);
|
||||
const user = (await res.json()) as { enabled?: boolean };
|
||||
if (user.enabled === false) return false;
|
||||
try {
|
||||
await run(
|
||||
"docker",
|
||||
["exec", "-e", "MESH_USER", "-e", "MESH_PW", this.imapContainer,
|
||||
"sh", "-c", 'doveadm auth test "$MESH_USER" "$MESH_PW"'],
|
||||
{ env: { ...process.env, MESH_USER: email, MESH_PW: password }, timeout: 30_000 },
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if ((err as { code?: number }).code === 77) return false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async listAliases(): Promise<MailuAlias[]> {
|
||||
const aliases = await this.api<any[]>("GET", "/alias");
|
||||
return (aliases ?? []).map((a) => ({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -62,4 +62,9 @@ runProvisioner("smtp", {
|
||||
// named-account consumer is an operator action until the harness carries values here.
|
||||
await mailu.deleteUser(`${p.as}@${domain()}`).catch(() => {});
|
||||
},
|
||||
// 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 mailu.holdsUser(addressOf(p), p.password);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user