Refactor of authentication code. Added configuration file selectable authentication method. Selectable from built-in Azure Identity, and custom PCA using MSAL.
Some checks failed
build / build (push) Failing after 14s
Some checks failed
build / build (push) Failing after 14s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.5.2",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity";
|
import {
|
||||||
import type { AuthenticationResult } from "@azure/msal-node";
|
DefaultAzureCredential,
|
||||||
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts";
|
ClientSecretCredential,
|
||||||
|
DeviceCodeCredential,
|
||||||
|
getBearerTokenProvider,
|
||||||
|
} from "@azure/identity";
|
||||||
|
import type { TokenCredential } from "@azure/core-auth";
|
||||||
|
import { SkAzureCredential } from "./sk-credential.ts";
|
||||||
|
|
||||||
type CredentialType = "d" | "default" | "cs" | "clientSecret" | "dc" | "deviceCode";
|
import { translateResourceNamesToScopes } from "./index.ts";
|
||||||
|
|
||||||
export async function getCredential(
|
type CredentialType =
|
||||||
|
| "d"
|
||||||
|
| "default"
|
||||||
|
| "cs"
|
||||||
|
| "clientSecret"
|
||||||
|
| "dc"
|
||||||
|
| "deviceCode"
|
||||||
|
| "sk"
|
||||||
|
| "skCredential";
|
||||||
|
|
||||||
|
export function getCredential(
|
||||||
credentialType: CredentialType,
|
credentialType: CredentialType,
|
||||||
tenantId?: string,
|
tenantId?: string,
|
||||||
clientId?: string,
|
clientId?: string,
|
||||||
clientSecret?: string,
|
clientSecret?: string,
|
||||||
): Promise<DefaultAzureCredential | ClientSecretCredential | DeviceCodeCredential> {
|
): TokenCredential {
|
||||||
switch (credentialType) {
|
switch (credentialType) {
|
||||||
case "d":
|
case "d":
|
||||||
case "default":
|
case "default":
|
||||||
@@ -23,11 +38,7 @@ export async function getCredential(
|
|||||||
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
|
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new ClientSecretCredential(
|
return new ClientSecretCredential(tenantId, clientId, clientSecret);
|
||||||
tenantId,
|
|
||||||
clientId,
|
|
||||||
clientSecret,
|
|
||||||
);
|
|
||||||
case "dc":
|
case "dc":
|
||||||
case "deviceCode":
|
case "deviceCode":
|
||||||
if (!tenantId || !clientId) {
|
if (!tenantId || !clientId) {
|
||||||
@@ -42,15 +53,33 @@ export async function getCredential(
|
|||||||
console.log(info.message);
|
console.log(info.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
case "sk":
|
||||||
|
case "skCredential":
|
||||||
|
if (!tenantId || !clientId) {
|
||||||
|
throw new Error(
|
||||||
|
"tenantId and clientId are required for SkAzureCredential",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new SkAzureCredential(tenantId, clientId);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported credential type: ${credentialType}`);
|
throw new Error(`Unsupported credential type: ${credentialType}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acquireResourceToken(
|
export async function getTokenUsingAzureIdentity(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
resource: string,
|
resources: string[],
|
||||||
): Promise<AuthenticationResult | null> {
|
): Promise<string> {
|
||||||
return acquireResourceTokenPca(tenantId, clientId, resource);
|
const scopes = translateResourceNamesToScopes(resources);
|
||||||
|
const credential = getCredential("default", tenantId, clientId);
|
||||||
|
|
||||||
|
const getBearerToken = getBearerTokenProvider(credential, scopes);
|
||||||
|
const accessToken = await getBearerToken();
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error("Failed to acquire access token with Azure Identity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@
|
|||||||
* This module provides authentication functionalities for Azure services.
|
* This module provides authentication functionalities for Azure services.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { getCredential } from "./client-auth.ts";
|
import { getTokenUsingMsal } from "./pca-auth.ts";
|
||||||
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts";
|
import { getTokenUsingAzureIdentity } from "./client-auth.ts";
|
||||||
|
import { loadConfig } from "../index.ts";
|
||||||
|
|
||||||
|
// Reexporting functions and types from submodules
|
||||||
export {
|
export {
|
||||||
loginInteractive,
|
loginInteractive,
|
||||||
loginDeviceCode,
|
loginDeviceCode,
|
||||||
@@ -16,10 +19,60 @@ export {
|
|||||||
parseResources,
|
parseResources,
|
||||||
} from "./pca-auth.ts";
|
} from "./pca-auth.ts";
|
||||||
|
|
||||||
export async function acquireResourceToken(
|
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",
|
||||||
|
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"];
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic utility functions
|
||||||
|
export type AuthMode = "azure-identity" | "msal";
|
||||||
|
|
||||||
|
export async function getAccessToken(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
resource: string,
|
resources: string[]
|
||||||
) {
|
): Promise<string> {
|
||||||
return acquireResourceTokenPca(tenantId, clientId, resource);
|
const config = await loadConfig();
|
||||||
|
if (config.authMode === "msal") {
|
||||||
|
const result = await getTokenUsingMsal(tenantId, clientId, resources);
|
||||||
|
if (!result?.accessToken) {
|
||||||
|
throw new Error("Failed to acquire access token with MSAL.");
|
||||||
}
|
}
|
||||||
|
return result.accessToken;
|
||||||
|
} else {
|
||||||
|
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));
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -14,15 +14,10 @@ import type {
|
|||||||
TokenCacheContext,
|
TokenCacheContext,
|
||||||
} from "@azure/msal-node";
|
} from "@azure/msal-node";
|
||||||
|
|
||||||
const RESOURCE_SCOPE_BY_NAME = {
|
import type { ResourceName } from "../azure/index.ts";
|
||||||
graph: "https://graph.microsoft.com/.default",
|
import { RESOURCE_SCOPE_BY_NAME, DEFAULT_RESOURCES } from "../azure/index.ts";
|
||||||
devops: "499b84ac-1321-427f-aa17-267ca6975798/.default",
|
import { translateResourceNamesToScopes } from "./index.ts";
|
||||||
arm: "https://management.azure.com/.default",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type ResourceName = keyof typeof RESOURCE_SCOPE_BY_NAME;
|
|
||||||
|
|
||||||
const DEFAULT_RESOURCES: ResourceName[] = ["graph", "devops", "arm"];
|
|
||||||
const LOGIN_REQUIRED_MESSAGE = "Login required. Run: sk-az-tools login";
|
const LOGIN_REQUIRED_MESSAGE = "Login required. Run: sk-az-tools login";
|
||||||
const BROWSER_KEYWORDS = Object.keys(apps).sort();
|
const BROWSER_KEYWORDS = Object.keys(apps).sort();
|
||||||
const OPEN_APPS = apps as Record<string, string | readonly string[]>;
|
const OPEN_APPS = apps as Record<string, string | readonly string[]>;
|
||||||
@@ -296,8 +291,8 @@ export async function loginInteractive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loginDeviceCode(
|
export async function loginDeviceCode(
|
||||||
tenantId: string | undefined,
|
tenantId: string,
|
||||||
clientId: string | undefined,
|
clientId: string,
|
||||||
scopes: string[],
|
scopes: string[],
|
||||||
): Promise<AuthenticationResult | null> {
|
): Promise<AuthenticationResult | null> {
|
||||||
if (!tenantId) throw new Error("tenantId is required");
|
if (!tenantId) throw new Error("tenantId is required");
|
||||||
@@ -320,8 +315,8 @@ export async function loginDeviceCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function login(
|
export async function login(
|
||||||
tenantId: string | undefined,
|
tenantId: string,
|
||||||
clientId: string | undefined,
|
clientId: string,
|
||||||
resourcesCsv?: string,
|
resourcesCsv?: string,
|
||||||
useDeviceCode = false,
|
useDeviceCode = false,
|
||||||
noBrowser = false,
|
noBrowser = false,
|
||||||
@@ -338,7 +333,7 @@ export async function login(
|
|||||||
validateBrowserOptions(browser, browserProfile);
|
validateBrowserOptions(browser, browserProfile);
|
||||||
|
|
||||||
const resources = parseResources(resourcesCsv);
|
const resources = parseResources(resourcesCsv);
|
||||||
const scopes = resources.map((resourceName) => RESOURCE_SCOPE_BY_NAME[resourceName]);
|
const scopes = translateResourceNamesToScopes(resources);
|
||||||
const pca = await createPca(tenantId, clientId);
|
const pca = await createPca(tenantId, clientId);
|
||||||
const session = await readSessionState();
|
const session = await readSessionState();
|
||||||
const preferredAccount = session.activeAccountUpn
|
const preferredAccount = session.activeAccountUpn
|
||||||
@@ -347,22 +342,19 @@ export async function login(
|
|||||||
|
|
||||||
const results: Array<{ resource: string; expiresOn: string | null }> = [];
|
const results: Array<{ resource: string; expiresOn: string | null }> = [];
|
||||||
let selectedAccount: AccountInfo | null = preferredAccount;
|
let selectedAccount: AccountInfo | null = preferredAccount;
|
||||||
for (let index = 0; index < resources.length; index += 1) {
|
let token = await acquireTokenWithCache(pca, scopes, selectedAccount);
|
||||||
const resource = resources[index];
|
|
||||||
const scope = [scopes[index]];
|
|
||||||
let token = await acquireTokenWithCache(pca, scope, selectedAccount);
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
if (useDeviceCode) {
|
if (useDeviceCode) {
|
||||||
token = await pca.acquireTokenByDeviceCode({
|
token = await pca.acquireTokenByDeviceCode({
|
||||||
scopes: scope,
|
scopes: scopes,
|
||||||
deviceCodeCallback: (response) => {
|
deviceCodeCallback: (response) => {
|
||||||
writeStderr(response.message);
|
writeStderr(response.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
token = await pca.acquireTokenInteractive({
|
token = await pca.acquireTokenInteractive({
|
||||||
scopes: scope,
|
scopes: scopes,
|
||||||
openBrowser: async (url: string) => {
|
openBrowser: async (url: string) => {
|
||||||
if (noBrowser) {
|
if (noBrowser) {
|
||||||
writeStderr(`Visit:\n${url}`);
|
writeStderr(`Visit:\n${url}`);
|
||||||
@@ -375,14 +367,13 @@ export async function login(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (token?.account) {
|
if (token?.account) {
|
||||||
selectedAccount = token.account;
|
selectedAccount = token.account;
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
resource,
|
resource: resources.join(","),
|
||||||
expiresOn: token?.expiresOn?.toISOString?.() ?? null,
|
expiresOn: token?.expiresOn?.toISOString?.() ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -400,20 +391,14 @@ export async function login(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acquireResourceToken(
|
export async function getTokenUsingMsal(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
resource: string,
|
resources: string[],
|
||||||
): Promise<AuthenticationResult | null> {
|
): Promise<AuthenticationResult | null> {
|
||||||
if (!tenantId) throw new Error("tenantId is required");
|
if (!tenantId) throw new Error("tenantId is required");
|
||||||
if (!clientId) throw new Error("clientId is required");
|
if (!clientId) throw new Error("clientId is required");
|
||||||
if (!resource) throw new Error("resource is required");
|
if (!resources || resources.length === 0) throw new Error("resources are required");
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(RESOURCE_SCOPE_BY_NAME, resource)) {
|
|
||||||
throw new Error(`Invalid resource '${resource}'. Allowed: ${DEFAULT_RESOURCES.join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = RESOURCE_SCOPE_BY_NAME[resource as ResourceName];
|
|
||||||
|
|
||||||
const session = await readSessionState();
|
const session = await readSessionState();
|
||||||
if (!session.activeAccountUpn) {
|
if (!session.activeAccountUpn) {
|
||||||
@@ -426,10 +411,13 @@ export async function acquireResourceToken(
|
|||||||
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert short names of scopes to full resource scopes
|
||||||
|
const scopes = resources.map((res) => RESOURCE_SCOPE_BY_NAME[res as ResourceName] || res);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await pca.acquireTokenSilent({
|
return await pca.acquireTokenSilent({
|
||||||
account,
|
account,
|
||||||
scopes: [scope],
|
scopes,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
||||||
|
|||||||
29
src/azure/sk-credential.ts
Normal file
29
src/azure/sk-credential.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
import type { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth";
|
||||||
|
import { getTokenUsingMsal } from "./pca-auth.ts";
|
||||||
|
|
||||||
|
export class SkAzureCredential implements TokenCredential {
|
||||||
|
constructor(
|
||||||
|
private tenantId: string,
|
||||||
|
private clientId: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getToken(
|
||||||
|
scopes: string | string[],
|
||||||
|
_options?: GetTokenOptions,
|
||||||
|
): Promise<AccessToken | null> {
|
||||||
|
const resources = Array.isArray(scopes) ? scopes : [scopes];
|
||||||
|
const result = await getTokenUsingMsal(this.tenantId, this.clientId, resources);
|
||||||
|
|
||||||
|
if (!result?.accessToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: result.accessToken,
|
||||||
|
expiresOnTimestamp: result.expiresOn
|
||||||
|
? result.expiresOn.getTime()
|
||||||
|
: Date.now() + 55 * 60 * 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { acquireResourceToken } from "../../azure/index.ts";
|
import { getAccessToken } from "../../azure/index.ts";
|
||||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||||
import { loadConfig } from "../../index.ts";
|
import { loadAuthConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -13,22 +13,21 @@ Options:
|
|||||||
--type, -t <value> Token type: azurerm|devops`;
|
--type, -t <value> Token type: azurerm|devops`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runGetTokenCommand(values: CommandValues): Promise<unknown> {
|
export async function runGetTokenCommand(
|
||||||
|
values: CommandValues,
|
||||||
|
): Promise<unknown> {
|
||||||
const tokenType = (values.type ?? "").toString().trim().toLowerCase();
|
const tokenType = (values.type ?? "").toString().trim().toLowerCase();
|
||||||
if (!tokenType) {
|
if (!tokenType) {
|
||||||
throw new Error("--type is required for get-token (allowed: azurerm, devops)");
|
throw new Error(
|
||||||
|
"--type is required for get-token (allowed: azurerm, devops)",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await loadConfig("public-config");
|
const config = await loadAuthConfig("public-config");
|
||||||
|
|
||||||
if (tokenType === "azurerm") {
|
if (tokenType === "azurerm") {
|
||||||
const result = await acquireResourceToken(
|
const accessToken = await getAccessToken(config.tenantId, config.clientId, ["arm"]);
|
||||||
config.tenantId,
|
|
||||||
config.clientId,
|
|
||||||
"arm",
|
|
||||||
);
|
|
||||||
|
|
||||||
const accessToken = result?.accessToken;
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new Error("Failed to obtain AzureRM token");
|
throw new Error("Failed to obtain AzureRM token");
|
||||||
}
|
}
|
||||||
@@ -40,7 +39,10 @@ export async function runGetTokenCommand(values: CommandValues): Promise<unknown
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tokenType === "devops") {
|
if (tokenType === "devops") {
|
||||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
const accessToken = await getDevOpsApiToken(
|
||||||
|
config.tenantId,
|
||||||
|
config.clientId,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
tokenType,
|
tokenType,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { listAppGrants } from "../../graph/app.ts";
|
import { listAppGrants } from "../../graph/app.ts";
|
||||||
|
import { getGraphClient } from "../../graph/index.ts";
|
||||||
import { getGraphClientFromPublicConfig } from "./shared.ts";
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
export function usageListAppGrants(): string {
|
export function usageListAppGrants(): string {
|
||||||
@@ -17,6 +16,6 @@ export async function runListAppGrantsCommand(values: CommandValues): Promise<un
|
|||||||
throw new Error("--app-id is required for list-app-grants");
|
throw new Error("--app-id is required for list-app-grants");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
const client = await getGraphClient();
|
||||||
return listAppGrants(client, values["app-id"]);
|
return listAppGrants(client, values["app-id"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { listAppPermissions, listAppPermissionsResolved } from "../../graph/app.ts";
|
import { listAppPermissions, listAppPermissionsResolved } from "../../graph/app.ts";
|
||||||
|
|
||||||
import { filterByPermissionName, getGraphClientFromPublicConfig } from "./shared.ts";
|
import { filterByPermissionName } from "./shared.ts";
|
||||||
|
import { getGraphClient } from "../../graph/index.ts";
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
@@ -37,7 +38,7 @@ export async function runListAppPermissionsCommand(values: CommandValues): Promi
|
|||||||
throw new Error("--app-id is required for list-app-permissions");
|
throw new Error("--app-id is required for list-app-permissions");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
const client = await getGraphClient();
|
||||||
let result: unknown = values.resolve || values.filter
|
let result: unknown = values.resolve || values.filter
|
||||||
? await listAppPermissionsResolved(client, values["app-id"])
|
? await listAppPermissionsResolved(client, values["app-id"])
|
||||||
: await listAppPermissions(client, values["app-id"]);
|
: await listAppPermissions(client, values["app-id"]);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { listApps } from "../../graph/app.ts";
|
import { listApps } from "../../graph/app.ts";
|
||||||
|
import { filterByDisplayName } from "./shared.ts";
|
||||||
import { filterByDisplayName, getGraphClientFromPublicConfig } from "./shared.ts";
|
import { getGraphClient } from "../../graph/index.ts";
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
export function usageListApps(): string {
|
export function usageListApps(): string {
|
||||||
@@ -15,7 +15,8 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runListAppsCommand(values: CommandValues): Promise<unknown> {
|
export async function runListAppsCommand(values: CommandValues): Promise<unknown> {
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
const client = await getGraphClient();
|
||||||
|
|
||||||
let result = await listApps(client, values["display-name"], values["app-id"]);
|
let result = await listApps(client, values["display-name"], values["app-id"]);
|
||||||
if (values["app-id"] && result.length > 1) {
|
if (values["app-id"] && result.length > 1) {
|
||||||
throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`);
|
throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { listResourcePermissions } from "../../graph/app.ts";
|
import { listResourcePermissions } from "../../graph/app.ts";
|
||||||
|
import { getGraphClient } from "../../graph/index.ts";
|
||||||
import { filterByPermissionName, getGraphClientFromPublicConfig } from "./shared.ts";
|
import { filterByPermissionName } from "./shared.ts";
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
export function usageListResourcePermissions(): string {
|
export function usageListResourcePermissions(): string {
|
||||||
@@ -22,7 +22,7 @@ export async function runListResourcePermissionsCommand(values: CommandValues):
|
|||||||
throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both");
|
throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
const client = await getGraphClient();
|
||||||
let result = await listResourcePermissions(
|
let result = await listResourcePermissions(
|
||||||
client,
|
client,
|
||||||
values["app-id"],
|
values["app-id"],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { login } from "../../azure/index.ts";
|
import { login } from "../../azure/index.ts";
|
||||||
import { loadConfig } from "../../index.ts";
|
import { loadAuthConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runLoginCommand(values: CommandValues): Promise<unknown> {
|
export async function runLoginCommand(values: CommandValues): Promise<unknown> {
|
||||||
const config = await loadConfig("public-config");
|
const config = await loadAuthConfig("public-config");
|
||||||
return login(
|
return login(
|
||||||
config.tenantId,
|
config.tenantId,
|
||||||
config.clientId,
|
config.clientId,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { logout } from "../../azure/index.ts";
|
import { logout } from "../../azure/index.ts";
|
||||||
import { loadConfig } from "../../index.ts";
|
import { loadAuthConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -13,6 +13,6 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runLogoutCommand(values: CommandValues): Promise<unknown> {
|
export async function runLogoutCommand(values: CommandValues): Promise<unknown> {
|
||||||
const config = await loadConfig("public-config");
|
const config = await loadAuthConfig("public-config");
|
||||||
return logout(config.tenantId, config.clientId, Boolean(values.all));
|
return logout(config.tenantId, config.clientId, Boolean(values.all));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { acquireResourceToken } from "../../azure/index.ts";
|
import { getAccessToken } from "../../azure/index.ts";
|
||||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||||
import { loadConfig } from "../../index.ts";
|
import { loadAuthConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -19,7 +19,9 @@ Authorization is added automatically for:
|
|||||||
dev.azure.com Uses devops token`;
|
dev.azure.com Uses devops token`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHeaderLine(header?: string): { name: string; value: string } | null {
|
function parseHeaderLine(
|
||||||
|
header?: string,
|
||||||
|
): { name: string; value: string } | null {
|
||||||
if (!header || header.trim() === "") {
|
if (!header || header.trim() === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -54,15 +56,10 @@ async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await loadConfig("public-config");
|
const config = await loadAuthConfig("public-config");
|
||||||
|
|
||||||
if (host === "management.azure.com") {
|
if (host === "management.azure.com") {
|
||||||
const result = await acquireResourceToken(
|
const accessToken = await getAccessToken(config.tenantId, config.clientId, ["arm"]);
|
||||||
config.tenantId,
|
|
||||||
config.clientId,
|
|
||||||
"arm",
|
|
||||||
);
|
|
||||||
const accessToken = result?.accessToken;
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new Error("Failed to obtain AzureRM token");
|
throw new Error("Failed to obtain AzureRM token");
|
||||||
}
|
}
|
||||||
@@ -74,7 +71,8 @@ async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runRestCommand(values: CommandValues): Promise<unknown> {
|
export async function runRestCommand(values: CommandValues): Promise<unknown> {
|
||||||
const method = (values.method ?? "GET").toString().trim().toUpperCase() || "GET";
|
const method =
|
||||||
|
(values.method ?? "GET").toString().trim().toUpperCase() || "GET";
|
||||||
const urlValue = (values.url ?? "").toString().trim();
|
const urlValue = (values.url ?? "").toString().trim();
|
||||||
|
|
||||||
if (!urlValue) {
|
if (!urlValue) {
|
||||||
|
|||||||
@@ -1,24 +1,54 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { Client } from "@microsoft/microsoft-graph-client";
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
import { acquireResourceToken } from "../azure/index.ts";
|
import { getAccessToken } from "../azure/index.ts";
|
||||||
|
import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity";
|
||||||
|
|
||||||
type GraphApiToken = {
|
// export async function getGraphClientUsingMsal(
|
||||||
accessToken: string;
|
// tenantId: string,
|
||||||
[key: string]: unknown;
|
// clientId: string,
|
||||||
};
|
// ): Promise<Client> {
|
||||||
|
// const graphApiToken = await getAccessToken(tenantId, clientId, ["graph"]);
|
||||||
|
|
||||||
export async function getGraphClient(
|
// return Client.init({
|
||||||
|
// authProvider: (done) => {
|
||||||
|
// done(null, graphApiToken);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
type GraphAuthProvider = (
|
||||||
|
done: (error: Error | null, accessToken: string | null) => void
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export function getMsalAuthProvider(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
): Promise<{ graphApiToken: GraphApiToken; client: any }> {
|
): GraphAuthProvider {
|
||||||
const graphApiToken = await acquireResourceToken(tenantId, clientId, "graph") as GraphApiToken;
|
return (done) => {
|
||||||
|
void getAccessToken(tenantId, clientId, ["graph"])
|
||||||
|
.then((accessToken) => done(null, accessToken))
|
||||||
|
.catch((err) => done(err as Error, null));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const client = Client.init({
|
export function getAzureIdentityAuthProvider(
|
||||||
authProvider: (done) => {
|
tenantId: string,
|
||||||
done(null, graphApiToken.accessToken);
|
clientId: string,
|
||||||
},
|
) {
|
||||||
|
const credential = new DefaultAzureCredential({
|
||||||
|
tenantId,
|
||||||
|
managedIdentityClientId: clientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { graphApiToken, client };
|
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));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,22 @@
|
|||||||
export * from "./auth.ts";
|
export * from "./auth.ts";
|
||||||
export * from "./app.ts";
|
export * from "./app.ts";
|
||||||
export * from "./sp.ts";
|
export * from "./sp.ts";
|
||||||
|
|
||||||
|
import { loadAuthConfig, loadConfig } from "../index.ts";
|
||||||
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
|
|
||||||
|
import { getMsalAuthProvider, getAzureIdentityAuthProvider } from "./auth.ts";
|
||||||
|
|
||||||
|
export async function getGraphClient(): Promise<Client> {
|
||||||
|
const config = await loadConfig();
|
||||||
|
|
||||||
|
const authConfig = await loadAuthConfig("public-config");
|
||||||
|
const authProvider =
|
||||||
|
config.authMode === "azure-identity"
|
||||||
|
? getAzureIdentityAuthProvider(authConfig.tenantId, authConfig.clientId)
|
||||||
|
: getMsalAuthProvider(authConfig.tenantId, authConfig.clientId);
|
||||||
|
|
||||||
|
return Client.init({
|
||||||
|
authProvider: authProvider,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
20
src/index.ts
20
src/index.ts
@@ -2,14 +2,10 @@
|
|||||||
|
|
||||||
import { validate as validateUuid } from "uuid";
|
import { validate as validateUuid } from "uuid";
|
||||||
import { getConfig } from "@slawek/sk-tools";
|
import { getConfig } from "@slawek/sk-tools";
|
||||||
|
import type { AuthConfig, Config } from "./types.ts";
|
||||||
|
|
||||||
type Config = {
|
export async function loadAuthConfig(configName: string): Promise<AuthConfig> {
|
||||||
tenantId: string;
|
if (configName.trim() === "") {
|
||||||
clientId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function loadConfig(configName: string): Promise<Config> {
|
|
||||||
if (typeof configName !== "string" || configName.trim() === "") {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid config name. Expected a non-empty string like "public-config" or "confidential-config".',
|
'Invalid config name. Expected a non-empty string like "public-config" or "confidential-config".',
|
||||||
);
|
);
|
||||||
@@ -34,3 +30,13 @@ export async function loadConfig(configName: string): Promise<Config> {
|
|||||||
clientId,
|
clientId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<Config> {
|
||||||
|
|
||||||
|
const json = (await getConfig("sk-az-tools", "config")) as Record<string, unknown>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeAccountUpn: typeof json.activeAccountUpn === "string" ? json.activeAccountUpn : undefined,
|
||||||
|
authMode: typeof json.authMode === "string" ? json.authMode : "msal"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
11
src/types.ts
Normal file
11
src/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export type AuthConfig = {
|
||||||
|
tenantId: string;
|
||||||
|
clientId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
activeAccountUpn: string | undefined;
|
||||||
|
authMode: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user