feat: improve API keys and build workflows
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user