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:
@@ -22,7 +22,7 @@ and is then given, by the host, from an ordinary declaration:
|
|||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `mesh.json` | every consumer, what it asked for, and where its credential is |
|
| `mesh.json` | every consumer, what it asked for, and where its credential is |
|
||||||
| `<node>.secret` | one consumer's password, alone in the file, sealed in transit and written in plain by the host |
|
| `<node>.<module>.secret` | one consumer's password, alone in the file, sealed in transit and written in plain by the host. Named after both, because a consumer is a module on a machine and a node routinely runs several (`novox/hq` 04-ISSUES/022) |
|
||||||
|
|
||||||
Two files rather than one because the mesh discarded the plaintext and cannot compose a document
|
Two files rather than one because the mesh discarded the plaintext and cannot compose a document
|
||||||
containing it. The consequence is a good one: the readable half stays readable, and the secret
|
containing it. The consequence is a good one: the readable half stays readable, and the secret
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async function meshWrote(
|
|||||||
given: consumers.map((c) => ({
|
given: consumers.map((c) => ({
|
||||||
from: c.module,
|
from: c.module,
|
||||||
node: c.node,
|
node: c.node,
|
||||||
secret: `${GRANTS}/${c.node}.secret`,
|
secret: `${GRANTS}/${c.node}.${c.module}.secret`,
|
||||||
values: { bucket: c.bucket },
|
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.
|
// make the revocation test pass for a reason that is not the one being tested.
|
||||||
await must(`find ${GRANTS} -name '*.secret' -delete`);
|
await must(`find ${GRANTS} -name '*.secret' -delete`);
|
||||||
for (const c of consumers) {
|
for (const c of consumers) {
|
||||||
await must(`printf %s ${shellQuote(c.secret)} > ${GRANTS}/${c.node}.secret`);
|
await must(`printf %s ${shellQuote(c.secret)} > ${GRANTS}/${c.node}.${c.module}.secret`);
|
||||||
await must(`chmod 600 ${GRANTS}/${c.node}.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);
|
assert.ok(ok, out);
|
||||||
|
|
||||||
const listed = await admin(`admin user list root --json`);
|
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}`);
|
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();
|
const { out, ok } = await provision();
|
||||||
assert.ok(ok, out);
|
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}`);
|
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}`);
|
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();
|
const { out, ok } = await provision();
|
||||||
assert.ok(ok, out);
|
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}`);
|
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");
|
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();
|
const staged = await provision();
|
||||||
assert.ok(staged.ok, staged.out);
|
assert.ok(staged.ok, staged.out);
|
||||||
const present = await admin(`admin user list root --json`);
|
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([
|
await meshWrote([
|
||||||
{ node: "workstation", module: "photos", bucket: "photos", secret: "rotated-secret-cccccccc" },
|
{ 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);
|
assert.ok(ok, out);
|
||||||
|
|
||||||
const after = await admin(`admin user list root --json`);
|
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}`);
|
assert.ok(!after.out.includes("mesh_laptop_invoices"), `a key nobody asks for survived:\n${after.out}`);
|
||||||
const still = await canUse("mesh_laptop", "second-secret-bbbbbbbb", "invoices");
|
const still = await canUse("mesh_laptop_invoices", "second-secret-bbbbbbbb", "invoices");
|
||||||
assert.ok(!still.ok, "a revoked key still works");
|
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([
|
await meshWrote([
|
||||||
{ node: "workstation", module: "photos", bucket: "photos", secret: "rotated-secret-cccccccc" },
|
{ 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();
|
const { out, ok } = await provision();
|
||||||
assert.ok(!ok, `it carried on without the credential:\n${out}`);
|
assert.ok(!ok, `it carried on without the credential:\n${out}`);
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ async function meshWrote(
|
|||||||
given: consumers.map((c) => ({
|
given: consumers.map((c) => ({
|
||||||
from: c.module,
|
from: c.module,
|
||||||
node: c.node,
|
node: c.node,
|
||||||
secret: `${GRANTS}/${c.node}.secret`,
|
secret: `${GRANTS}/${c.node}.${c.module}.secret`,
|
||||||
values: { name: c.name },
|
values: { name: c.name },
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@@ -90,8 +90,8 @@ async function meshWrote(
|
|||||||
// make the revocation test pass for a reason that is not the one being tested.
|
// make the revocation test pass for a reason that is not the one being tested.
|
||||||
await must(`find ${GRANTS} -name '*.secret' -delete`);
|
await must(`find ${GRANTS} -name '*.secret' -delete`);
|
||||||
for (const c of consumers) {
|
for (const c of consumers) {
|
||||||
await must(`printf %s ${shellQuote(c.password)} > ${GRANTS}/${c.node}.secret`);
|
await must(`printf %s ${shellQuote(c.password)} > ${GRANTS}/${c.node}.${c.module}.secret`);
|
||||||
await must(`chmod 600 ${GRANTS}/${c.node}.secret`);
|
await must(`chmod 600 ${GRANTS}/${c.node}.${c.module}.secret`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,9 +178,9 @@ test("a password the mesh generated becomes a login that works", { skip, timeout
|
|||||||
const { out, ok } = await provision();
|
const { out, ok } = await provision();
|
||||||
assert.ok(ok, out);
|
assert.ok(ok, out);
|
||||||
|
|
||||||
assert.equal(await sql(`select rolcanlogin from pg_roles where rolname = 'mesh_workstation'`), "t");
|
assert.equal(await sql(`select rolcanlogin from pg_roles where rolname = 'mesh_workstation_meshboard'`), "t");
|
||||||
assert.equal(await sql(`select 1 from pg_database where datname = 'meshboard'`), "1");
|
assert.equal(await sql(`select 1 from pg_database where datname = 'meshboard'`), "1");
|
||||||
const attempt = await tryLogIn("mesh_workstation", "first-password-aaa", "meshboard");
|
const attempt = await tryLogIn("mesh_workstation_meshboard", "first-password-aaa", "meshboard");
|
||||||
assert.ok(attempt.ok, `the consumer cannot log in with the password the mesh gave it:\n${attempt.out}`);
|
assert.ok(attempt.ok, `the consumer cannot log in with the password the mesh gave it:\n${attempt.out}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ test("running it again reaches the same state and says nothing", { skip, timeout
|
|||||||
const { out, ok } = await provision();
|
const { out, ok } = await provision();
|
||||||
assert.ok(ok, out);
|
assert.ok(ok, out);
|
||||||
assert.equal(out.trim(), "", `it did work on a second run: ${out}`);
|
assert.equal(out.trim(), "", `it did work on a second run: ${out}`);
|
||||||
assert.ok(await canLogIn("mesh_workstation", "first-password-aaa", "meshboard"));
|
assert.ok(await canLogIn("mesh_workstation_meshboard", "first-password-aaa", "meshboard"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rotating the password makes the new one work and the old one stop", { skip, timeout: 300_000 }, async () => {
|
test("rotating the password makes the new one work and the old one stop", { skip, timeout: 300_000 }, async () => {
|
||||||
@@ -203,11 +203,11 @@ test("rotating the password makes the new one work and the old one stop", { skip
|
|||||||
assert.ok(ok, out);
|
assert.ok(ok, out);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
await canLogIn("mesh_workstation", "second-password-bbb", "meshboard"),
|
await canLogIn("mesh_workstation_meshboard", "second-password-bbb", "meshboard"),
|
||||||
"the rotated password does not work",
|
"the rotated password does not work",
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await canLogIn("mesh_workstation", "first-password-aaa", "meshboard"),
|
await canLogIn("mesh_workstation_meshboard", "first-password-aaa", "meshboard"),
|
||||||
false,
|
false,
|
||||||
"the old password still works, so the rotation changed nothing",
|
"the old password still works, so the rotation changed nothing",
|
||||||
);
|
);
|
||||||
@@ -220,11 +220,11 @@ test("a consumer that goes away loses its login", { skip, timeout: 300_000 }, as
|
|||||||
await meshWrote([]);
|
await meshWrote([]);
|
||||||
const { out, ok } = await provision();
|
const { out, ok } = await provision();
|
||||||
assert.ok(ok, out);
|
assert.ok(ok, out);
|
||||||
assert.match(out, /revoked mesh_workstation/);
|
assert.match(out, /revoked mesh_workstation_meshboard/);
|
||||||
|
|
||||||
assert.equal(await sql(`select rolcanlogin from pg_roles where rolname = 'mesh_workstation'`), "f");
|
assert.equal(await sql(`select rolcanlogin from pg_roles where rolname = 'mesh_workstation_meshboard'`), "f");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await canLogIn("mesh_workstation", "second-password-bbb", "meshboard"),
|
await canLogIn("mesh_workstation_meshboard", "second-password-bbb", "meshboard"),
|
||||||
false,
|
false,
|
||||||
"a consumer nobody asks for any more can still log in",
|
"a consumer nobody asks for any more can still log in",
|
||||||
);
|
);
|
||||||
@@ -248,11 +248,11 @@ test("a manifest naming a credential that was never written is refused", { skip,
|
|||||||
await meshWrote([]);
|
await meshWrote([]);
|
||||||
await must(
|
await must(
|
||||||
`printf %s '{"contributions":1,"requirement":"postgres-database","given":[` +
|
`printf %s '{"contributions":1,"requirement":"postgres-database","given":[` +
|
||||||
`{"from":"meshboard","node":"ghost","secret":"${GRANTS}/ghost.secret","values":{"name":"ghost"}}` +
|
`{"from":"meshboard","node":"ghost","secret":"${GRANTS}/ghost.meshboard.secret","values":{"name":"ghost"}}` +
|
||||||
`]}' > ${GRANTS}/mesh.json`,
|
`]}' > ${GRANTS}/mesh.json`,
|
||||||
);
|
);
|
||||||
const { out, ok } = await provision();
|
const { out, ok } = await provision();
|
||||||
assert.equal(ok, false, "it carried on past a missing credential");
|
assert.equal(ok, false, "it carried on past a missing credential");
|
||||||
assert.match(out, /should be at .*ghost\.secret/);
|
assert.match(out, /should be at .*ghost\.secret/);
|
||||||
assert.equal(await sql(`select count(*) from pg_roles where rolname = 'mesh_ghost'`), "0");
|
assert.equal(await sql(`select count(*) from pg_roles where rolname = 'mesh_ghost_meshboard'`), "0");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user