|
|
|
@@ -1969,10 +1969,20 @@ test("the forge runs, on a database the mesh gave it", { skip, timeout: 900_000
|
|
|
|
|
"the login exists and the database it owns does not");
|
|
|
|
|
|
|
|
|
|
// And the forge itself, answering. Not that its container exists — that it serves.
|
|
|
|
|
//
|
|
|
|
|
// On the port the mesh assigned, not the one the module declared (novox/hq ADR 0038): the
|
|
|
|
|
// module says 3000 and the machine publishes wherever the mesh put it. Read from the plan,
|
|
|
|
|
// because the plan is the same composition a push sends.
|
|
|
|
|
const planned = await mesh("plan anchor --json", 120_000);
|
|
|
|
|
const mapping = (JSON.parse(planned.slice(planned.indexOf("{"))).resources as any[])
|
|
|
|
|
.find((r) => r.id === "gitea.server")?.ports
|
|
|
|
|
?.map(String).find((p: string) => p.endsWith(":3000"));
|
|
|
|
|
assert.ok(mapping, "the plan does not say where the machine publishes the forge");
|
|
|
|
|
const at = mapping.split(":")[0];
|
|
|
|
|
let answered = false;
|
|
|
|
|
let said = { out: "", ok: false };
|
|
|
|
|
for (let i = 0; i < 60 && !answered; i++) {
|
|
|
|
|
said = await on("anchor", `curl -sf -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000/`, 30_000);
|
|
|
|
|
said = await on("anchor", `curl -sf -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:${at}/`, 30_000);
|
|
|
|
|
answered = said.out.trim().startsWith("2") || said.out.trim() === "303";
|
|
|
|
|
if (!answered) await new Promise((r) => setTimeout(r, 5000));
|
|
|
|
|
}
|
|
|
|
@@ -1990,3 +2000,80 @@ test("the forge runs, on a database the mesh gave it", { skip, timeout: 900_000
|
|
|
|
|
await mesh("unassign anchor postgres");
|
|
|
|
|
await mesh("push anchor", 300_000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("a consumer's cache grant means exactly its own keys", { skip, timeout: 600_000 }, async (t) => {
|
|
|
|
|
// The third provision after a database and a bucket, and the first whose tenancy is enforced
|
|
|
|
|
// by the store's own ACL rather than by separate namespaces: every consumer shares one
|
|
|
|
|
// keyspace, so the grant is a pattern — and the test is that the pattern means what the
|
|
|
|
|
// manifest said, in both directions.
|
|
|
|
|
const raw = readFileSync(`${process.env["MESH_LAB_MODULES"]}/redis.json`, "utf8");
|
|
|
|
|
const pinned = pinnedInto(raw, stocked);
|
|
|
|
|
assert.deepEqual(stillUnpinned(pinned), [],
|
|
|
|
|
"redis still names an image nothing serves, so it could not start");
|
|
|
|
|
await must("anchor", `printf %s ${quote(pinned)} > /run-redis.json`);
|
|
|
|
|
await must("anchor", `docker cp /run-redis.json mesh-control:/run-redis.json`);
|
|
|
|
|
await mesh("module add /run-redis.json");
|
|
|
|
|
|
|
|
|
|
// A consumer with no container: what is under test is the credential's reach, and files on the
|
|
|
|
|
// machine are enough to prove it — the same reduction the first credential test makes.
|
|
|
|
|
await must("anchor", `printf %s '{"module":"cachetest","version":"1",` +
|
|
|
|
|
`"requires":["redis-cache"],` +
|
|
|
|
|
`"contributes":{"redis-cache":{"prefix":"cachetest"}},` +
|
|
|
|
|
`"binds":{"redis-cache":"/var/lib/cachetest/cache.json"},` +
|
|
|
|
|
`"secrets":{"redis-cache":"/var/lib/cachetest/cache.secret"},` +
|
|
|
|
|
`"resources":[{"id":"state","type":"directory","path":"/var/lib/cachetest","mode":"0700"}]}' ` +
|
|
|
|
|
`> /cachetest.json`);
|
|
|
|
|
await must("anchor", `docker cp /cachetest.json mesh-control:/cachetest.json`);
|
|
|
|
|
await mesh("module add /cachetest.json");
|
|
|
|
|
|
|
|
|
|
await mesh("assign anchor redis");
|
|
|
|
|
await mesh("assign anchor cachetest");
|
|
|
|
|
await mesh("push anchor", 300_000);
|
|
|
|
|
await settled("anchor");
|
|
|
|
|
|
|
|
|
|
t.after(async () => {
|
|
|
|
|
for (const name of ["cachetest", "redis"]) {
|
|
|
|
|
await mesh(`unassign anchor ${name}`).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
await mesh("push anchor", 300_000).catch(() => {});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// What the mesh told each end. The consumer's user name comes from its binding; the user's
|
|
|
|
|
// password from the sealed file beside it — both written by the host, neither invented here.
|
|
|
|
|
const bound = JSON.parse(await must("anchor", `cat /var/lib/cachetest/cache.json`));
|
|
|
|
|
const user = bound.as;
|
|
|
|
|
assert.ok(user?.startsWith("mesh_"), `the binding does not carry a usable user: ${user}`);
|
|
|
|
|
const secret = (await must("anchor", `cat /var/lib/cachetest/cache.secret`)).trim();
|
|
|
|
|
|
|
|
|
|
// The provisioner has to have run before anything can authenticate. Waited for via the store
|
|
|
|
|
// itself: the user list, asked with the server's own password, which the conf file the host
|
|
|
|
|
// wrote holds on the machine.
|
|
|
|
|
const admin = (await must("anchor",
|
|
|
|
|
`awk '/^requirepass/ {print $2}' /var/lib/redis-module/redis.conf`)).trim();
|
|
|
|
|
let granted = false;
|
|
|
|
|
for (let i = 0; i < 40 && !granted; i++) {
|
|
|
|
|
const users = (await on("anchor",
|
|
|
|
|
`docker exec redis redis-cli --no-auth-warning -a ${quote(admin)} ACL USERS`)).out;
|
|
|
|
|
granted = users.includes(user);
|
|
|
|
|
if (!granted) await new Promise((r) => setTimeout(r, 3000));
|
|
|
|
|
}
|
|
|
|
|
assert.ok(granted, `no user was created for the consumer:
|
|
|
|
|
` +
|
|
|
|
|
`${(await on("anchor", "docker logs mesh-provision-redis 2>&1 | tail -20")).out}`);
|
|
|
|
|
|
|
|
|
|
const asConsumer = (command: string) =>
|
|
|
|
|
on("anchor", `docker exec redis redis-cli --no-auth-warning ` +
|
|
|
|
|
`--user ${quote(user)} --pass ${quote(secret)} ${command}`);
|
|
|
|
|
|
|
|
|
|
// Its own keys: usable.
|
|
|
|
|
assert.match((await asConsumer("SET cachetest:proof yes")).out, /OK/,
|
|
|
|
|
"the consumer cannot write under the prefix it was granted");
|
|
|
|
|
assert.match((await asConsumer("GET cachetest:proof")).out, /yes/,
|
|
|
|
|
"the consumer cannot read back what it wrote");
|
|
|
|
|
|
|
|
|
|
// Anyone else's: refused by the store itself, which is the entire point of the grant.
|
|
|
|
|
assert.match((await asConsumer("SET other:proof no")).out, /NOPERM|no permissions/i,
|
|
|
|
|
"the consumer wrote outside its prefix — the grant means more than the manifest said");
|
|
|
|
|
assert.match((await asConsumer("FLUSHALL")).out, /NOPERM|no permissions/i,
|
|
|
|
|
"the consumer can flush the store, which no tenant may");
|
|
|
|
|
});
|
|
|
|
|