feat: improve API keys and build workflows

This commit is contained in:
2026-10-04 20:05:13 +00:00 Unverified
parent 62f2362a2e
commit e4623efe86
27 changed files with 2080 additions and 235 deletions
+83 -8
View File
@@ -136,9 +136,27 @@ describe("API keys", () => {
});
expect(differentWorkspace.status).toBe(403);
const noExpiry = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
});
expect(noExpiry.status).toBe(403);
expect((await noExpiry.json()) as { code: string }).toHaveProperty(
"code",
"API_KEY_DELEGATION_FORBIDDEN",
);
const laterExpiry = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-06T00:00:00.000Z",
});
expect(laterExpiry.status).toBe(403);
const subset = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
});
expect(subset.status).toBe(201);
expect(
@@ -168,7 +186,7 @@ describe("API keys", () => {
event.spec.action === "api_key.create" &&
event.spec.outcome === "denied",
),
).toHaveLength(4);
).toHaveLength(6);
expect(JSON.stringify(denied)).not.toContain("delegation-parent");
await store.createApiKey({
@@ -184,7 +202,10 @@ describe("API keys", () => {
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt: "2026-09-20T00:00:00.000Z",
}),
},
"unscoped-delegation",
),
@@ -193,6 +214,33 @@ describe("API keys", () => {
expect(await unscopedSubset.json()).not.toHaveProperty("workspace");
});
test("allows a non-expiring parent to delegate a non-expiring child", async () => {
const { app, store } = await setup();
await store.createApiKey({
id: "key_nonexpiring_parent",
tokenHash: hashToken("nonexpiring-parent"),
username: "ci",
capabilities: ["users:write", "kubernetes:read"],
});
const created = await app(
request(
"/api/v2/users/ci/keys",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
},
"nonexpiring-parent",
),
);
expect(created.status).toBe(201);
const child = (await created.json()) as { token: string };
expect(child).not.toHaveProperty("expiresAt");
expect((await app(request("/api/v2/me", {}, child.token))).status).toBe(
200,
);
});
test("rejects expired keys and expiry longer than 365 days", async () => {
const { app, store } = await setup();
await store.createApiKey({
@@ -221,7 +269,7 @@ describe("API keys", () => {
).toBe(400);
});
test("uses the default expiry, cleans up expired keys, and does not log keys out", async () => {
test("preserves non-expiring keys while cleaning up finite keys and sessions", async () => {
const { app, store } = await setup();
const created = await app(
request("/api/v2/users/ci/keys", {
@@ -230,17 +278,44 @@ describe("API keys", () => {
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
}),
);
const key = (await created.json()) as { token: string; expiresAt: string };
expect(key.expiresAt).toBe("2026-12-04T00:00:00.000Z");
expect(created.status).toBe(201);
const key = (await created.json()) as { token: string };
expect(key).not.toHaveProperty("expiresAt");
const listed = await app(request("/api/v2/users/ci/keys"));
const { items } = (await listed.json()) as { items: object[] };
expect(items).toHaveLength(1);
expect(items[0]).not.toHaveProperty("expiresAt");
const finite = await app(
request("/api/v2/users/ci/keys", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt: "2026-10-05T00:00:00.000Z",
}),
}),
);
expect(finite.status).toBe(201);
const finiteKey = (await finite.json()) as {
token: string;
expiresAt: string;
};
expect(finiteKey.expiresAt).toBe("2026-10-05T00:00:00.000Z");
expect(
(await app(request("/api/v2/logout", { method: "POST" }, key.token)))
.status,
).toBe(204);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(200);
expect(await cleanupExpiredSessions(store, Date.parse(key.expiresAt))).toBe(
2,
expect((await app(request("/api/v2/me", {}, finiteKey.token))).status).toBe(
200,
);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(401);
expect(
await cleanupExpiredSessions(store, Date.parse(finiteKey.expiresAt)),
).toBe(2);
expect((await app(request("/api/v2/me", {}, finiteKey.token))).status).toBe(
401,
);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(200);
});
test("denies a workspace-scoped key outside its workspace", async () => {