Files
mesh-catalog/modules/minio/test/links.test.ts
T

118 lines
5.4 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { mkdtempSync, readFileSync, statSync, existsSync, readdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
// Issue 1594: a link key per bucket (novox/hq ADR 0468 §8). The fake admin below stands for minio's service
// accounts; the real client's policies come from client.ts's bucketPolicy, which the tests import.
import { bucketPolicy } from "../client.ts";
import { ensureLinkKey, linkKeyId, linkSecretPath } from "../links.ts";
import { handlers } from "../provisioner/handlers.ts";
class FakeAdmin {
baseUrl = "http://127.0.0.1:9000";
buckets = new Set<string>();
keys = new Map<string, { bucket: string; secret: string; policy: string }>();
created: string[] = [];
async bucketExists(b: string) { return this.buckets.has(b); }
async createBucket(b: string) { this.buckets.add(b); }
async canReachAs() { return true; }
async accessKeyExists(id: string) { return this.keys.has(id); }
async removeAccessKey(id: string) {
if (!this.keys.delete(id)) throw new Error("no such key");
}
async createAccessKey(bucket: string, id: string, secret: string, policy: string = bucketPolicy(bucket)) {
this.keys.set(id, { bucket, secret, policy });
this.created.push(id);
return { accessKey: id, secretKey: secret };
}
}
function setup() {
const minio = new FakeAdmin();
const dir = join(mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), "minio-links-")), "links");
const events: Array<{ type: string; body: unknown }> = [];
const h = handlers({ minio, linksDir: dir, announce: async (type, body) => { events.push({ type, body }); } });
return { minio, dir, events, h };
}
const provision = { as: "brother-key", password: "consumer-secret", consumer: "brother", derived: { bucket: "brother" } } as any;
test("creating a consumer's bucket makes a link key allowed only get and put of the bucket's objects, its secret in links/<bucket>.secret at 0600", async () => {
// 1594/AC1
const { minio, dir, h } = setup();
await h.create(provision);
const link = minio.keys.get(linkKeyId("brother"));
assert.ok(link, "a link key exists for the bucket");
const policy = JSON.parse(link.policy);
assert.equal(policy.Statement.length, 1);
assert.deepEqual(policy.Statement[0].Action, ["s3:GetObject", "s3:PutObject"]);
assert.deepEqual(policy.Statement[0].Resource, ["arn:aws:s3:::brother/*"]);
const path = linkSecretPath(dir, "brother");
assert.equal(statSync(path).mode & 0o777, 0o600);
assert.equal(readFileSync(path, "utf8").trim(), link.secret);
assert.ok(link.secret.length >= 32);
});
test("buckets that existed before get a link key at start, and a second start makes none twice", async () => {
// 1594/AC2
const { minio, dir } = setup();
for (const b of ["brother", "mesh-mail", "nextcloud", "invoicing"]) minio.buckets.add(b);
for (const b of minio.buckets) await ensureLinkKey(minio, dir, b);
assert.equal(minio.keys.size, 4);
const first = [...minio.created];
const secret = readFileSync(linkSecretPath(dir, "nextcloud"), "utf8");
for (const b of minio.buckets) await ensureLinkKey(minio, dir, b);
assert.deepEqual(minio.created, first, "no key made a second time");
assert.equal(readFileSync(linkSecretPath(dir, "nextcloud"), "utf8"), secret, "the secret is kept");
});
test("a link key lost in minio is made again under the kept secret", async () => {
// 1594/AC2
const { minio, dir } = setup();
await ensureLinkKey(minio, dir, "brother");
const secret = minio.keys.get(linkKeyId("brother"))!.secret;
minio.keys.clear();
await ensureLinkKey(minio, dir, "brother");
assert.equal(minio.keys.get(linkKeyId("brother"))!.secret, secret);
});
test("removing a consumer revokes its bucket's link key and keeps the bucket", async () => {
// 1594/AC3
const { minio, dir, h } = setup();
await h.create(provision);
assert.ok(minio.keys.has(linkKeyId("brother")));
await h.remove({ as: "brother-key", derived: { bucket: "brother" } });
assert.ok(!minio.keys.has(linkKeyId("brother")), "link key revoked");
assert.ok(!minio.keys.has("brother-key"), "consumer key revoked");
assert.ok(minio.buckets.has("brother"), "bucket kept");
assert.ok(!existsSync(linkSecretPath(dir, "brother")), "secret file removed");
});
test("the consumer's own key keeps its s3:* policy, and the link key's secret is in no event", async () => {
// 1594/AC4
const { minio, dir, events, h } = setup();
await h.create(provision);
const own = minio.keys.get("brother-key")!;
assert.equal(own.secret, "consumer-secret");
assert.equal(own.policy, bucketPolicy("brother"));
assert.deepEqual(JSON.parse(own.policy).Statement[0].Action, ["s3:*"]);
const linkSecret = minio.keys.get(linkKeyId("brother"))!.secret;
await h.remove({ as: "brother-key", derived: { bucket: "brother" } });
assert.ok(events.length >= 2);
for (const e of events) assert.ok(!JSON.stringify(e).includes(linkSecret), `${e.type} carries no link secret`);
assert.deepEqual(readdirSync(dir), [], "nothing else is kept in the links directory");
});
test("a link key id fits an access key (20 characters) whatever the bucket name", () => {
assert.equal(linkKeyId("a".repeat(63)).length, 20);
assert.notEqual(linkKeyId("brother"), linkKeyId("mesh-mail"));
});
test("the link key id is the one the object-store tool derives (cmd/object-store server_test.go holds the same literal)", () => {
// 1516/AC1
assert.equal(linkKeyId("brother"), "link0b4bd77cec705cf5");
});