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
+64 -19
View File
@@ -61,7 +61,9 @@ function parseCapabilities(
"platform:adopt",
]);
if (!capabilities.length || capabilities.some((item) => !allowed.has(item)))
throw new Error("Provide at least one valid capability");
throw new Error(
"Provide valid capabilities (for example: --capabilities kubernetes:read,kubernetes:write; choices: kubernetes:read, kubernetes:write, kubernetes:exec, users:read, users:write, sessions:revoke, platform:adopt)",
);
return [...new Set(capabilities)] as CreateApiKeyRequest["capabilities"];
}
@@ -202,22 +204,37 @@ function renderApiKeys(keys: ApiKey[]): string {
return toTable(
keys.map((key) => ({
id: key.id,
username: key.username,
capabilities: key.capabilities.join(","),
workspace: key.workspace ?? "",
expires: key.expiresAt,
expires: key.expiresAt ?? "never",
disabled: key.disabled ? "yes" : "no",
})),
);
}
export async function listApiKeys(
username: string,
username?: string,
request: UsersApiRequest = apiRequest,
): Promise<string> {
const response = await request<ListApiKeysResponse>(
`/users/${encodeURIComponent(username)}/keys`,
const usernames = username
? [username]
: (await request<ListUsersResponse>("/users")).items.map(
(user) => user.username,
);
const results = await Promise.all(
usernames.map((name) =>
request<ListApiKeysResponse>(`/users/${encodeURIComponent(name)}/keys`),
),
);
return renderApiKeys(
results
.flatMap((result) => result.items)
.sort(
(a, b) =>
a.username.localeCompare(b.username) || a.id.localeCompare(b.id),
),
);
return renderApiKeys(response.items);
}
export async function createApiKey(
@@ -225,6 +242,11 @@ export async function createApiKey(
body: CreateApiKeyRequest,
request: UsersApiRequest = apiRequest,
): Promise<CreateApiKeyResponse> {
const user = await request<UserResponse>(
`/users/${encodeURIComponent(username)}`,
);
if (user.disabled)
throw new Error(`Cannot create API key: user '${username}' is inactive`);
return request<CreateApiKeyResponse>(
`/users/${encodeURIComponent(username)}/keys`,
{ method: "POST", json: body },
@@ -318,39 +340,62 @@ const keys = defineCommand({
meta: { name: "keys", description: "Manage user API keys" },
subCommands: {
ls: defineCommand({
meta: { name: "ls", description: "List a user's API keys" },
meta: {
name: "ls",
description: "List all API keys (optional positional username filters the results)",
},
async run({ args }) {
console.log(await listApiKeys(requireUsername(args._[0])));
console.log(await listApiKeys(args._[0]));
},
}),
create: defineCommand({
meta: { name: "create", description: "Create an API key" },
meta: {
name: "create",
description:
"Create an API key for a user (e.g. kuber users keys create alice --capabilities kubernetes:read --expires-days none)",
},
args: {
username: {
type: "positional",
required: true,
description: "Username to own the API key",
},
capabilities: {
type: "string",
required: true,
description: "Comma-separated capabilities",
description:
"Comma-separated capabilities, e.g. kubernetes:read,kubernetes:write (also kubernetes:exec, users:read, users:write, sessions:revoke, platform:adopt)",
},
workspace: {
type: "string",
description: "Restrict the key to a workspace",
},
expiresDays: {
"expires-days": {
type: "string",
default: "90",
description: "Expiry in days (1-365)",
description: "Expiry in days (1-365), or none for a non-expiring key",
},
},
async run({ args }) {
const days = Number(args.expiresDays);
if (!Number.isSafeInteger(days) || days < 1 || days > 365)
throw new Error("--expires-days must be an integer from 1 to 365");
const key = await createApiKey(requireUsername(args._[0]), {
const days =
args["expires-days"] === "none"
? undefined
: Number(args["expires-days"]);
if (
days !== undefined &&
(!Number.isSafeInteger(days) || days < 1 || days > 365)
)
throw new Error(
"--expires-days must be an integer from 1 to 365, or none",
);
const key = await createApiKey(requireUsername(args.username), {
capabilities: parseCapabilities(args.capabilities),
...(args.workspace && { workspace: args.workspace }),
expiresAt: new Date(
Date.now() + days * 24 * 60 * 60 * 1000,
).toISOString(),
...(days !== undefined && {
expiresAt: new Date(
Date.now() + days * 24 * 60 * 60 * 1000,
).toISOString(),
}),
});
console.log(
"Store this API key securely now. It will not be shown again:",