feat: improve API keys and build workflows
This commit is contained in:
+64
-19
@@ -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:",
|
||||
|
||||
Reference in New Issue
Block a user