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
+157 -1
View File
@@ -12,6 +12,7 @@ import {
revokeUserSessions,
setUserDisabled,
updateUser,
users,
} from "../../command/users";
import type { ApiRequestInit } from "../../lib/api";
@@ -130,7 +131,10 @@ describe("user administration commands", () => {
await createApiKey(
"alice",
{ capabilities: ["kubernetes:write"] },
requestReturning(key, calls),
async <T>(path: string, init?: ApiRequestInit): Promise<T> => {
calls.push({ path, init });
return (init ? key : user) as T;
},
);
expect(
await revokeApiKey(
@@ -148,6 +152,7 @@ describe("user administration commands", () => {
);
expect(calls).toEqual([
{ path: "/users/alice%2Fexample/keys", init: undefined },
{ path: "/users/alice", init: undefined },
{
path: "/users/alice/keys",
init: { method: "POST", json: { capabilities: ["kubernetes:write"] } },
@@ -158,6 +163,157 @@ describe("user administration commands", () => {
},
]);
});
test("lists all users' keys without a username and filters with one", async () => {
const calls: Call[] = [];
const request = async <T>(
path: string,
init?: ApiRequestInit,
): Promise<T> => {
calls.push({ path, init });
if (path === "/users")
return { items: [user, { ...user, username: "bob" }] } as T;
return {
items: [
{
id: `${path.includes("bob") ? "bob" : "alice"}-key`,
username: path.includes("bob") ? "bob" : "alice",
capabilities: ["kubernetes:read"],
disabled: false,
},
],
} as T;
};
const all = await listApiKeys(undefined, request);
expect(all).toContain("alice-key");
expect(all).toContain("bob-key");
expect(all).toContain("never");
expect(calls.map((call) => call.path)).toEqual([
"/users",
"/users/alice/keys",
"/users/bob/keys",
]);
calls.length = 0;
expect(await listApiKeys("alice", request)).not.toContain("bob-key");
expect(calls.map((call) => call.path)).toEqual(["/users/alice/keys"]);
});
test("key creation requires an existing active user", async () => {
const calls: Call[] = [];
const body = { capabilities: ["kubernetes:read" as const] };
await expect(
createApiKey("missing", body, async (path) => {
calls.push({ path });
throw new Error("User 'missing' not found");
}),
).rejects.toThrow("User 'missing' not found");
expect(calls).toEqual([{ path: "/users/missing" }]);
await expect(
createApiKey(
"alice",
body,
requestReturning({ ...user, disabled: true }, calls),
),
).rejects.toThrow("user 'alice' is inactive");
expect(calls.at(-1)?.path).toBe("/users/alice");
});
test("creates non-expiring and finite API keys with the requested POST bodies", async () => {
const calls: Call[] = [];
const key = {
id: "key-identifier-123",
username: "alice",
capabilities: ["kubernetes:read"] as const,
disabled: false,
token: "shown-once-token",
};
const request = async <T>(
path: string,
init?: ApiRequestInit,
): Promise<T> => {
calls.push({ path, init });
return (init ? key : user) as T;
};
await createApiKey(
"alice",
{ capabilities: ["kubernetes:read"], workspace: "team/shop" },
request,
);
const expiresAt = "2026-12-01T00:00:00.000Z";
await createApiKey(
"alice",
{
capabilities: ["kubernetes:read"],
workspace: "team/shop",
expiresAt,
},
request,
);
expect(calls).toEqual([
{ path: "/users/alice", init: undefined },
{
path: "/users/alice/keys",
init: {
method: "POST",
json: { capabilities: ["kubernetes:read"], workspace: "team/shop" },
},
},
{ path: "/users/alice", init: undefined },
{
path: "/users/alice/keys",
init: {
method: "POST",
json: {
capabilities: ["kubernetes:read"],
workspace: "team/shop",
expiresAt,
},
},
},
]);
expect(calls[1]?.init?.json).not.toHaveProperty("expiresAt");
});
test("key command help declares username, capability examples, and non-expiry", async () => {
const commands = (users.subCommands as Record<string, any>)?.keys;
if (!commands || typeof commands === "function")
throw new Error("Missing keys command");
const subCommands = await Promise.resolve(commands.subCommands);
const create = subCommands?.create;
const ls = subCommands?.ls;
if (
!create ||
typeof create === "function" ||
!ls ||
typeof ls === "function"
)
throw new Error("Missing key subcommands");
expect(create.args?.username).toMatchObject({
type: "positional",
required: true,
});
expect(create.args?.capabilities?.description).toContain(
"kubernetes:read,kubernetes:write",
);
expect(create.args?.["expires-days"]?.description).toContain("none");
expect(ls.meta?.description).toContain("optional positional username");
await expect(
create.run?.({
args: { username: "alice", capabilities: "invalid", "expires-days": "90" },
} as never),
).rejects.toThrow("kubernetes:read,kubernetes:write");
await expect(
create.run?.({
args: {
username: "alice",
capabilities: "kubernetes:read",
"expires-days": "NaN",
},
} as never),
).rejects.toThrow("1 to 365, or none");
});
});
const operation = {