Compare commits
3 Commits
67dd2045e3
...
059590fde4
| Author | SHA1 | Date | |
|---|---|---|---|
| 059590fde4 | |||
| 63029d1119 | |||
| aa6f9e24f8 |
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
"@slawek/sk-tools": ">=0.1.0",
|
"@slawek/sk-tools": ">=0.1.0",
|
||||||
"azure-devops-node-api": "^15.1.2",
|
"azure-devops-node-api": "^15.1.2",
|
||||||
"minimatch": "^10.1.2",
|
"minimatch": "^10.1.2",
|
||||||
"open": "^10.1.0"
|
"open": "^10.1.0",
|
||||||
|
"semver": "^7.7.2",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"sk-az-tools": "dist/cli.js"
|
"sk-az-tools": "dist/cli.js"
|
||||||
@@ -154,6 +156,15 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@azure/identity/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@azure/logger": {
|
"node_modules/@azure/logger": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
|
||||||
@@ -233,6 +244,15 @@
|
|||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@azure/msal-node/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
@@ -1576,12 +1596,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
"azure-devops-node-api": "^15.1.2",
|
"azure-devops-node-api": "^15.1.2",
|
||||||
"minimatch": "^10.1.2",
|
"minimatch": "^10.1.2",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"semver": "^7.7.2"
|
"semver": "^7.7.2",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": ">=24.0.0",
|
"@types/node": ">=24.0.0",
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity";
|
import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity";
|
||||||
|
import type { AuthenticationResult } from "@azure/msal-node";
|
||||||
|
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts";
|
||||||
|
|
||||||
type CredentialType = "d" | "default" | "cs" | "clientSecret" | "dc" | "deviceCode";
|
type CredentialType = "d" | "default" | "cs" | "clientSecret" | "dc" | "deviceCode";
|
||||||
|
|
||||||
type CredentialOptions = {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId?: string;
|
|
||||||
clientSecret?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getCredential(
|
export async function getCredential(
|
||||||
credentialType: CredentialType,
|
credentialType: CredentialType,
|
||||||
options: CredentialOptions,
|
tenantId?: string,
|
||||||
|
clientId?: string,
|
||||||
|
clientSecret?: string,
|
||||||
): Promise<DefaultAzureCredential | ClientSecretCredential | DeviceCodeCredential> {
|
): Promise<DefaultAzureCredential | ClientSecretCredential | DeviceCodeCredential> {
|
||||||
switch (credentialType) {
|
switch (credentialType) {
|
||||||
case "d":
|
case "d":
|
||||||
@@ -20,26 +18,26 @@ export async function getCredential(
|
|||||||
return new DefaultAzureCredential();
|
return new DefaultAzureCredential();
|
||||||
case "cs":
|
case "cs":
|
||||||
case "clientSecret":
|
case "clientSecret":
|
||||||
if (!options.tenantId || !options.clientId || !options.clientSecret) {
|
if (!tenantId || !clientId || !clientSecret) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
|
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new ClientSecretCredential(
|
return new ClientSecretCredential(
|
||||||
options.tenantId,
|
tenantId,
|
||||||
options.clientId,
|
clientId,
|
||||||
options.clientSecret,
|
clientSecret,
|
||||||
);
|
);
|
||||||
case "dc":
|
case "dc":
|
||||||
case "deviceCode":
|
case "deviceCode":
|
||||||
if (!options.tenantId || !options.clientId) {
|
if (!tenantId || !clientId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"tenantId and clientId are required for DeviceCodeCredential",
|
"tenantId and clientId are required for DeviceCodeCredential",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new DeviceCodeCredential({
|
return new DeviceCodeCredential({
|
||||||
tenantId: options.tenantId,
|
tenantId,
|
||||||
clientId: options.clientId,
|
clientId,
|
||||||
userPromptCallback: (info) => {
|
userPromptCallback: (info) => {
|
||||||
console.log(info.message);
|
console.log(info.message);
|
||||||
},
|
},
|
||||||
@@ -48,3 +46,11 @@ export async function getCredential(
|
|||||||
throw new Error(`Unsupported credential type: ${credentialType}`);
|
throw new Error(`Unsupported credential type: ${credentialType}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function acquireResourceToken(
|
||||||
|
tenantId: string,
|
||||||
|
clientId: string,
|
||||||
|
resource: string,
|
||||||
|
): Promise<AuthenticationResult | null> {
|
||||||
|
return acquireResourceTokenPca(tenantId, clientId, resource);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { getCredential } from "./client-auth.ts";
|
export { getCredential } from "./client-auth.ts";
|
||||||
|
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts";
|
||||||
export {
|
export {
|
||||||
loginInteractive,
|
loginInteractive,
|
||||||
loginDeviceCode,
|
loginDeviceCode,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
parseResources,
|
parseResources,
|
||||||
acquireResourceTokenFromLogin,
|
|
||||||
} from "./pca-auth.ts";
|
} from "./pca-auth.ts";
|
||||||
|
|
||||||
|
export async function acquireResourceToken(
|
||||||
|
tenantId: string,
|
||||||
|
clientId: string,
|
||||||
|
resource: string,
|
||||||
|
) {
|
||||||
|
return acquireResourceTokenPca(tenantId, clientId, resource);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
import open, { apps } from "open";
|
import open, { apps } from "open";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
import { writeFile, mkdir, unlink } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { PublicClientApplication } from "@azure/msal-node";
|
import { PublicClientApplication } from "@azure/msal-node";
|
||||||
|
import { getConfig, getConfigDir } from "@slawek/sk-tools";
|
||||||
import type {
|
import type {
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
AuthenticationResult,
|
AuthenticationResult,
|
||||||
ICachePlugin,
|
ICachePlugin,
|
||||||
TokenCacheContext,
|
TokenCacheContext,
|
||||||
} from "@azure/msal-node";
|
} from "@azure/msal-node";
|
||||||
import os from "node:os";
|
|
||||||
|
|
||||||
const RESOURCE_SCOPE_BY_NAME = {
|
const RESOURCE_SCOPE_BY_NAME = {
|
||||||
graph: "https://graph.microsoft.com/.default",
|
graph: "https://graph.microsoft.com/.default",
|
||||||
@@ -27,96 +27,32 @@ 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[]>;
|
||||||
const CHROMIUM_BROWSERS = new Set(["edge", "chrome", "brave"]);
|
const CHROMIUM_BROWSERS = new Set(["edge", "chrome", "brave"]);
|
||||||
|
const CONFIG_FILE_NAME = "config";
|
||||||
|
|
||||||
type SessionState = {
|
type SessionState = {
|
||||||
activeAccountUpn: string | null;
|
activeAccountUpn: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BrowserOptions = {
|
|
||||||
browser?: string;
|
|
||||||
browserProfile?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoginInteractiveOptions = {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId?: string;
|
|
||||||
scopes: string[];
|
|
||||||
showAuthUrlOnly?: boolean;
|
|
||||||
browser?: string;
|
|
||||||
browserProfile?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoginDeviceCodeOptions = {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId?: string;
|
|
||||||
scopes: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoginOptions = {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId?: string;
|
|
||||||
resourcesCsv?: string;
|
|
||||||
useDeviceCode?: boolean;
|
|
||||||
noBrowser?: boolean;
|
|
||||||
browser?: string;
|
|
||||||
browserProfile?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AcquireResourceTokenOptions = {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId?: string;
|
|
||||||
resource?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LogoutOptions = {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId?: string;
|
|
||||||
clearAll?: boolean;
|
|
||||||
userPrincipalName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getCacheRoot(): string {
|
|
||||||
const isWindows = process.platform === "win32";
|
|
||||||
const userRoot = isWindows
|
|
||||||
? process.env.LOCALAPPDATA || os.homedir()
|
|
||||||
: os.homedir();
|
|
||||||
|
|
||||||
return isWindows
|
|
||||||
? path.join(userRoot, "sk-az-tools")
|
|
||||||
: path.join(userRoot, ".config", "sk-az-tools");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSessionFilePath(): string {
|
|
||||||
return path.join(getCacheRoot(), "login-session.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readSessionState(): Promise<SessionState> {
|
async function readSessionState(): Promise<SessionState> {
|
||||||
try {
|
const parsed = (await getConfig("sk-az-tools", CONFIG_FILE_NAME)) as { activeAccountUpn?: unknown };
|
||||||
const sessionJson = await readFile(getSessionFilePath(), "utf8");
|
|
||||||
const parsed = JSON.parse(sessionJson) as { activeAccountUpn?: unknown };
|
|
||||||
return {
|
return {
|
||||||
activeAccountUpn:
|
activeAccountUpn:
|
||||||
typeof parsed?.activeAccountUpn === "string"
|
typeof parsed?.activeAccountUpn === "string"
|
||||||
? parsed.activeAccountUpn
|
? parsed.activeAccountUpn
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
|
||||||
if ((err as { code?: string } | null)?.code === "ENOENT") {
|
|
||||||
return { activeAccountUpn: null };
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeSessionState(state: SessionState): Promise<void> {
|
async function writeSessionState(state: SessionState): Promise<void> {
|
||||||
const sessionPath = getSessionFilePath();
|
const sessionPath = path.join(getConfigDir("sk-az-tools"), `${CONFIG_FILE_NAME}.json`);
|
||||||
await mkdir(path.dirname(sessionPath), { recursive: true });
|
await mkdir(path.dirname(sessionPath), { recursive: true });
|
||||||
await writeFile(sessionPath, JSON.stringify(state, null, 2), "utf8");
|
await writeFile(sessionPath, JSON.stringify(state, null, 2), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearSessionState(): Promise<void> {
|
async function clearSessionState(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await unlink(getSessionFilePath());
|
const sessionPath = path.join(getConfigDir("sk-az-tools"), `${CONFIG_FILE_NAME}.json`);
|
||||||
|
await unlink(sessionPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ((err as { code?: string } | null)?.code !== "ENOENT") {
|
if ((err as { code?: string } | null)?.code !== "ENOENT") {
|
||||||
throw err;
|
throw err;
|
||||||
@@ -124,10 +60,6 @@ async function clearSessionState(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUpn(upn: unknown): string {
|
|
||||||
return typeof upn === "string" ? upn.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeStderr(message: string): void {
|
function writeStderr(message: string): void {
|
||||||
process.stderr.write(`${message}\n`);
|
process.stderr.write(`${message}\n`);
|
||||||
}
|
}
|
||||||
@@ -165,7 +97,7 @@ function getBrowserKeyword(browser?: string): string {
|
|||||||
return keyword.toLowerCase();
|
return keyword.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBrowserOpenOptions({ browser, browserProfile }: BrowserOptions): Parameters<typeof open>[1] {
|
function getBrowserOpenOptions(browser?: string, browserProfile?: string): Parameters<typeof open>[1] {
|
||||||
const browserName = getBrowserAppName(browser);
|
const browserName = getBrowserAppName(browser);
|
||||||
|
|
||||||
if (!browserProfile || browserProfile.trim() === "") {
|
if (!browserProfile || browserProfile.trim() === "") {
|
||||||
@@ -194,7 +126,7 @@ function getBrowserOpenOptions({ browser, browserProfile }: BrowserOptions): Par
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateBrowserOptions({ browser, browserProfile }: BrowserOptions): void {
|
function validateBrowserOptions(browser?: string, browserProfile?: string): void {
|
||||||
if (browser && browser.trim() !== "") {
|
if (browser && browser.trim() !== "") {
|
||||||
getBrowserAppName(browser);
|
getBrowserAppName(browser);
|
||||||
}
|
}
|
||||||
@@ -246,8 +178,8 @@ function fileCachePlugin(cachePath: string): ICachePlugin {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPca({ tenantId, clientId }: { tenantId: string; clientId: string }): Promise<PublicClientApplication> {
|
async function createPca(tenantId: string, clientId: string): Promise<PublicClientApplication> {
|
||||||
const cacheRoot = getCacheRoot();
|
const cacheRoot = getConfigDir("sk-az-tools");
|
||||||
const cachePath = path.join(cacheRoot, `${clientId}-msal.cache`);
|
const cachePath = path.join(cacheRoot, `${clientId}-msal.cache`);
|
||||||
let cachePlugin: ICachePlugin;
|
let cachePlugin: ICachePlugin;
|
||||||
try {
|
try {
|
||||||
@@ -281,15 +213,11 @@ async function createPca({ tenantId, clientId }: { tenantId: string; clientId: s
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acquireTokenWithCache({
|
async function acquireTokenWithCache(
|
||||||
pca,
|
pca: PublicClientApplication,
|
||||||
scopes,
|
scopes: string[],
|
||||||
account,
|
account?: AccountInfo | null,
|
||||||
}: {
|
): Promise<AuthenticationResult | null> {
|
||||||
pca: PublicClientApplication;
|
|
||||||
scopes: string[];
|
|
||||||
account?: AccountInfo | null;
|
|
||||||
}): Promise<AuthenticationResult | null> {
|
|
||||||
if (account) {
|
if (account) {
|
||||||
try {
|
try {
|
||||||
return await pca.acquireTokenSilent({
|
return await pca.acquireTokenSilent({
|
||||||
@@ -316,43 +244,40 @@ async function acquireTokenWithCache({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findAccountByUpn({
|
async function findAccountByUpn(
|
||||||
pca,
|
pca: PublicClientApplication,
|
||||||
upn,
|
upn: string,
|
||||||
}: {
|
): Promise<AccountInfo | null> {
|
||||||
pca: PublicClientApplication;
|
const normalized = upn.trim().toLowerCase();
|
||||||
upn: string | null;
|
|
||||||
}): Promise<AccountInfo | null> {
|
|
||||||
const normalized = normalizeUpn(upn);
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await pca.getTokenCache().getAllAccounts();
|
const accounts = await pca.getTokenCache().getAllAccounts();
|
||||||
return (
|
return (
|
||||||
accounts.find((account) => normalizeUpn(account?.username) === normalized) ??
|
accounts.find((account) => account.username.trim().toLowerCase() === normalized) ??
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginInteractive({
|
export async function loginInteractive(
|
||||||
tenantId,
|
tenantId: string | undefined,
|
||||||
clientId,
|
clientId: string | undefined,
|
||||||
scopes,
|
scopes: string[],
|
||||||
showAuthUrlOnly = false,
|
showAuthUrlOnly = false,
|
||||||
browser,
|
browser?: string,
|
||||||
browserProfile,
|
browserProfile?: string,
|
||||||
}: LoginInteractiveOptions): 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 (!Array.isArray(scopes) || scopes.length === 0) {
|
if (!Array.isArray(scopes) || scopes.length === 0) {
|
||||||
throw new Error("scopes[] is required");
|
throw new Error("scopes[] is required");
|
||||||
}
|
}
|
||||||
validateBrowserOptions({ browser, browserProfile });
|
validateBrowserOptions(browser, browserProfile);
|
||||||
|
|
||||||
const pca = await createPca({ tenantId, clientId });
|
const pca = await createPca(tenantId, clientId);
|
||||||
|
|
||||||
const cached = await acquireTokenWithCache({ pca, scopes });
|
const cached = await acquireTokenWithCache(pca, scopes);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
return pca.acquireTokenInteractive({
|
return pca.acquireTokenInteractive({
|
||||||
@@ -362,7 +287,7 @@ export async function loginInteractive({
|
|||||||
writeStderr(`Visit:\n${url}`);
|
writeStderr(`Visit:\n${url}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const options = getBrowserOpenOptions({ browser, browserProfile });
|
const options = getBrowserOpenOptions(browser, browserProfile);
|
||||||
await open(url, options).catch(() => {
|
await open(url, options).catch(() => {
|
||||||
writeStderr(`Visit:\n${url}`);
|
writeStderr(`Visit:\n${url}`);
|
||||||
});
|
});
|
||||||
@@ -370,16 +295,20 @@ export async function loginInteractive({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginDeviceCode({ tenantId, clientId, scopes }: LoginDeviceCodeOptions): Promise<AuthenticationResult | null> {
|
export async function loginDeviceCode(
|
||||||
|
tenantId: string | undefined,
|
||||||
|
clientId: string | undefined,
|
||||||
|
scopes: string[],
|
||||||
|
): 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 (!Array.isArray(scopes) || scopes.length === 0) {
|
if (!Array.isArray(scopes) || scopes.length === 0) {
|
||||||
throw new Error("scopes[] is required");
|
throw new Error("scopes[] is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pca = await createPca({ tenantId, clientId });
|
const pca = await createPca(tenantId, clientId);
|
||||||
|
|
||||||
const cached = await acquireTokenWithCache({ pca, scopes });
|
const cached = await acquireTokenWithCache(pca, scopes);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
return pca.acquireTokenByDeviceCode({
|
return pca.acquireTokenByDeviceCode({
|
||||||
@@ -390,15 +319,15 @@ export async function loginDeviceCode({ tenantId, clientId, scopes }: LoginDevic
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login({
|
export async function login(
|
||||||
tenantId,
|
tenantId: string | undefined,
|
||||||
clientId,
|
clientId: string | undefined,
|
||||||
resourcesCsv,
|
resourcesCsv?: string,
|
||||||
useDeviceCode = false,
|
useDeviceCode = false,
|
||||||
noBrowser = false,
|
noBrowser = false,
|
||||||
browser,
|
browser?: string,
|
||||||
browserProfile,
|
browserProfile?: string,
|
||||||
}: LoginOptions): Promise<{
|
): Promise<{
|
||||||
accountUpn: string | null;
|
accountUpn: string | null;
|
||||||
resources: Array<{ resource: string; expiresOn: string | null }>;
|
resources: Array<{ resource: string; expiresOn: string | null }>;
|
||||||
flow: "device-code" | "interactive";
|
flow: "device-code" | "interactive";
|
||||||
@@ -406,27 +335,22 @@ export async function login({
|
|||||||
}> {
|
}> {
|
||||||
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");
|
||||||
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 = resources.map((resourceName) => RESOURCE_SCOPE_BY_NAME[resourceName]);
|
||||||
const pca = await createPca({ tenantId, clientId });
|
const pca = await createPca(tenantId, clientId);
|
||||||
const session = await readSessionState();
|
const session = await readSessionState();
|
||||||
const preferredAccount = await findAccountByUpn({
|
const preferredAccount = session.activeAccountUpn
|
||||||
pca,
|
? await findAccountByUpn(pca, session.activeAccountUpn)
|
||||||
upn: session.activeAccountUpn,
|
: null;
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
for (let index = 0; index < resources.length; index += 1) {
|
||||||
const resource = resources[index];
|
const resource = resources[index];
|
||||||
const scope = [scopes[index]];
|
const scope = [scopes[index]];
|
||||||
let token = await acquireTokenWithCache({
|
let token = await acquireTokenWithCache(pca, scope, selectedAccount);
|
||||||
pca,
|
|
||||||
scopes: scope,
|
|
||||||
account: selectedAccount,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
if (useDeviceCode) {
|
if (useDeviceCode) {
|
||||||
@@ -444,7 +368,7 @@ export async function login({
|
|||||||
writeStderr(`Visit:\n${url}`);
|
writeStderr(`Visit:\n${url}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const options = getBrowserOpenOptions({ browser, browserProfile });
|
const options = getBrowserOpenOptions(browser, browserProfile);
|
||||||
await open(url, options).catch(() => {
|
await open(url, options).catch(() => {
|
||||||
writeStderr(`Visit:\n${url}`);
|
writeStderr(`Visit:\n${url}`);
|
||||||
});
|
});
|
||||||
@@ -476,11 +400,11 @@ export async function login({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acquireResourceTokenFromLogin({
|
export async function acquireResourceToken(
|
||||||
tenantId,
|
tenantId: string,
|
||||||
clientId,
|
clientId: string,
|
||||||
resource,
|
resource: string,
|
||||||
}: AcquireResourceTokenOptions): 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 (!resource) throw new Error("resource is required");
|
||||||
@@ -496,11 +420,8 @@ export async function acquireResourceTokenFromLogin({
|
|||||||
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pca = await createPca({ tenantId, clientId });
|
const pca = await createPca(tenantId, clientId);
|
||||||
const account = await findAccountByUpn({
|
const account = await findAccountByUpn(pca, session.activeAccountUpn);
|
||||||
pca,
|
|
||||||
upn: session.activeAccountUpn,
|
|
||||||
});
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
||||||
}
|
}
|
||||||
@@ -515,16 +436,16 @@ export async function acquireResourceTokenFromLogin({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout({
|
export async function logout(
|
||||||
tenantId,
|
tenantId: string,
|
||||||
clientId,
|
clientId: string,
|
||||||
clearAll = false,
|
clearAll = false,
|
||||||
userPrincipalName,
|
userPrincipalName?: string,
|
||||||
}: LogoutOptions): Promise<{ clearedAll: boolean; signedOut: string[] }> {
|
): Promise<{ clearedAll: boolean; signedOut: string[] }> {
|
||||||
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");
|
||||||
|
|
||||||
const pca = await createPca({ tenantId, clientId });
|
const pca = await createPca(tenantId, clientId);
|
||||||
const tokenCache = pca.getTokenCache();
|
const tokenCache = pca.getTokenCache();
|
||||||
const accounts = await tokenCache.getAllAccounts();
|
const accounts = await tokenCache.getAllAccounts();
|
||||||
const session = await readSessionState();
|
const session = await readSessionState();
|
||||||
@@ -540,9 +461,10 @@ export async function logout({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetUpn = normalizeUpn(userPrincipalName) || normalizeUpn(session.activeAccountUpn);
|
const targetUpn = (typeof userPrincipalName === "string" ? userPrincipalName.trim().toLowerCase() : "")
|
||||||
|
|| (typeof session.activeAccountUpn === "string" ? session.activeAccountUpn.trim().toLowerCase() : "");
|
||||||
const accountToSignOut = accounts.find(
|
const accountToSignOut = accounts.find(
|
||||||
(account) => normalizeUpn(account.username) === targetUpn,
|
(account) => account.username.trim().toLowerCase() === targetUpn,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!accountToSignOut) {
|
if (!accountToSignOut) {
|
||||||
|
|||||||
1
src/cli/commands/auth.ts
Normal file
1
src/cli/commands/auth.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { acquireResourceTokenFromLogin } from "../../azure/index.ts";
|
import { acquireResourceToken } from "../../azure/index.ts";
|
||||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||||
import { loadPublicConfig } from "../../index.ts";
|
import { loadConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -19,20 +19,14 @@ export async function runGetTokenCommand(values: CommandValues): Promise<unknown
|
|||||||
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 loadPublicConfig();
|
const config = await loadConfig("public-config");
|
||||||
if (!config.tenantId) {
|
|
||||||
throw new Error("tenantId is required");
|
|
||||||
}
|
|
||||||
if (!config.clientId) {
|
|
||||||
throw new Error("clientId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenType === "azurerm") {
|
if (tokenType === "azurerm") {
|
||||||
const result = await acquireResourceTokenFromLogin({
|
const result = await acquireResourceToken(
|
||||||
tenantId: config.tenantId,
|
config.tenantId,
|
||||||
clientId: config.clientId,
|
config.clientId,
|
||||||
resource: "arm",
|
"arm",
|
||||||
});
|
);
|
||||||
|
|
||||||
const accessToken = result?.accessToken;
|
const accessToken = result?.accessToken;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ Options:
|
|||||||
|
|
||||||
export async function runListAppsCommand(values: CommandValues): Promise<unknown> {
|
export async function runListAppsCommand(values: CommandValues): Promise<unknown> {
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
const { client } = await getGraphClientFromPublicConfig();
|
||||||
let result = await listApps(client, {
|
let result = await listApps(client, values["display-name"], values["app-id"]);
|
||||||
displayName: values["display-name"],
|
|
||||||
appId: 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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ export async function runListResourcePermissionsCommand(values: CommandValues):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
const { client } = await getGraphClientFromPublicConfig();
|
||||||
let result = await listResourcePermissions(client, {
|
let result = await listResourcePermissions(
|
||||||
appId: values["app-id"],
|
client,
|
||||||
displayName: values["display-name"],
|
values["app-id"],
|
||||||
});
|
values["display-name"],
|
||||||
|
);
|
||||||
if (values.filter) {
|
if (values.filter) {
|
||||||
result = filterByPermissionName(result, values.filter);
|
result = filterByPermissionName(result, values.filter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { loadPublicConfig } from "../../index.ts";
|
import { loadConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -17,14 +17,14 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runLoginCommand(values: CommandValues): Promise<unknown> {
|
export async function runLoginCommand(values: CommandValues): Promise<unknown> {
|
||||||
const config = await loadPublicConfig();
|
const config = await loadConfig("public-config");
|
||||||
return login({
|
return login(
|
||||||
tenantId: config.tenantId,
|
config.tenantId,
|
||||||
clientId: config.clientId,
|
config.clientId,
|
||||||
resourcesCsv: values.resources,
|
values.resources,
|
||||||
useDeviceCode: Boolean(values["use-device-code"]),
|
Boolean(values["use-device-code"]),
|
||||||
noBrowser: Boolean(values["no-browser"]),
|
Boolean(values["no-browser"]),
|
||||||
browser: values.browser,
|
values.browser,
|
||||||
browserProfile: values["browser-profile"],
|
values["browser-profile"],
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { loadPublicConfig } from "../../index.ts";
|
import { loadConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -13,10 +13,6 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runLogoutCommand(values: CommandValues): Promise<unknown> {
|
export async function runLogoutCommand(values: CommandValues): Promise<unknown> {
|
||||||
const config = await loadPublicConfig();
|
const config = await loadConfig("public-config");
|
||||||
return logout({
|
return logout(config.tenantId, config.clientId, Boolean(values.all));
|
||||||
tenantId: config.tenantId,
|
|
||||||
clientId: config.clientId,
|
|
||||||
clearAll: Boolean(values.all),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { acquireResourceTokenFromLogin } from "../../azure/index.ts";
|
import { acquireResourceToken } from "../../azure/index.ts";
|
||||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
import { getDevOpsApiToken } from "../../devops/index.ts";
|
||||||
import { loadPublicConfig } from "../../index.ts";
|
import { loadConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
import type { CommandValues } from "./types.ts";
|
||||||
|
|
||||||
@@ -54,20 +54,14 @@ async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await loadPublicConfig();
|
const config = await loadConfig("public-config");
|
||||||
if (!config.tenantId) {
|
|
||||||
throw new Error("tenantId is required");
|
|
||||||
}
|
|
||||||
if (!config.clientId) {
|
|
||||||
throw new Error("clientId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host === "management.azure.com") {
|
if (host === "management.azure.com") {
|
||||||
const result = await acquireResourceTokenFromLogin({
|
const result = await acquireResourceToken(
|
||||||
tenantId: config.tenantId,
|
config.tenantId,
|
||||||
clientId: config.clientId,
|
config.clientId,
|
||||||
resource: "arm",
|
"arm",
|
||||||
});
|
);
|
||||||
const accessToken = result?.accessToken;
|
const accessToken = result?.accessToken;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new Error("Failed to obtain AzureRM token");
|
throw new Error("Failed to obtain AzureRM token");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { minimatch } from "minimatch";
|
import { minimatch } from "minimatch";
|
||||||
|
|
||||||
import { loadPublicConfig } from "../../index.ts";
|
import { loadConfig } from "../../index.ts";
|
||||||
import { getGraphClient } from "../../graph/auth.ts";
|
import { getGraphClient } from "../../graph/auth.ts";
|
||||||
|
|
||||||
type PermissionRow = {
|
type PermissionRow = {
|
||||||
@@ -28,9 +28,6 @@ export function filterByDisplayName<T extends DisplayNameRow>(rows: T[], pattern
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getGraphClientFromPublicConfig(): Promise<{ client: any }> {
|
export async function getGraphClientFromPublicConfig(): Promise<{ client: any }> {
|
||||||
const config = await loadPublicConfig();
|
const config = await loadConfig("public-config");
|
||||||
return getGraphClient({
|
return getGraphClient(config.tenantId, config.clientId);
|
||||||
tenantId: config.tenantId,
|
|
||||||
clientId: config.clientId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,21 +8,16 @@ import readline from "node:readline";
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
|
|
||||||
type RunAzOptions = {
|
|
||||||
quiet?: boolean;
|
|
||||||
allowFailure?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RunAzResult = {
|
type RunAzResult = {
|
||||||
status: number;
|
status: number;
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function runAz(args: string[], options: RunAzOptions = {}): RunAzResult {
|
function runAz(args: string[], quiet = false, allowFailure = false): RunAzResult {
|
||||||
const result = spawnSync("az", args, {
|
const result = spawnSync("az", args, {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
stdio: options.quiet
|
stdio: quiet
|
||||||
? ["ignore", "ignore", "ignore"]
|
? ["ignore", "ignore", "ignore"]
|
||||||
: ["ignore", "pipe", "pipe"],
|
: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
@@ -31,7 +26,7 @@ function runAz(args: string[], options: RunAzOptions = {}): RunAzResult {
|
|||||||
throw result.error;
|
throw result.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.status !== 0 && options.allowFailure !== true) {
|
if (result.status !== 0 && allowFailure !== true) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
(result.stderr || "").trim() || `az ${args.join(" ")} failed`,
|
(result.stderr || "").trim() || `az ${args.join(" ")} failed`,
|
||||||
);
|
);
|
||||||
@@ -198,7 +193,7 @@ Options:
|
|||||||
"--enable-id-token-issuance",
|
"--enable-id-token-issuance",
|
||||||
"true",
|
"true",
|
||||||
],
|
],
|
||||||
{ quiet: true },
|
true,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -210,14 +205,12 @@ Options:
|
|||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
runAz(["ad", "sp", "create", "--id", appId], {
|
runAz(["ad", "sp", "create", "--id", appId], true, true);
|
||||||
quiet: true,
|
|
||||||
allowFailure: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const adminConsentResult = runAz(
|
const adminConsentResult = runAz(
|
||||||
["ad", "app", "permission", "admin-consent", "--id", appId],
|
["ad", "app", "permission", "admin-consent", "--id", appId],
|
||||||
{ quiet: true, allowFailure: true },
|
true,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
if (adminConsentResult.status !== 0) {
|
if (adminConsentResult.status !== 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ type LoginInteractiveResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getDevOpsApiToken(tenantId: string, clientId: string): Promise<string> {
|
export async function getDevOpsApiToken(tenantId: string, clientId: string): Promise<string> {
|
||||||
const result = await loginInteractive({
|
const result = await loginInteractive(
|
||||||
tenantId,
|
tenantId,
|
||||||
clientId,
|
clientId,
|
||||||
scopes: AZURE_DEVOPS_SCOPES,
|
AZURE_DEVOPS_SCOPES,
|
||||||
}) as LoginInteractiveResult;
|
) as LoginInteractiveResult;
|
||||||
|
|
||||||
const accessToken = result?.accessToken;
|
const accessToken = result?.accessToken;
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ type GraphResult<T = GraphObject> = {
|
|||||||
value?: T[];
|
value?: T[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppQueryOptions = {
|
|
||||||
displayName?: string;
|
|
||||||
appId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RequiredResourceAccessItem = {
|
type RequiredResourceAccessItem = {
|
||||||
type?: string;
|
type?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -38,11 +33,6 @@ type ServicePrincipal = {
|
|||||||
appRoles?: GraphPermission[];
|
appRoles?: GraphPermission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResourcePermissionsOptions = {
|
|
||||||
appId?: string;
|
|
||||||
displayName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getApp(client: any, displayName: string): Promise<GraphObject | null> {
|
export async function getApp(client: any, displayName: string): Promise<GraphObject | null> {
|
||||||
const result = await client
|
const result = await client
|
||||||
.api("/applications")
|
.api("/applications")
|
||||||
@@ -68,8 +58,11 @@ export async function deleteApp(client: any, appObjectId: string): Promise<void>
|
|||||||
await client.api(`/applications/${appObjectId}`).delete();
|
await client.api(`/applications/${appObjectId}`).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listApps(client: any, options: AppQueryOptions = {}): Promise<GraphObject[]> {
|
export async function listApps(
|
||||||
const { displayName, appId } = options;
|
client: any,
|
||||||
|
displayName?: string,
|
||||||
|
appId?: string,
|
||||||
|
): Promise<GraphObject[]> {
|
||||||
let request = client.api("/applications");
|
let request = client.api("/applications");
|
||||||
const filters: string[] = [];
|
const filters: string[] = [];
|
||||||
|
|
||||||
@@ -219,8 +212,11 @@ export async function listAppGrants(client: any, appId: string): Promise<GraphOb
|
|||||||
return Array.isArray(grantsResult?.value) ? grantsResult.value : [];
|
return Array.isArray(grantsResult?.value) ? grantsResult.value : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listResourcePermissions(client: any, options: ResourcePermissionsOptions = {}): Promise<Array<Record<string, unknown>>> {
|
export async function listResourcePermissions(
|
||||||
const { appId, displayName } = options;
|
client: any,
|
||||||
|
appId?: string,
|
||||||
|
displayName?: string,
|
||||||
|
): Promise<Array<Record<string, unknown>>> {
|
||||||
if (!appId && !displayName) {
|
if (!appId && !displayName) {
|
||||||
throw new Error("appId or displayName is required");
|
throw new Error("appId or displayName is required");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { Client } from "@microsoft/microsoft-graph-client";
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
import { acquireResourceTokenFromLogin } from "../azure/index.ts";
|
import { acquireResourceToken } from "../azure/index.ts";
|
||||||
|
|
||||||
type GraphClientOptions = {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type GraphApiToken = {
|
type GraphApiToken = {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getGraphClient({ tenantId, clientId }: GraphClientOptions): Promise<{ graphApiToken: GraphApiToken; client: any }> {
|
export async function getGraphClient(
|
||||||
const graphApiToken = await acquireResourceTokenFromLogin({
|
tenantId: string,
|
||||||
tenantId,
|
clientId: string,
|
||||||
clientId,
|
): Promise<{ graphApiToken: GraphApiToken; client: any }> {
|
||||||
resource: "graph",
|
const graphApiToken = await acquireResourceToken(tenantId, clientId, "graph") as GraphApiToken;
|
||||||
}) as GraphApiToken;
|
|
||||||
|
|
||||||
const client = Client.init({
|
const client = Client.init({
|
||||||
authProvider: (done) => {
|
authProvider: (done) => {
|
||||||
|
|||||||
60
src/index.ts
60
src/index.ts
@@ -1,58 +1,36 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { readFile } from "node:fs/promises";
|
import { validate as validateUuid } from "uuid";
|
||||||
import os from "node:os";
|
import { getConfig } from "@slawek/sk-tools";
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
tenantId?: string;
|
tenantId: string;
|
||||||
clientId?: string;
|
clientId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfigCandidate = {
|
export async function loadConfig(configName: string): Promise<Config> {
|
||||||
tenantId?: unknown;
|
if (typeof configName !== "string" || configName.trim() === "") {
|
||||||
clientId?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getUserConfigDir(): string {
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
return process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
|
|
||||||
}
|
|
||||||
|
|
||||||
return process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadConfig(configFileName: string): Promise<Config> {
|
|
||||||
if (typeof configFileName !== "string" || configFileName.trim() === "") {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid config file name. Expected a non-empty string like "public-config.json" or "confidential-config.json".',
|
'Invalid config name. Expected a non-empty string like "public-config" or "confidential-config".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envConfig: Config = {
|
const envConfig = {
|
||||||
tenantId: process.env.AZURE_TENANT_ID,
|
tenantId: process.env.AZURE_TENANT_ID,
|
||||||
clientId: process.env.AZURE_CLIENT_ID,
|
clientId: process.env.AZURE_CLIENT_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
const configPath = path.join(getUserConfigDir(), "sk-az-tools", configFileName);
|
const json = (await getConfig("sk-az-tools", configName)) as Record<string, unknown>;
|
||||||
return readFile(configPath, "utf8")
|
|
||||||
.then((configJson) => JSON.parse(configJson) as ConfigCandidate)
|
const tenantId = (typeof json.tenantId === "string" && json.tenantId ? json.tenantId : envConfig.tenantId) ?? "";
|
||||||
.catch((err: unknown) => {
|
const clientId = (typeof json.clientId === "string" && json.clientId ? json.clientId : envConfig.clientId) ?? "";
|
||||||
if ((err as { code?: string } | null)?.code === "ENOENT") {
|
|
||||||
return {} as ConfigCandidate;
|
if (!validateUuid(tenantId ?? "") || !validateUuid(clientId ?? "")) {
|
||||||
}
|
throw new Error("tenantId and clientId must be valid GUIDs.");
|
||||||
throw err;
|
|
||||||
})
|
|
||||||
.then((json) => ({
|
|
||||||
tenantId: typeof json.tenantId === "string" && json.tenantId ? json.tenantId : envConfig.tenantId,
|
|
||||||
clientId: typeof json.clientId === "string" && json.clientId ? json.clientId : envConfig.clientId,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPublicConfig(): Promise<Config> {
|
return {
|
||||||
return loadConfig("public-config.json");
|
tenantId,
|
||||||
}
|
clientId,
|
||||||
|
};
|
||||||
export function loadConfidentialConfig(): Promise<Config> {
|
|
||||||
return loadConfig("confidential-config.json");
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user