Update: Refactored commands into their own source files.
This commit is contained in:
89
src/cli.ts
89
src/cli.ts
@@ -4,6 +4,15 @@
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { runCommand } from "./cli/commands.ts";
|
||||
import { usageGetToken } from "./cli/commands/get-token.ts";
|
||||
import { usageListAppGrants } from "./cli/commands/list-app-grants.ts";
|
||||
import { usageListAppPermissions } from "./cli/commands/list-app-permissions.ts";
|
||||
import { usageListApps } from "./cli/commands/list-apps.ts";
|
||||
import { usageListResourcePermissions } from "./cli/commands/list-resource-permissions.ts";
|
||||
import { usageLogin } from "./cli/commands/login.ts";
|
||||
import { usageLogout } from "./cli/commands/logout.ts";
|
||||
import { usageRest } from "./cli/commands/rest.ts";
|
||||
import { usageTable } from "./cli/commands/table.ts";
|
||||
import {
|
||||
normalizeOutputFormat,
|
||||
omitPermissionGuidColumns,
|
||||
@@ -57,86 +66,6 @@ Use: sk-az-tools --help <command>
|
||||
or: sk-az-tools <command> --help`;
|
||||
}
|
||||
|
||||
function usageListApps(): string {
|
||||
return `Usage: sk-az-tools list-apps [--display-name|-n <name>] [--app-id|-i <appId>] [--filter|-f <glob>] [global options]
|
||||
|
||||
Options:
|
||||
-n, --display-name <name> Get app by name
|
||||
-i, --app-id <appId> Get app by id
|
||||
-f, --filter <glob> Filter by app display name glob`;
|
||||
}
|
||||
|
||||
function usageLogin(): string {
|
||||
return `Usage: sk-az-tools login [--resources <csv>] [--use-device-code] [--no-browser] [--browser <name>] [--browser-profile <profile>] [global options]
|
||||
|
||||
Options:
|
||||
--resources <csv> Comma-separated resources: graph,devops,arm (default: all)
|
||||
--use-device-code Use device code flow instead of interactive flow
|
||||
--no-browser Do not launch browser; print interactive URL to stderr
|
||||
--browser <name> Browser keyword: brave|browser|browserPrivate|chrome|edge|firefox
|
||||
--browser-profile <name> Chromium profile name (e.g. Default, "Profile 1")`;
|
||||
}
|
||||
|
||||
function usageLogout(): string {
|
||||
return `Usage: sk-az-tools logout [--all] [global options]
|
||||
|
||||
Options:
|
||||
--all Clear login state and remove all cached accounts`;
|
||||
}
|
||||
|
||||
function usageGetToken(): string {
|
||||
return `Usage: sk-az-tools get-token --type|-t <azurerm|devops> [global options]
|
||||
|
||||
Options:
|
||||
-t, --type <value> Token type: azurerm|devops`;
|
||||
}
|
||||
|
||||
function usageRest(): string {
|
||||
return `Usage: sk-az-tools rest [--method <httpMethod>] --url <url> [--header <name: value>] [global options]
|
||||
|
||||
Options:
|
||||
--method <httpMethod> HTTP method (default: GET; examples: GET, POST, PATCH, DELETE)
|
||||
--url <url> Full URL to call
|
||||
--header <name: value> Extra request header; example: "Content-Type: application/json"
|
||||
|
||||
Authorization is added automatically for:
|
||||
management.azure.com Uses azurerm token
|
||||
dev.azure.com Uses devops token`;
|
||||
}
|
||||
|
||||
function usageListAppPermissions(): string {
|
||||
return `Usage: sk-az-tools list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s] [--filter|-f <glob>] [global options]
|
||||
|
||||
Options:
|
||||
-i, --app-id <appId> Application (client) ID (required)
|
||||
-r, --resolve Resolve permission GUIDs to human-readable values
|
||||
-s, --short Makes output more compact
|
||||
-f, --filter <glob> Filter by permission name glob`;
|
||||
}
|
||||
|
||||
function usageListAppGrants(): string {
|
||||
return `Usage: sk-az-tools list-app-grants --app-id|-i <appId> [global options]
|
||||
|
||||
Options:
|
||||
-i, --app-id <appId> Application (client) ID (required)`;
|
||||
}
|
||||
|
||||
function usageListResourcePermissions(): string {
|
||||
return `Usage: sk-az-tools list-resource-permissions [--app-id|-i <appId> | --display-name|-n <name>] [--filter|-f <glob>] [global options]
|
||||
|
||||
Options:
|
||||
-i, --app-id <appId> Resource app ID
|
||||
-n, --display-name <name> Resource app display name
|
||||
-f, --filter <glob> Filter by permission name glob`;
|
||||
}
|
||||
|
||||
function usageTable(): string {
|
||||
return `Usage: sk-az-tools table [--header|-H <spec|auto|a|original|o>] [global options]
|
||||
|
||||
Options:
|
||||
-H, --header <value> Header mode/spec: auto|a (default), original|o, OR "col1, col2" OR "key1: Label 1, key2: Label 2"`;
|
||||
}
|
||||
|
||||
function usageCommand(command: string): string {
|
||||
switch (command) {
|
||||
case "login":
|
||||
|
||||
@@ -1,303 +1,16 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { minimatch } from "minimatch";
|
||||
import { runGetTokenCommand } from "./commands/get-token.ts";
|
||||
import { runListAppGrantsCommand } from "./commands/list-app-grants.ts";
|
||||
import { runListAppPermissionsCommand } from "./commands/list-app-permissions.ts";
|
||||
import { runListAppsCommand } from "./commands/list-apps.ts";
|
||||
import { runListResourcePermissionsCommand } from "./commands/list-resource-permissions.ts";
|
||||
import { runLoginCommand } from "./commands/login.ts";
|
||||
import { runLogoutCommand } from "./commands/logout.ts";
|
||||
import { runRestCommand } from "./commands/rest.ts";
|
||||
import { runTableCommand } from "./commands/table.ts";
|
||||
|
||||
import { loadPublicConfig } from "../index.ts";
|
||||
import { getGraphClient } from "../graph/auth.ts";
|
||||
import { acquireResourceTokenFromLogin, login, logout } from "../azure/index.ts";
|
||||
import { getDevOpsApiToken } from "../devops/index.ts";
|
||||
import {
|
||||
listApps,
|
||||
listAppPermissions,
|
||||
listAppPermissionsResolved,
|
||||
listAppGrants,
|
||||
listResourcePermissions,
|
||||
} from "../graph/app.ts";
|
||||
import { readJsonFromStdin } from "./utils.ts";
|
||||
|
||||
type CommandValues = {
|
||||
[key: string]: string | boolean | undefined;
|
||||
type?: string;
|
||||
method?: string;
|
||||
url?: string;
|
||||
header?: string;
|
||||
resources?: string;
|
||||
"use-device-code"?: boolean;
|
||||
"no-browser"?: boolean;
|
||||
browser?: string;
|
||||
"browser-profile"?: string;
|
||||
all?: boolean;
|
||||
"display-name"?: string;
|
||||
"app-id"?: string;
|
||||
filter?: string;
|
||||
resolve?: boolean;
|
||||
};
|
||||
|
||||
type PermissionRow = {
|
||||
permissionValue?: string | null;
|
||||
permissionDisplayName?: string | null;
|
||||
};
|
||||
|
||||
type DisplayNameRow = {
|
||||
displayName?: string | null;
|
||||
};
|
||||
|
||||
function parseHeaderLine(header?: string): { name: string; value: string } | null {
|
||||
if (!header || header.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const separatorIndex = header.indexOf(":");
|
||||
if (separatorIndex < 1) {
|
||||
throw new Error("--header must be in the format 'Name: Value'");
|
||||
}
|
||||
|
||||
const name = header.slice(0, separatorIndex).trim();
|
||||
const value = header.slice(separatorIndex + 1).trim();
|
||||
if (!name || !value) {
|
||||
throw new Error("--header must be in the format 'Name: Value'");
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
function hasAuthorizationHeader(headers: Headers): boolean {
|
||||
for (const headerName of headers.keys()) {
|
||||
if (headerName.toLowerCase() === "authorization") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
|
||||
const host = url.hostname.toLowerCase();
|
||||
if (host !== "management.azure.com" && host !== "dev.azure.com") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = await loadPublicConfig();
|
||||
if (!config.tenantId) {
|
||||
throw new Error("tenantId is required");
|
||||
}
|
||||
if (!config.clientId) {
|
||||
throw new Error("clientId is required");
|
||||
}
|
||||
|
||||
if (host === "management.azure.com") {
|
||||
const result = await acquireResourceTokenFromLogin({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
resource: "arm",
|
||||
});
|
||||
const accessToken = result?.accessToken;
|
||||
if (!accessToken) {
|
||||
throw new Error("Failed to obtain AzureRM token");
|
||||
}
|
||||
return `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
||||
return `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
function filterByPermissionName<T extends PermissionRow>(rows: T[], pattern: string): T[] {
|
||||
return rows.filter((item) =>
|
||||
minimatch(item.permissionValue ?? "", pattern, { nocase: true })
|
||||
|| minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }),
|
||||
);
|
||||
}
|
||||
|
||||
function filterByDisplayName<T extends DisplayNameRow>(rows: T[], pattern: string): T[] {
|
||||
return rows.filter((item) =>
|
||||
minimatch(item.displayName ?? "", pattern, { nocase: true }),
|
||||
);
|
||||
}
|
||||
|
||||
async function getGraphClientFromPublicConfig(): Promise<{ client: any }> {
|
||||
const config = await loadPublicConfig();
|
||||
return getGraphClient({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
});
|
||||
}
|
||||
|
||||
async function runTableCommand(): Promise<unknown> {
|
||||
return readJsonFromStdin();
|
||||
}
|
||||
|
||||
async function runLoginCommand(values: CommandValues): Promise<unknown> {
|
||||
const config = await loadPublicConfig();
|
||||
return login({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
resourcesCsv: values.resources,
|
||||
useDeviceCode: Boolean(values["use-device-code"]),
|
||||
noBrowser: Boolean(values["no-browser"]),
|
||||
browser: values.browser,
|
||||
browserProfile: values["browser-profile"],
|
||||
});
|
||||
}
|
||||
|
||||
async function runLogoutCommand(values: CommandValues): Promise<unknown> {
|
||||
const config = await loadPublicConfig();
|
||||
return logout({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
clearAll: Boolean(values.all),
|
||||
});
|
||||
}
|
||||
|
||||
async function runListAppsCommand(values: CommandValues): Promise<unknown> {
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
let result = await listApps(client, {
|
||||
displayName: values["display-name"],
|
||||
appId: values["app-id"],
|
||||
});
|
||||
if (values["app-id"] && result.length > 1) {
|
||||
throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`);
|
||||
}
|
||||
if (values.filter) {
|
||||
result = filterByDisplayName(result, values.filter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function runListAppPermissionsCommand(values: CommandValues): Promise<unknown> {
|
||||
if (!values["app-id"]) {
|
||||
throw new Error("--app-id is required for list-app-permissions");
|
||||
}
|
||||
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
let result = values.resolve || values.filter
|
||||
? await listAppPermissionsResolved(client, values["app-id"])
|
||||
: await listAppPermissions(client, values["app-id"]);
|
||||
if (values.filter) {
|
||||
result = filterByPermissionName(result, values.filter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function runListAppGrantsCommand(values: CommandValues): Promise<unknown> {
|
||||
if (!values["app-id"]) {
|
||||
throw new Error("--app-id is required for list-app-grants");
|
||||
}
|
||||
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
return listAppGrants(client, values["app-id"]);
|
||||
}
|
||||
|
||||
async function runListResourcePermissionsCommand(values: CommandValues): Promise<unknown> {
|
||||
if (!values["app-id"] && !values["display-name"]) {
|
||||
throw new Error("--app-id or --display-name is required for list-resource-permissions");
|
||||
}
|
||||
if (values["app-id"] && values["display-name"]) {
|
||||
throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both");
|
||||
}
|
||||
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
let result = await listResourcePermissions(client, {
|
||||
appId: values["app-id"],
|
||||
displayName: values["display-name"],
|
||||
});
|
||||
if (values.filter) {
|
||||
result = filterByPermissionName(result, values.filter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function runGetTokenCommand(values: CommandValues): Promise<unknown> {
|
||||
const tokenType = (values.type ?? "").toString().trim().toLowerCase();
|
||||
if (!tokenType) {
|
||||
throw new Error("--type is required for get-token (allowed: azurerm, devops)");
|
||||
}
|
||||
|
||||
const config = await loadPublicConfig();
|
||||
if (!config.tenantId) {
|
||||
throw new Error("tenantId is required");
|
||||
}
|
||||
if (!config.clientId) {
|
||||
throw new Error("clientId is required");
|
||||
}
|
||||
|
||||
if (tokenType === "azurerm") {
|
||||
const result = await acquireResourceTokenFromLogin({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
resource: "arm",
|
||||
});
|
||||
|
||||
const accessToken = result?.accessToken;
|
||||
if (!accessToken) {
|
||||
throw new Error("Failed to obtain AzureRM token");
|
||||
}
|
||||
|
||||
return {
|
||||
tokenType,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
if (tokenType === "devops") {
|
||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
||||
return {
|
||||
tokenType,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Invalid --type '${values.type}'. Allowed: azurerm, devops`);
|
||||
}
|
||||
|
||||
async function runRestCommand(values: CommandValues): Promise<unknown> {
|
||||
const method = (values.method ?? "GET").toString().trim().toUpperCase() || "GET";
|
||||
const urlValue = (values.url ?? "").toString().trim();
|
||||
|
||||
if (!urlValue) {
|
||||
throw new Error("--url is required for rest");
|
||||
}
|
||||
|
||||
let targetUrl: URL;
|
||||
try {
|
||||
targetUrl = new URL(urlValue);
|
||||
} catch {
|
||||
throw new Error(`Invalid --url '${urlValue}'`);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
const customHeader = parseHeaderLine(values.header);
|
||||
if (customHeader) {
|
||||
headers.set(customHeader.name, customHeader.value);
|
||||
}
|
||||
|
||||
if (!hasAuthorizationHeader(headers)) {
|
||||
const authorization = await getAutoAuthorizationHeader(targetUrl);
|
||||
if (authorization) {
|
||||
headers.set("Authorization", authorization);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
});
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
let body: unknown;
|
||||
if (contentType.toLowerCase().includes("application/json")) {
|
||||
body = await response.json();
|
||||
} else {
|
||||
body = await response.text();
|
||||
}
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body,
|
||||
};
|
||||
}
|
||||
import type { CommandValues } from "./commands/types.ts";
|
||||
|
||||
export async function runCommand(command: string, values: CommandValues): Promise<unknown> {
|
||||
switch (command) {
|
||||
|
||||
57
src/cli/commands/get-token.ts
Normal file
57
src/cli/commands/get-token.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { acquireResourceTokenFromLogin } from "../../azure/index.ts";
|
||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||
import { loadPublicConfig } from "../../index.ts";
|
||||
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageGetToken(): string {
|
||||
return `Usage: sk-az-tools get-token --type|-t <azurerm|devops> [global options]
|
||||
|
||||
Options:
|
||||
-t, --type <value> Token type: azurerm|devops`;
|
||||
}
|
||||
|
||||
export async function runGetTokenCommand(values: CommandValues): Promise<unknown> {
|
||||
const tokenType = (values.type ?? "").toString().trim().toLowerCase();
|
||||
if (!tokenType) {
|
||||
throw new Error("--type is required for get-token (allowed: azurerm, devops)");
|
||||
}
|
||||
|
||||
const config = await loadPublicConfig();
|
||||
if (!config.tenantId) {
|
||||
throw new Error("tenantId is required");
|
||||
}
|
||||
if (!config.clientId) {
|
||||
throw new Error("clientId is required");
|
||||
}
|
||||
|
||||
if (tokenType === "azurerm") {
|
||||
const result = await acquireResourceTokenFromLogin({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
resource: "arm",
|
||||
});
|
||||
|
||||
const accessToken = result?.accessToken;
|
||||
if (!accessToken) {
|
||||
throw new Error("Failed to obtain AzureRM token");
|
||||
}
|
||||
|
||||
return {
|
||||
tokenType,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
if (tokenType === "devops") {
|
||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
||||
return {
|
||||
tokenType,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Invalid --type '${values.type}'. Allowed: azurerm, devops`);
|
||||
}
|
||||
22
src/cli/commands/list-app-grants.ts
Normal file
22
src/cli/commands/list-app-grants.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { listAppGrants } from "../../graph/app.ts";
|
||||
|
||||
import { getGraphClientFromPublicConfig } from "./shared.ts";
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageListAppGrants(): string {
|
||||
return `Usage: sk-az-tools list-app-grants --app-id|-i <appId> [global options]
|
||||
|
||||
Options:
|
||||
-i, --app-id <appId> Application (client) ID (required)`;
|
||||
}
|
||||
|
||||
export async function runListAppGrantsCommand(values: CommandValues): Promise<unknown> {
|
||||
if (!values["app-id"]) {
|
||||
throw new Error("--app-id is required for list-app-grants");
|
||||
}
|
||||
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
return listAppGrants(client, values["app-id"]);
|
||||
}
|
||||
31
src/cli/commands/list-app-permissions.ts
Normal file
31
src/cli/commands/list-app-permissions.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { listAppPermissions, listAppPermissionsResolved } from "../../graph/app.ts";
|
||||
|
||||
import { filterByPermissionName, getGraphClientFromPublicConfig } from "./shared.ts";
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageListAppPermissions(): string {
|
||||
return `Usage: sk-az-tools list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s] [--filter|-f <glob>] [global options]
|
||||
|
||||
Options:
|
||||
-i, --app-id <appId> Application (client) ID (required)
|
||||
-r, --resolve Resolve permission GUIDs to human-readable values
|
||||
-s, --short Makes output more compact
|
||||
-f, --filter <glob> Filter by permission name glob`;
|
||||
}
|
||||
|
||||
export async function runListAppPermissionsCommand(values: CommandValues): Promise<unknown> {
|
||||
if (!values["app-id"]) {
|
||||
throw new Error("--app-id is required for list-app-permissions");
|
||||
}
|
||||
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
let result = values.resolve || values.filter
|
||||
? await listAppPermissionsResolved(client, values["app-id"])
|
||||
: await listAppPermissions(client, values["app-id"]);
|
||||
if (values.filter) {
|
||||
result = filterByPermissionName(result, values.filter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
30
src/cli/commands/list-apps.ts
Normal file
30
src/cli/commands/list-apps.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { listApps } from "../../graph/app.ts";
|
||||
|
||||
import { filterByDisplayName, getGraphClientFromPublicConfig } from "./shared.ts";
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageListApps(): string {
|
||||
return `Usage: sk-az-tools list-apps [--display-name|-n <name>] [--app-id|-i <appId>] [--filter|-f <glob>] [global options]
|
||||
|
||||
Options:
|
||||
-n, --display-name <name> Get app by name
|
||||
-i, --app-id <appId> Get app by id
|
||||
-f, --filter <glob> Filter by app display name glob`;
|
||||
}
|
||||
|
||||
export async function runListAppsCommand(values: CommandValues): Promise<unknown> {
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
let result = await listApps(client, {
|
||||
displayName: values["display-name"],
|
||||
appId: values["app-id"],
|
||||
});
|
||||
if (values["app-id"] && result.length > 1) {
|
||||
throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`);
|
||||
}
|
||||
if (values.filter) {
|
||||
result = filterByDisplayName(result, values.filter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
34
src/cli/commands/list-resource-permissions.ts
Normal file
34
src/cli/commands/list-resource-permissions.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { listResourcePermissions } from "../../graph/app.ts";
|
||||
|
||||
import { filterByPermissionName, getGraphClientFromPublicConfig } from "./shared.ts";
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageListResourcePermissions(): string {
|
||||
return `Usage: sk-az-tools list-resource-permissions [--app-id|-i <appId> | --display-name|-n <name>] [--filter|-f <glob>] [global options]
|
||||
|
||||
Options:
|
||||
-i, --app-id <appId> Resource app ID
|
||||
-n, --display-name <name> Resource app display name
|
||||
-f, --filter <glob> Filter by permission name glob`;
|
||||
}
|
||||
|
||||
export async function runListResourcePermissionsCommand(values: CommandValues): Promise<unknown> {
|
||||
if (!values["app-id"] && !values["display-name"]) {
|
||||
throw new Error("--app-id or --display-name is required for list-resource-permissions");
|
||||
}
|
||||
if (values["app-id"] && values["display-name"]) {
|
||||
throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both");
|
||||
}
|
||||
|
||||
const { client } = await getGraphClientFromPublicConfig();
|
||||
let result = await listResourcePermissions(client, {
|
||||
appId: values["app-id"],
|
||||
displayName: values["display-name"],
|
||||
});
|
||||
if (values.filter) {
|
||||
result = filterByPermissionName(result, values.filter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
30
src/cli/commands/login.ts
Normal file
30
src/cli/commands/login.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { login } from "../../azure/index.ts";
|
||||
import { loadPublicConfig } from "../../index.ts";
|
||||
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageLogin(): string {
|
||||
return `Usage: sk-az-tools login [--resources <csv>] [--use-device-code] [--no-browser] [--browser <name>] [--browser-profile <profile>] [global options]
|
||||
|
||||
Options:
|
||||
--resources <csv> Comma-separated resources: graph,devops,arm (default: all)
|
||||
--use-device-code Use device code flow instead of interactive flow
|
||||
--no-browser Do not launch browser; print interactive URL to stderr
|
||||
--browser <name> Browser keyword: brave|browser|browserPrivate|chrome|edge|firefox
|
||||
--browser-profile <name> Chromium profile name (e.g. Default, "Profile 1")`;
|
||||
}
|
||||
|
||||
export async function runLoginCommand(values: CommandValues): Promise<unknown> {
|
||||
const config = await loadPublicConfig();
|
||||
return login({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
resourcesCsv: values.resources,
|
||||
useDeviceCode: Boolean(values["use-device-code"]),
|
||||
noBrowser: Boolean(values["no-browser"]),
|
||||
browser: values.browser,
|
||||
browserProfile: values["browser-profile"],
|
||||
});
|
||||
}
|
||||
22
src/cli/commands/logout.ts
Normal file
22
src/cli/commands/logout.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { logout } from "../../azure/index.ts";
|
||||
import { loadPublicConfig } from "../../index.ts";
|
||||
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageLogout(): string {
|
||||
return `Usage: sk-az-tools logout [--all] [global options]
|
||||
|
||||
Options:
|
||||
--all Clear login state and remove all cached accounts`;
|
||||
}
|
||||
|
||||
export async function runLogoutCommand(values: CommandValues): Promise<unknown> {
|
||||
const config = await loadPublicConfig();
|
||||
return logout({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
clearAll: Boolean(values.all),
|
||||
});
|
||||
}
|
||||
129
src/cli/commands/rest.ts
Normal file
129
src/cli/commands/rest.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { acquireResourceTokenFromLogin } from "../../azure/index.ts";
|
||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||
import { loadPublicConfig } from "../../index.ts";
|
||||
|
||||
import type { CommandValues } from "./types.ts";
|
||||
|
||||
export function usageRest(): string {
|
||||
return `Usage: sk-az-tools rest [--method <httpMethod>] --url <url> [--header <name: value>] [global options]
|
||||
|
||||
Options:
|
||||
--method <httpMethod> HTTP method (default: GET; examples: GET, POST, PATCH, DELETE)
|
||||
--url <url> Full URL to call
|
||||
--header <name: value> Extra request header; example: "Content-Type: application/json"
|
||||
|
||||
Authorization is added automatically for:
|
||||
management.azure.com Uses azurerm token
|
||||
dev.azure.com Uses devops token`;
|
||||
}
|
||||
|
||||
function parseHeaderLine(header?: string): { name: string; value: string } | null {
|
||||
if (!header || header.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const separatorIndex = header.indexOf(":");
|
||||
if (separatorIndex < 1) {
|
||||
throw new Error("--header must be in the format 'Name: Value'");
|
||||
}
|
||||
|
||||
const name = header.slice(0, separatorIndex).trim();
|
||||
const value = header.slice(separatorIndex + 1).trim();
|
||||
if (!name || !value) {
|
||||
throw new Error("--header must be in the format 'Name: Value'");
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
function hasAuthorizationHeader(headers: Headers): boolean {
|
||||
for (const headerName of headers.keys()) {
|
||||
if (headerName.toLowerCase() === "authorization") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
|
||||
const host = url.hostname.toLowerCase();
|
||||
if (host !== "management.azure.com" && host !== "dev.azure.com") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = await loadPublicConfig();
|
||||
if (!config.tenantId) {
|
||||
throw new Error("tenantId is required");
|
||||
}
|
||||
if (!config.clientId) {
|
||||
throw new Error("clientId is required");
|
||||
}
|
||||
|
||||
if (host === "management.azure.com") {
|
||||
const result = await acquireResourceTokenFromLogin({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
resource: "arm",
|
||||
});
|
||||
const accessToken = result?.accessToken;
|
||||
if (!accessToken) {
|
||||
throw new Error("Failed to obtain AzureRM token");
|
||||
}
|
||||
return `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
||||
return `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
export async function runRestCommand(values: CommandValues): Promise<unknown> {
|
||||
const method = (values.method ?? "GET").toString().trim().toUpperCase() || "GET";
|
||||
const urlValue = (values.url ?? "").toString().trim();
|
||||
|
||||
if (!urlValue) {
|
||||
throw new Error("--url is required for rest");
|
||||
}
|
||||
|
||||
let targetUrl: URL;
|
||||
try {
|
||||
targetUrl = new URL(urlValue);
|
||||
} catch {
|
||||
throw new Error(`Invalid --url '${urlValue}'`);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
const customHeader = parseHeaderLine(values.header);
|
||||
if (customHeader) {
|
||||
headers.set(customHeader.name, customHeader.value);
|
||||
}
|
||||
|
||||
if (!hasAuthorizationHeader(headers)) {
|
||||
const authorization = await getAutoAuthorizationHeader(targetUrl);
|
||||
if (authorization) {
|
||||
headers.set("Authorization", authorization);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
});
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
let body: unknown;
|
||||
if (contentType.toLowerCase().includes("application/json")) {
|
||||
body = await response.json();
|
||||
} else {
|
||||
body = await response.text();
|
||||
}
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body,
|
||||
};
|
||||
}
|
||||
36
src/cli/commands/shared.ts
Normal file
36
src/cli/commands/shared.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { minimatch } from "minimatch";
|
||||
|
||||
import { loadPublicConfig } from "../../index.ts";
|
||||
import { getGraphClient } from "../../graph/auth.ts";
|
||||
|
||||
type PermissionRow = {
|
||||
permissionValue?: string | null;
|
||||
permissionDisplayName?: string | null;
|
||||
};
|
||||
|
||||
type DisplayNameRow = {
|
||||
displayName?: string | null;
|
||||
};
|
||||
|
||||
export function filterByPermissionName<T extends PermissionRow>(rows: T[], pattern: string): T[] {
|
||||
return rows.filter((item) =>
|
||||
minimatch(item.permissionValue ?? "", pattern, { nocase: true })
|
||||
|| minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }),
|
||||
);
|
||||
}
|
||||
|
||||
export function filterByDisplayName<T extends DisplayNameRow>(rows: T[], pattern: string): T[] {
|
||||
return rows.filter((item) =>
|
||||
minimatch(item.displayName ?? "", pattern, { nocase: true }),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getGraphClientFromPublicConfig(): Promise<{ client: any }> {
|
||||
const config = await loadPublicConfig();
|
||||
return getGraphClient({
|
||||
tenantId: config.tenantId,
|
||||
clientId: config.clientId,
|
||||
});
|
||||
}
|
||||
14
src/cli/commands/table.ts
Normal file
14
src/cli/commands/table.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { readJsonFromStdin } from "../utils.ts";
|
||||
|
||||
export function usageTable(): string {
|
||||
return `Usage: sk-az-tools table [--header|-H <spec|auto|a|original|o>] [global options]
|
||||
|
||||
Options:
|
||||
-H, --header <value> Header mode/spec: auto|a (default), original|o, OR "col1, col2" OR "key1: Label 1, key2: Label 2"`;
|
||||
}
|
||||
|
||||
export async function runTableCommand(): Promise<unknown> {
|
||||
return readJsonFromStdin();
|
||||
}
|
||||
19
src/cli/commands/types.ts
Normal file
19
src/cli/commands/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export type CommandValues = {
|
||||
[key: string]: string | boolean | undefined;
|
||||
type?: string;
|
||||
method?: string;
|
||||
url?: string;
|
||||
header?: string;
|
||||
resources?: string;
|
||||
"use-device-code"?: boolean;
|
||||
"no-browser"?: boolean;
|
||||
browser?: string;
|
||||
"browser-profile"?: string;
|
||||
all?: boolean;
|
||||
"display-name"?: string;
|
||||
"app-id"?: string;
|
||||
filter?: string;
|
||||
resolve?: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user