Authentication refactoring.
This commit is contained in:
@@ -26,14 +26,14 @@ Note: `rest --header` is a command-specific HTTP header option and is unrelated
|
||||
|
||||
**Command name:** `login`
|
||||
|
||||
**Usage:** `sk-az-tools login [--resources <csv>] [--use-device-code] [--no-browser] [--browser <name>] [--browser-profile <profile>] [global options]`
|
||||
**Usage:** `sk-az-tools login [resource...] [--use-device-code] [--no-browser] [--browser-name <name>] [--browser-profile <profile>] [global options]`
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--resources` <csv> - Comma-separated resources to authenticate. Allowed values: `graph`, `devops`, `arm`. Default is all three.
|
||||
- `[resource...]` - One or more resources to authenticate. Allowed values: `graph`, `devops`, `azurerm`. Default is `azurerm`.
|
||||
- `--use-device-code` - Use device code flow instead of browser-based interactive flow.
|
||||
- `--no-browser` - Do not launch browser automatically. Print the sign-in URL to stderr.
|
||||
- `--browser` <name> - Browser keyword used for interactive sign-in. Allowed values: `brave`, `browser`, `browserPrivate`, `chrome`, `edge`, `firefox`.
|
||||
- `--browser-name` <name> - Browser keyword used for interactive sign-in. Allowed values: `brave`, `browser`, `browserPrivate`, `chrome`, `edge`, `firefox`.
|
||||
- `--browser-profile` <name> - Chromium profile name (for example: `Default`, `Profile 1`).
|
||||
|
||||
**Description:** The `login` command authenticates user sign-in for selected resource audiences and caches tokens for subsequent commands.
|
||||
|
||||
29
scripts/sample-az-graph-devops.sh
Executable file
29
scripts/sample-az-graph-devops.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Hardcode variables.
|
||||
SUBSCRIPTION_ID="c885a276-c882-483f-b216-42f73715161d"
|
||||
|
||||
ACCESS_TOKEN=$(sk-az-tools get-token graph)
|
||||
|
||||
|
||||
# List Azure resource groups via Azure Resource Manager API
|
||||
echo "Azure Resource Groups in subscription '$SUBSCRIPTION_ID':"
|
||||
curl -sSL -H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
"https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourcegroups?api-version=2021-04-01" |
|
||||
jq '.value[] | {id, name, location}'
|
||||
|
||||
echo "---"
|
||||
|
||||
# Get current user ('me') via Microsoft Graph
|
||||
echo "Current User (me):"
|
||||
curl -sSL -H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
"https://graph.microsoft.com/v1.0/me" |
|
||||
jq '{id, displayName, userPrincipalName}'
|
||||
|
||||
echo "---"
|
||||
|
||||
# List Azure DevOps projects in the given org
|
||||
echo "Azure DevOps Projects in org 'skoszewski':"
|
||||
curl -sSL -H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
"https://dev.azure.com/skoszewski/_apis/projects?api-version=7.1" |
|
||||
jq '.value[] | {id, name, state}'
|
||||
@@ -8,7 +8,10 @@
|
||||
|
||||
import { getTokenUsingMsal } from "./pca-auth.ts";
|
||||
import { getTokenUsingAzureIdentity } from "./client-auth.ts";
|
||||
import { loadConfig } from "../index.ts";
|
||||
import { loadAuthConfig, loadConfig } from "../index.ts";
|
||||
import { SkAzureCredential } from "./sk-credential.ts";
|
||||
import { DefaultAzureCredential } from "@azure/identity";
|
||||
import type { TokenCredential } from "@azure/core-auth";
|
||||
|
||||
// Reexporting functions and types from submodules
|
||||
export {
|
||||
@@ -24,21 +27,42 @@ export { getCredential } from "./client-auth.ts";
|
||||
export const RESOURCE_SCOPE_BY_NAME = {
|
||||
graph: "https://graph.microsoft.com/.default",
|
||||
devops: "499b84ac-1321-427f-aa17-267ca6975798/.default",
|
||||
arm: "https://management.azure.com/.default",
|
||||
azurerm: "https://management.azure.com/.default",
|
||||
openai: "https://cognitiveservices.azure.com/.default",
|
||||
} as const;
|
||||
|
||||
export type ResourceName = keyof typeof RESOURCE_SCOPE_BY_NAME;
|
||||
export const DEFAULT_RESOURCES: ResourceName[] = ["graph", "devops", "arm"];
|
||||
export const DEFAULT_RESOURCES: ResourceName[] = ["graph", "devops", "azurerm"];
|
||||
|
||||
// A helper function to translate short resource names to their corresponding scopes
|
||||
export function translateResourceNamesToScopes(resourceNames: string[]): string[] {
|
||||
return resourceNames.map((name) => RESOURCE_SCOPE_BY_NAME[name as ResourceName]);
|
||||
}
|
||||
|
||||
export function supportedResourceNames(): ResourceName[] {
|
||||
return Object.keys(RESOURCE_SCOPE_BY_NAME) as ResourceName[];
|
||||
}
|
||||
|
||||
// Generic utility functions
|
||||
export type AuthMode = "azure-identity" | "msal";
|
||||
|
||||
export async function getTokenCredential(
|
||||
tenantId?: string,
|
||||
clientId?: string,
|
||||
): Promise<TokenCredential> {
|
||||
const config = await loadConfig();
|
||||
|
||||
if (config.authMode === "azure-identity") {
|
||||
return new DefaultAzureCredential();
|
||||
}
|
||||
|
||||
const authConfig = await loadAuthConfig("public-config");
|
||||
return new SkAzureCredential(
|
||||
tenantId || authConfig.tenantId,
|
||||
clientId || authConfig.clientId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAccessToken(
|
||||
tenantId: string,
|
||||
clientId: string,
|
||||
@@ -55,24 +79,3 @@ export async function getAccessToken(
|
||||
return getTokenUsingAzureIdentity(tenantId, clientId, resources);
|
||||
}
|
||||
}
|
||||
|
||||
// export function getAzureIdentityGraphAuthProvider(
|
||||
// tenantId: string,
|
||||
// clientId: string,
|
||||
// ) {
|
||||
// const credential = new DefaultAzureCredential({
|
||||
// tenantId,
|
||||
// managedIdentityClientId: clientId,
|
||||
// });
|
||||
|
||||
// const getBearerToken = getBearerTokenProvider(
|
||||
// credential,
|
||||
// "https://graph.microsoft.com/.default",
|
||||
// );
|
||||
|
||||
// return (done: (error: Error | null, accessToken: string | null) => void) => {
|
||||
// void getBearerToken()
|
||||
// .then((token) => done(null, token))
|
||||
// .catch((err) => done(err as Error, null));
|
||||
// };
|
||||
// }
|
||||
@@ -22,14 +22,14 @@ const LOGIN_REQUIRED_MESSAGE = "Login required. Run: sk-az-tools login";
|
||||
const BROWSER_KEYWORDS = Object.keys(apps).sort();
|
||||
const OPEN_APPS = apps as Record<string, string | readonly string[]>;
|
||||
const CHROMIUM_BROWSERS = new Set(["edge", "chrome", "brave"]);
|
||||
const CONFIG_FILE_NAME = "config";
|
||||
const SESSION_STATE_NAME = "session-state";
|
||||
|
||||
type SessionState = {
|
||||
activeAccountUpn: string | null;
|
||||
};
|
||||
|
||||
async function readSessionState(): Promise<SessionState> {
|
||||
const parsed = (await getConfig("sk-az-tools", CONFIG_FILE_NAME)) as { activeAccountUpn?: unknown };
|
||||
const parsed = (await getConfig("sk-az-tools", SESSION_STATE_NAME)) as { activeAccountUpn?: unknown };
|
||||
return {
|
||||
activeAccountUpn:
|
||||
typeof parsed?.activeAccountUpn === "string"
|
||||
@@ -39,14 +39,14 @@ async function readSessionState(): Promise<SessionState> {
|
||||
}
|
||||
|
||||
async function writeSessionState(state: SessionState): Promise<void> {
|
||||
const sessionPath = path.join(getConfigDir("sk-az-tools"), `${CONFIG_FILE_NAME}.json`);
|
||||
const sessionPath = path.join(getConfigDir("sk-az-tools"), `${SESSION_STATE_NAME}.json`);
|
||||
await mkdir(path.dirname(sessionPath), { recursive: true });
|
||||
await writeFile(sessionPath, JSON.stringify(state, null, 2), "utf8");
|
||||
}
|
||||
|
||||
async function clearSessionState(): Promise<void> {
|
||||
try {
|
||||
const sessionPath = path.join(getConfigDir("sk-az-tools"), `${CONFIG_FILE_NAME}.json`);
|
||||
const sessionPath = path.join(getConfigDir("sk-az-tools"), `${SESSION_STATE_NAME}.json`);
|
||||
await unlink(sessionPath);
|
||||
} catch (err) {
|
||||
if ((err as { code?: string } | null)?.code !== "ENOENT") {
|
||||
@@ -104,19 +104,19 @@ function getBrowserOpenOptions(browser?: string, browserProfile?: string): Param
|
||||
const browserKeyword = getBrowserKeyword(browser);
|
||||
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
|
||||
throw new Error(
|
||||
"--browser-profile is supported only with --browser edge|chrome|brave",
|
||||
"--browser-profile is supported only with --browser-name edge|chrome|brave",
|
||||
);
|
||||
}
|
||||
|
||||
if (!browserName) {
|
||||
throw new Error("--browser-profile requires --browser");
|
||||
throw new Error("--browser-profile requires --browser-name");
|
||||
}
|
||||
|
||||
return {
|
||||
wait: false,
|
||||
app: {
|
||||
name: browserName,
|
||||
arguments: [`--profile-directory=${browserProfile.trim()}`],
|
||||
arguments: [`--profile-directory=${browserProfile.trim()}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -130,19 +130,18 @@ function validateBrowserOptions(browser?: string, browserProfile?: string): void
|
||||
const browserKeyword = getBrowserKeyword(browser);
|
||||
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
|
||||
throw new Error(
|
||||
"--browser-profile is supported only with --browser edge|chrome|brave",
|
||||
"--browser-profile is supported only with --browser-name edge|chrome|brave",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseResources(resourcesCsv?: string): ResourceName[] {
|
||||
if (!resourcesCsv || resourcesCsv.trim() === "") {
|
||||
export function parseResources(resourcesInput?: string[]): ResourceName[] {
|
||||
if (!resourcesInput || resourcesInput.length === 0) {
|
||||
return [...DEFAULT_RESOURCES];
|
||||
}
|
||||
|
||||
const resources = resourcesCsv
|
||||
.split(",")
|
||||
const resources = resourcesInput
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -317,7 +316,7 @@ export async function loginDeviceCode(
|
||||
export async function login(
|
||||
tenantId: string,
|
||||
clientId: string,
|
||||
resourcesCsv?: string,
|
||||
resourcesInput?: string[],
|
||||
useDeviceCode = false,
|
||||
noBrowser = false,
|
||||
browser?: string,
|
||||
@@ -332,8 +331,8 @@ export async function login(
|
||||
if (!clientId) throw new Error("clientId is required");
|
||||
validateBrowserOptions(browser, browserProfile);
|
||||
|
||||
const resources = parseResources(resourcesCsv);
|
||||
const scopes = translateResourceNamesToScopes(resources);
|
||||
const resources = parseResources(resourcesInput);
|
||||
const scopes = translateResourceNamesToScopes(resources) as string[];
|
||||
const pca = await createPca(tenantId, clientId);
|
||||
const session = await readSessionState();
|
||||
const preferredAccount = session.activeAccountUpn
|
||||
@@ -344,6 +343,10 @@ export async function login(
|
||||
let selectedAccount: AccountInfo | null = preferredAccount;
|
||||
let token = await acquireTokenWithCache(pca, scopes, selectedAccount);
|
||||
|
||||
if (token?.account) {
|
||||
selectedAccount = token.account;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
if (useDeviceCode) {
|
||||
token = await pca.acquireTokenByDeviceCode({
|
||||
@@ -378,6 +381,11 @@ export async function login(
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedAccount) {
|
||||
const accounts = await pca.getTokenCache().getAllAccounts();
|
||||
selectedAccount = accounts[0] ?? null;
|
||||
}
|
||||
|
||||
const activeAccountUpn = selectedAccount?.username ?? null;
|
||||
if (activeAccountUpn) {
|
||||
await writeSessionState({ activeAccountUpn });
|
||||
|
||||
68
src/cli.ts
68
src/cli.ts
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Command, Option } from "commander";
|
||||
import { Argument, Command, Option } from "commander";
|
||||
import { renderCliOutput } from "@slawek/sk-tools";
|
||||
import { supportedResourceNames, ResourceName } from "./azure/index.ts";
|
||||
|
||||
// Commands
|
||||
import { runGetTokenCommand } from "./cli/commands/get-token.ts";
|
||||
@@ -13,6 +14,7 @@ import { runListResourcePermissionsCommand } from "./cli/commands/list-resource-
|
||||
import { runLoginCommand } from "./cli/commands/login.ts";
|
||||
import { runLogoutCommand } from "./cli/commands/logout.ts";
|
||||
import { runRestCommand } from "./cli/commands/rest.ts";
|
||||
import { runTestCommand } from "./cli/commands/test-command.ts";
|
||||
|
||||
import pkg from "../package.json" with { type: "json" };
|
||||
const { version: packageVersion } = pkg;
|
||||
@@ -33,33 +35,28 @@ async function main(): Promise<void> {
|
||||
skAzTools
|
||||
.command("login")
|
||||
.description("Authenticate selected resources")
|
||||
.option("--resources <csv>", "Comma-separated resources: graph,devops,arm")
|
||||
.addArgument(
|
||||
new Argument("[resource...]", "Resources: graph|devops|azurerm")
|
||||
.choices(supportedResourceNames())
|
||||
.default(["azurerm"]),
|
||||
)
|
||||
.option("--use-device-code", "Use device code flow")
|
||||
.option("--no-browser", "Do not launch browser")
|
||||
.option("--browser <name>", "Browser keyword: brave|browser|browserPrivate|chrome|edge|firefox")
|
||||
.option("--browser-name <name>", "Browser keyword: brave|browser|browserPrivate|chrome|edge|firefox")
|
||||
.option("--browser-profile <name>", "Chromium profile name")
|
||||
.action(async (options) => {
|
||||
const output = await runLoginCommand(options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
});
|
||||
.action(runLoginCommand);
|
||||
|
||||
skAzTools
|
||||
.command("logout")
|
||||
.description("Sign out and clear login state")
|
||||
.option("--all", "Clear login state and remove all cached accounts")
|
||||
.action(async (options) => {
|
||||
const output = await runLogoutCommand(options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
});
|
||||
.action(runLogoutCommand);
|
||||
|
||||
skAzTools
|
||||
.command("get-token")
|
||||
.description("Get access token (azurerm|devops)")
|
||||
.addOption(new Option("-t, --type <value>", "Token type").choices(["azurerm", "devops"]))
|
||||
.action(async (options) => {
|
||||
const output = await runGetTokenCommand(options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
});
|
||||
.description("Get an access token for a resource or resources.")
|
||||
.addArgument(new Argument("<type>", "Token type.").choices(supportedResourceNames()))
|
||||
.action(runGetTokenCommand);
|
||||
|
||||
skAzTools
|
||||
.command("rest")
|
||||
@@ -71,10 +68,14 @@ async function main(): Promise<void> {
|
||||
.addHelpText("after", `
|
||||
Authorization is added automatically for:
|
||||
management.azure.com Uses azurerm token
|
||||
dev.azure.com Uses devops token`)
|
||||
.action(async (url, options) => {
|
||||
dev.azure.com Uses devops token
|
||||
graph.microsoft.com Uses graph token
|
||||
cognitiveservices.azure.com Uses openai token
|
||||
*.openai.azure.com Uses openai token`)
|
||||
.action(async (url, options, command) => {
|
||||
const output = await runRestCommand(url, options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
const allOptions = command.optsWithGlobals();
|
||||
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||
});
|
||||
|
||||
skAzTools
|
||||
@@ -83,9 +84,10 @@ Authorization is added automatically for:
|
||||
.option("-n, --display-name <name>", "Get app by display name")
|
||||
.option("-i, --app-id <id>", "Get app by id")
|
||||
.option("-f, --filter <pattern>", "Filter display name glob")
|
||||
.action(async (options) => {
|
||||
.action(async (options, command) => {
|
||||
const output = await runListAppsCommand(options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
const allOptions = command.optsWithGlobals();
|
||||
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||
});
|
||||
|
||||
skAzTools
|
||||
@@ -95,18 +97,20 @@ Authorization is added automatically for:
|
||||
.option("-r, --resolve", "Resolve permission GUIDs to human-readable values")
|
||||
.option("-s, --short", "Makes output more compact")
|
||||
.option("-f, --filter <glob>", "Filter by permission name glob")
|
||||
.action(async (options) => {
|
||||
.action(async (options, command) => {
|
||||
const output = await runListAppPermissionsCommand(options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
const allOptions = command.optsWithGlobals();
|
||||
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||
});
|
||||
|
||||
skAzTools
|
||||
.command("list-app-grants")
|
||||
.description("List OAuth2 grants for an app")
|
||||
.option("-i, --app-id <appId>", "Application (client) ID")
|
||||
.action(async (options) => {
|
||||
.action(async (options, command) => {
|
||||
const output = await runListAppGrantsCommand(options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
const allOptions = command.optsWithGlobals();
|
||||
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||
});
|
||||
|
||||
skAzTools
|
||||
@@ -115,10 +119,18 @@ Authorization is added automatically for:
|
||||
.option("-i, --app-id <appId>", "Resource app ID")
|
||||
.option("-n, --display-name <name>", "Resource app display name")
|
||||
.option("-f, --filter <glob>", "Filter by permission name glob")
|
||||
.action(async (options) => {
|
||||
.action(async (options, command) => {
|
||||
const output = await runListResourcePermissionsCommand(options);
|
||||
renderCliOutput(output, skAzTools.opts().output, skAzTools.opts().query, skAzTools.opts().columns);
|
||||
const allOptions = command.optsWithGlobals();
|
||||
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||
});
|
||||
|
||||
// Hidden test command for development purposes
|
||||
skAzTools
|
||||
.command("test", { hidden: true })
|
||||
.description("Test command for development")
|
||||
.action(runTestCommand);
|
||||
|
||||
await skAzTools.parseAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
@@ -1,48 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { getAccessToken } from "../../azure/index.ts";
|
||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||
import { loadAuthConfig } from "../../index.ts";
|
||||
|
||||
type GetTokenOptions = {
|
||||
type?: string;
|
||||
};
|
||||
import { RESOURCE_SCOPE_BY_NAME, ResourceName, supportedResourceNames, getTokenCredential } from "../../azure/index.ts";
|
||||
|
||||
export async function runGetTokenCommand(
|
||||
options: GetTokenOptions,
|
||||
): Promise<unknown> {
|
||||
const tokenType = (options.type ?? "").toString().trim().toLowerCase();
|
||||
if (!tokenType) {
|
||||
throw new Error(
|
||||
"--type is required for get-token (allowed: azurerm, devops)",
|
||||
);
|
||||
type: ResourceName,
|
||||
): Promise<void> {
|
||||
if (!type || !supportedResourceNames().includes(type)) {
|
||||
throw new Error(`Token type is required for get-token (allowed: ${supportedResourceNames().join(", ")})`);
|
||||
}
|
||||
|
||||
const config = await loadAuthConfig("public-config");
|
||||
const credential = await getTokenCredential();
|
||||
|
||||
if (tokenType === "azurerm") {
|
||||
const accessToken = await getAccessToken(config.tenantId, config.clientId, ["arm"]);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error("Failed to obtain AzureRM token");
|
||||
}
|
||||
|
||||
return {
|
||||
tokenType,
|
||||
accessToken,
|
||||
};
|
||||
const accessToken = await credential.getToken(RESOURCE_SCOPE_BY_NAME[type]);
|
||||
if (!accessToken) {
|
||||
throw new Error("Failed to obtain access token.");
|
||||
}
|
||||
|
||||
if (tokenType === "devops") {
|
||||
const accessToken = await getDevOpsApiToken(
|
||||
config.tenantId,
|
||||
config.clientId,
|
||||
);
|
||||
return {
|
||||
tokenType,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Invalid --type '${options.type}'. Allowed: azurerm, devops`);
|
||||
// Output only the token string for easy consumption in scripts
|
||||
console.log(accessToken.token);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { login } from "../../azure/index.ts";
|
||||
import type { ResourceName } from "../../azure/index.ts";
|
||||
import { loadAuthConfig } from "../../index.ts";
|
||||
|
||||
type LoginOptions = {
|
||||
resources?: string;
|
||||
useDeviceCode?: boolean;
|
||||
noBrowser?: boolean;
|
||||
browser?: string;
|
||||
browserName?: string;
|
||||
browserProfile?: string;
|
||||
};
|
||||
|
||||
export async function runLoginCommand(options: LoginOptions): Promise<unknown> {
|
||||
type LoginResult = {
|
||||
accountUpn: string | null;
|
||||
resources: Array<{ resource: string; expiresOn: string | null }>;
|
||||
flow: "device-code" | "interactive";
|
||||
browserLaunchAttempted: boolean;
|
||||
};
|
||||
|
||||
export async function runLoginCommand(resources: ResourceName[], options: LoginOptions): Promise<void> {
|
||||
const config = await loadAuthConfig("public-config");
|
||||
return login(
|
||||
|
||||
const result = await login(
|
||||
config.tenantId,
|
||||
config.clientId,
|
||||
options.resources,
|
||||
resources,
|
||||
Boolean(options.useDeviceCode),
|
||||
Boolean(options.noBrowser),
|
||||
options.browser,
|
||||
options.browserName,
|
||||
options.browserProfile,
|
||||
);
|
||||
) as LoginResult;
|
||||
|
||||
console.log(`Logged in as ${result.accountUpn ?? "<unknown>"} using ${result.flow} flow for resources: ${resources.join(",")}`);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,28 @@ type LogoutOptions = {
|
||||
all?: boolean;
|
||||
};
|
||||
|
||||
export async function runLogoutCommand(options: LogoutOptions): Promise<unknown> {
|
||||
type LogoutResult = {
|
||||
clearedAll: boolean;
|
||||
signedOut: string[];
|
||||
};
|
||||
|
||||
export async function runLogoutCommand(options: LogoutOptions): Promise<void> {
|
||||
const config = await loadAuthConfig("public-config");
|
||||
return logout(config.tenantId, config.clientId, Boolean(options.all));
|
||||
const result = await logout(config.tenantId, config.clientId, Boolean(options.all)) as LogoutResult;
|
||||
|
||||
if (result.signedOut.length === 0) {
|
||||
console.log(
|
||||
result.clearedAll
|
||||
? "Cleared all cached accounts."
|
||||
: "No active account to sign out.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.clearedAll) {
|
||||
console.log(`Cleared all cached accounts: ${result.signedOut.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Signed out: ${result.signedOut.join(", ")}`);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { getAccessToken } from "../../azure/index.ts";
|
||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||
import { loadAuthConfig } from "../../index.ts";
|
||||
import { RESOURCE_SCOPE_BY_NAME, ResourceName, getTokenCredential } from "../../azure/index.ts";
|
||||
|
||||
function parseHeaderLine(
|
||||
header?: string,
|
||||
@@ -35,24 +33,39 @@ function hasAuthorizationHeader(headers: Headers): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveResourceNameForHost(host: string): ResourceName | null {
|
||||
switch (host) {
|
||||
case "management.azure.com":
|
||||
return "azurerm";
|
||||
case "dev.azure.com":
|
||||
return "devops";
|
||||
case "graph.microsoft.com":
|
||||
return "graph";
|
||||
case "cognitiveservices.azure.com":
|
||||
return "openai";
|
||||
default:
|
||||
if (host.endsWith(".openai.azure.com")) {
|
||||
return "openai";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
|
||||
const host = url.hostname.toLowerCase();
|
||||
if (host !== "management.azure.com" && host !== "dev.azure.com") {
|
||||
const resourceName = resolveResourceNameForHost(host);
|
||||
if (!resourceName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = await loadAuthConfig("public-config");
|
||||
const credential = await getTokenCredential();
|
||||
|
||||
if (host === "management.azure.com") {
|
||||
const accessToken = await getAccessToken(config.tenantId, config.clientId, ["arm"]);
|
||||
if (!accessToken) {
|
||||
throw new Error("Failed to obtain AzureRM token");
|
||||
}
|
||||
return `Bearer ${accessToken}`;
|
||||
const accessToken = await credential.getToken(RESOURCE_SCOPE_BY_NAME[resourceName]);
|
||||
if (!accessToken?.token) {
|
||||
throw new Error(`Failed to obtain ${resourceName} token`);
|
||||
}
|
||||
|
||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
||||
return `Bearer ${accessToken}`;
|
||||
return `Bearer ${accessToken.token}`;
|
||||
}
|
||||
|
||||
type httpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
6
src/cli/commands/test-command.ts
Normal file
6
src/cli/commands/test-command.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Hidden test command for development purposes
|
||||
export async function runTestCommand(): Promise<void> {
|
||||
console.log("Test command executed.");
|
||||
}
|
||||
@@ -4,35 +4,23 @@
|
||||
* A DevOps helpers module.
|
||||
*/
|
||||
|
||||
import { loginInteractive } from "../azure/index.ts";
|
||||
import { RESOURCE_SCOPE_BY_NAME, getTokenCredential } from "../azure/index.ts";
|
||||
import * as azdev from "azure-devops-node-api";
|
||||
|
||||
const AZURE_DEVOPS_SCOPES = ["https://app.vssps.visualstudio.com/.default"];
|
||||
|
||||
type LoginInteractiveResult = {
|
||||
accessToken?: string;
|
||||
export type DevOpsClients = {
|
||||
coreClient: Awaited<ReturnType<azdev.WebApi["getCoreApi"]>>;
|
||||
gitClient: Awaited<ReturnType<azdev.WebApi["getGitApi"]>>;
|
||||
};
|
||||
|
||||
export async function getDevOpsApiToken(tenantId: string, clientId: string): Promise<string> {
|
||||
const result = await loginInteractive(
|
||||
tenantId,
|
||||
clientId,
|
||||
AZURE_DEVOPS_SCOPES,
|
||||
) as LoginInteractiveResult;
|
||||
export async function getDevOpsClients(orgUrl: string, tenantId?: string, clientId?: string): Promise<DevOpsClients> {
|
||||
const credential = await getTokenCredential(tenantId, clientId);
|
||||
|
||||
const accessToken = result?.accessToken;
|
||||
|
||||
if (!accessToken) {
|
||||
const accessToken = await credential.getToken(RESOURCE_SCOPE_BY_NAME.devops);
|
||||
if (!accessToken?.token) {
|
||||
throw new Error("Failed to obtain Azure DevOps API token");
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
export async function getDevOpsClients(orgUrl: string, tenantId: string, clientId: string): Promise<{ coreClient: unknown; gitClient: unknown }> {
|
||||
const accessToken = await getDevOpsApiToken(tenantId, clientId);
|
||||
|
||||
const authHandler = azdev.getBearerHandler(accessToken);
|
||||
const authHandler = azdev.getBearerHandler(accessToken.token);
|
||||
const connection = new azdev.WebApi(orgUrl, authHandler);
|
||||
|
||||
const coreClient = await connection.getCoreApi();
|
||||
|
||||
Reference in New Issue
Block a user