A grant file and a provisioned login name the module too

novox/hq 04-ISSUES/022: a consumer is a module on a machine, not a
machine. The mesh now writes <node>.<module>.secret and the provisioners
name the role and the access key after both.

The fixtures here write what the mesh writes, so they move with it —
that is the whole point of them, and a fixture that kept the old shape
would agree with the bug rather than catch it.

The object-store assertions are the ones that mattered most: one store
holds every bucket behind one endpoint, so isolation is a policy rather
than a property. One access key per machine meant every module on a node
shared it, and the policy confining each consumer to its own bucket
confined none of them.
This commit is contained in:
2026-09-01 02:45:40 +02:00
parent 0ffb24ff5d
commit e7f4a49e40
3 changed files with 27 additions and 27 deletions
+13 -13
View File
@@ -90,7 +90,7 @@ async function meshWrote(
given: consumers.map((c) => ({
from: c.module,
node: c.node,
secret: `${GRANTS}/${c.node}.secret`,
secret: `${GRANTS}/${c.node}.${c.module}.secret`,
values: { bucket: c.bucket },
})),
};
@@ -100,8 +100,8 @@ async function meshWrote(
// make the revocation test pass for a reason that is not the one being tested.
await must(`find ${GRANTS} -name '*.secret' -delete`);
for (const c of consumers) {
await must(`printf %s ${shellQuote(c.secret)} > ${GRANTS}/${c.node}.secret`);
await must(`chmod 600 ${GRANTS}/${c.node}.secret`);
await must(`printf %s ${shellQuote(c.secret)} > ${GRANTS}/${c.node}.${c.module}.secret`);
await must(`chmod 600 ${GRANTS}/${c.node}.${c.module}.secret`);
}
}
@@ -197,9 +197,9 @@ test("a secret the mesh generated becomes a key that works", { skip, timeout: 30
assert.ok(ok, out);
const listed = await admin(`admin user list root --json`);
assert.ok(listed.out.includes("mesh_workstation"), `no key was made for the consumer:\n${listed.out}`);
assert.ok(listed.out.includes("mesh_workstation_photos"), `no key was made for the consumer:\n${listed.out}`);
const used = await canUse("mesh_workstation", "first-secret-aaaaaaaa", "photos");
const used = await canUse("mesh_workstation_photos", "first-secret-aaaaaaaa", "photos");
assert.ok(used.ok, `the consumer cannot use the bucket the mesh gave it:\n${used.out}`);
});
@@ -214,10 +214,10 @@ test("a consumer cannot reach another consumer's bucket", { skip, timeout: 300_0
const { out, ok } = await provision();
assert.ok(ok, out);
const own = await canUse("mesh_laptop", "second-secret-bbbbbbbb", "invoices");
const own = await canUse("mesh_laptop_invoices", "second-secret-bbbbbbbb", "invoices");
assert.ok(own.ok, `a consumer cannot use its own bucket:\n${own.out}`);
const other = await canUse("mesh_laptop", "second-secret-bbbbbbbb", "photos");
const other = await canUse("mesh_laptop_invoices", "second-secret-bbbbbbbb", "photos");
assert.ok(!other.ok, `a consumer reached another consumer's bucket:\n${other.out}`);
});
@@ -230,10 +230,10 @@ test("rotating the secret makes the new one work and the old one stop", { skip,
const { out, ok } = await provision();
assert.ok(ok, out);
const now = await canUse("mesh_workstation", "rotated-secret-cccccccc", "photos");
const now = await canUse("mesh_workstation_photos", "rotated-secret-cccccccc", "photos");
assert.ok(now.ok, `the rotated secret does not work:\n${now.out}`);
const before = await canUse("mesh_workstation", "first-secret-aaaaaaaa", "photos");
const before = await canUse("mesh_workstation_photos", "first-secret-aaaaaaaa", "photos");
assert.ok(!before.ok, "the secret that was rotated away still works");
});
@@ -252,7 +252,7 @@ test("a consumer that goes away loses its key", { skip, timeout: 300_000 }, asyn
const staged = await provision();
assert.ok(staged.ok, staged.out);
const present = await admin(`admin user list root --json`);
assert.ok(present.out.includes("mesh_laptop"), `the consumer to be removed was never made:\n${present.out}`);
assert.ok(present.out.includes("mesh_laptop_invoices"), `the consumer to be removed was never made:\n${present.out}`);
await meshWrote([
{ node: "workstation", module: "photos", bucket: "photos", secret: "rotated-secret-cccccccc" },
@@ -261,8 +261,8 @@ test("a consumer that goes away loses its key", { skip, timeout: 300_000 }, asyn
assert.ok(ok, out);
const after = await admin(`admin user list root --json`);
assert.ok(!after.out.includes("mesh_laptop"), `a key nobody asks for survived:\n${after.out}`);
const still = await canUse("mesh_laptop", "second-secret-bbbbbbbb", "invoices");
assert.ok(!after.out.includes("mesh_laptop_invoices"), `a key nobody asks for survived:\n${after.out}`);
const still = await canUse("mesh_laptop_invoices", "second-secret-bbbbbbbb", "invoices");
assert.ok(!still.ok, "a revoked key still works");
});
@@ -286,7 +286,7 @@ test("a manifest naming a credential that was never written is refused", { skip,
await meshWrote([
{ node: "workstation", module: "photos", bucket: "photos", secret: "rotated-secret-cccccccc" },
]);
await must(`rm -f ${GRANTS}/workstation.secret`);
await must(`rm -f ${GRANTS}/workstation.photos.secret`);
const { out, ok } = await provision();
assert.ok(!ok, `it carried on without the credential:\n${out}`);