// SPDX-License-Identifier: MIT import { minimatch } from "minimatch"; 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 { 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(rows: T[], pattern: string): T[] { return rows.filter((item) => minimatch(item.permissionValue ?? "", pattern, { nocase: true }) || minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }), ); } function filterByDisplayName(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 { return readJsonFromStdin(); } async function runLoginCommand(values: CommandValues): Promise { 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 { const config = await loadPublicConfig(); return logout({ tenantId: config.tenantId, clientId: config.clientId, clearAll: Boolean(values.all), }); } async function runListAppsCommand(values: CommandValues): Promise { 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 { 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 { 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 { 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 { 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 { 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, }; } export async function runCommand(command: string, values: CommandValues): Promise { switch (command) { case "login": return runLoginCommand(values); case "logout": return runLogoutCommand(values); case "table": return runTableCommand(); case "list-apps": return runListAppsCommand(values); case "list-app-permissions": return runListAppPermissionsCommand(values); case "list-app-grants": return runListAppGrantsCommand(values); case "list-resource-permissions": return runListResourcePermissionsCommand(values); case "get-token": return runGetTokenCommand(values); case "rest": return runRestCommand(values); default: throw new Error(`Unknown command: ${command}`); } }