118 lines
5.4 KiB
TypeScript
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");
|
|
});
|