Compare commits
22 Commits
059590fde4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fc99f62c3 | |||
| d6adb5a3ba | |||
| 4dd3056b2f | |||
| 3e2b54ba3c | |||
| 0269313516 | |||
| ade8f065e0 | |||
| 1bae7d8f85 | |||
| 0714fc5c1b | |||
| d8d72be7e9 | |||
| b678dd5ace | |||
| d69402a33d | |||
| 2fa8fcfc3c | |||
| 059fc3c1da | |||
| 97f7011f97 | |||
| dda13b7e2a | |||
| 5265e5300c | |||
| 9fd770999b | |||
| a98c77cd2e | |||
| 0829b35113 | |||
| 63eb9c3cad | |||
| cd119c90c2 | |||
| 88ac901222 |
@@ -6,18 +6,34 @@ The `sk-az-tools` package may act as a CLI tool that provides various commands f
|
|||||||
- Azure Resource Manager
|
- Azure Resource Manager
|
||||||
- Azure DevOps Services
|
- Azure DevOps Services
|
||||||
|
|
||||||
|
## Global Options
|
||||||
|
|
||||||
|
These options apply to all commands unless stated otherwise:
|
||||||
|
|
||||||
|
- `--query`, `-q` <jmespath> - Apply JMESPath filter before output rendering.
|
||||||
|
- `--output`, `-o` <format> - Output format: `table|t|alignedtable|at|prettytable|pt|tsv`.
|
||||||
|
- `--columns`, `-C` <definition> - Column selection for table outputs:
|
||||||
|
- `col1` - Select column (case-insensitive match), keep raw header label.
|
||||||
|
- `col1:` - Select column (case-insensitive match), use auto-generated header label.
|
||||||
|
- `col1: Label 1` - Select column (case-insensitive match), use custom header label.
|
||||||
|
- Prefix token with `=` for exact column-name match: `=col1`, `=col1:`, `=col1:Label`.
|
||||||
|
- Tokens are comma-separated and rendered in the specified order.
|
||||||
|
- `--help`, `-h` - Show command help.
|
||||||
|
|
||||||
|
Note: `rest --header` is a command-specific HTTP header option and is unrelated to `--columns`.
|
||||||
|
|
||||||
## Login
|
## Login
|
||||||
|
|
||||||
**Command name:** `login`
|
**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:**
|
**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.
|
- `--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.
|
- `--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`).
|
- `--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.
|
**Description:** The `login` command authenticates user sign-in for selected resource audiences and caches tokens for subsequent commands.
|
||||||
|
|||||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -1,20 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.4.5",
|
"version": "0.8.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.4.3",
|
"version": "0.8.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/identity": "^4.13.0",
|
"@azure/identity": "^4.13.0",
|
||||||
"@azure/msal-node": "^5.0.3",
|
"@azure/msal-node": "^5.0.3",
|
||||||
"@azure/msal-node-extensions": "^1.2.0",
|
"@azure/msal-node-extensions": "^1.2.0",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"@slawek/sk-tools": ">=0.1.0",
|
"@slawek/sk-tools": "^0.4.1",
|
||||||
"azure-devops-node-api": "^15.1.2",
|
"azure-devops-node-api": "^15.1.2",
|
||||||
|
"commander": "^14.0.3",
|
||||||
"minimatch": "^10.1.2",
|
"minimatch": "^10.1.2",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
@@ -290,13 +291,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@slawek/sk-tools": {
|
"node_modules/@slawek/sk-tools": {
|
||||||
"version": "0.1.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://gitea.koszewscy.waw.pl/api/packages/slawek/npm/%40slawek%2Fsk-tools/-/0.1.1/sk-tools-0.1.1.tgz",
|
"resolved": "https://gitea.koszewscy.waw.pl/api/packages/slawek/npm/%40slawek%2Fsk-tools/-/0.4.1/sk-tools-0.4.1.tgz",
|
||||||
"integrity": "sha512-DDG4d35VRGWD7kVpKre/jyXiWG+hgcJ8nrF38FInlWYEEciKb8gHSjqFvol2sQufKBTtvDtQTGbLIW+yqsehdA==",
|
"integrity": "sha512-rTw/m6ZK72HGELcCC+ze1sNcqt4LM5dBUdJ3c5UsOT95qTZAGauVBKsslpVd4Kotf24vNlGFQ1fpVDcT5sluwQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"commander": "^14.0.3",
|
||||||
"d3-dsv": "^3.0.1",
|
"d3-dsv": "^3.0.1",
|
||||||
"jmespath": "^0.16.0"
|
"jmespath": "^0.16.0",
|
||||||
|
"semver": "^7.7.4",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"sk-tools": "dist/cli.js"
|
"sk-tools": "dist/cli.js"
|
||||||
@@ -315,13 +319,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.11.0",
|
"version": "25.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
|
||||||
"integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==",
|
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typespec/ts-http-runtime": {
|
"node_modules/@typespec/ts-http-runtime": {
|
||||||
@@ -500,12 +504,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "7.2.0",
|
"version": "14.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-dsv": {
|
"node_modules/d3-dsv": {
|
||||||
@@ -533,6 +537,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-dsv/node_modules/commander": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -1583,9 +1596,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/sk-az-tools",
|
"name": "@slawek/sk-az-tools",
|
||||||
"version": "0.4.5",
|
"version": "0.8.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
||||||
|
"build:watch": "tsc --watch",
|
||||||
"create-pca": "node dist/create-pca.js",
|
"create-pca": "node dist/create-pca.js",
|
||||||
"bump-patch": "node scripts/bump-patch.mjs",
|
"bump-patch": "node scripts/bump-patch.mjs",
|
||||||
"make-deps": "node scripts/make-mermaid-func-deps.mjs",
|
"make-deps": "node scripts/make-mermaid-func-deps.mjs",
|
||||||
@@ -23,8 +24,9 @@
|
|||||||
"@azure/msal-node": "^5.0.3",
|
"@azure/msal-node": "^5.0.3",
|
||||||
"@azure/msal-node-extensions": "^1.2.0",
|
"@azure/msal-node-extensions": "^1.2.0",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"@slawek/sk-tools": ">=0.1.0",
|
"@slawek/sk-tools": "^0.4.1",
|
||||||
"azure-devops-node-api": "^15.1.2",
|
"azure-devops-node-api": "^15.1.2",
|
||||||
|
"commander": "^14.0.3",
|
||||||
"minimatch": "^10.1.2",
|
"minimatch": "^10.1.2",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
|
|||||||
60
scripts/check-package-version.mjs
Executable file
60
scripts/check-package-version.mjs
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { parseArgs } from "node:util";
|
||||||
|
import { inc } from "semver";
|
||||||
|
|
||||||
|
const skAzToolsPackagePath = resolve("package.json");
|
||||||
|
const skAzToolsPackageLockPath = resolve("package-lock.json");
|
||||||
|
const skToolsPackagePath = resolve("../sk-tools", "package.json");
|
||||||
|
|
||||||
|
const skAzToolsPackage = JSON.parse(readFileSync(skAzToolsPackagePath, "utf-8"));
|
||||||
|
const skAzToolsPackageLock = JSON.parse(readFileSync(skAzToolsPackageLockPath, "utf-8"));
|
||||||
|
const skToolsPackage = JSON.parse(readFileSync(skToolsPackagePath, "utf-8"));
|
||||||
|
|
||||||
|
const { values } = parseArgs({
|
||||||
|
options: {
|
||||||
|
update: { type: "boolean", short: "u", description: "Update @slawek/sk-tools to the latest version." },
|
||||||
|
bump: { type: "string", short: "b", description: "Bump the version of @slawek/sk-az-tools in package.json. Allowed values: major, minor, patch." }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (values.bump !== undefined && !["major", "minor", "patch"].includes(values.bump)) {
|
||||||
|
console.error(`Invalid bump type: ${values.bump}. Allowed values are: major, minor, patch.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package versions
|
||||||
|
console.log(`SK Tools version: ${skToolsPackage.version}`);
|
||||||
|
console.log(`SK Azure Tools version: ${skAzToolsPackage.version}\n`);
|
||||||
|
|
||||||
|
if (values.bump) {
|
||||||
|
const newVersion = inc(skAzToolsPackage.version, values.bump);
|
||||||
|
if (!newVersion) {
|
||||||
|
console.error(`Failed to bump version: ${skAzToolsPackage.version}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
skAzToolsPackage.version = newVersion;
|
||||||
|
writeFileSync(skAzToolsPackagePath, JSON.stringify(skAzToolsPackage, null, 4));
|
||||||
|
console.log(`Bumped SK Azure Tools version to: ${newVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`SK Azure Tools Locked version: ${skAzToolsPackageLock.version}`);
|
||||||
|
|
||||||
|
// Update package.json if --update flag is set
|
||||||
|
// or if the version of @slawek/sk-az-tools in package.json
|
||||||
|
// is different than the version in package-lock.json.
|
||||||
|
if (values.update || skAzToolsPackage.version !== skAzToolsPackageLock.version) {
|
||||||
|
console.log(`Updating package.json...`);
|
||||||
|
skAzToolsPackage.dependencies["@slawek/sk-tools"] = `>=${skToolsPackage.version}`;
|
||||||
|
writeFileSync(skAzToolsPackagePath, JSON.stringify(skAzToolsPackage, null, 4));
|
||||||
|
// Install and link the updated package
|
||||||
|
spawnSync("npm", ["install", "@slawek/sk-tools"], { stdio: "inherit" });
|
||||||
|
spawnSync("npm", ["link", "@slawek/sk-tools"], { stdio: "inherit" });
|
||||||
|
// Show the updated dependency tree
|
||||||
|
spawnSync("npm", ["ls"], { stdio: "inherit" });
|
||||||
|
} else {
|
||||||
|
console.log(`\nSK Tools version requested: ${skAzToolsPackage.dependencies["@slawek/sk-tools"] ?? "not found"}`);
|
||||||
|
}
|
||||||
0
scripts/make-mermaid-func-deps.mjs
Normal file → Executable file
0
scripts/make-mermaid-func-deps.mjs
Normal file → Executable file
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}'
|
||||||
@@ -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,14 @@
|
|||||||
* 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 { 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 {
|
export {
|
||||||
loginInteractive,
|
loginInteractive,
|
||||||
loginDeviceCode,
|
loginDeviceCode,
|
||||||
@@ -16,10 +22,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",
|
||||||
|
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", "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,
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,27 +14,22 @@ 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[]>;
|
||||||
const CHROMIUM_BROWSERS = new Set(["edge", "chrome", "brave"]);
|
const CHROMIUM_BROWSERS = new Set(["edge", "chrome", "brave"]);
|
||||||
const CONFIG_FILE_NAME = "config";
|
const SESSION_STATE_NAME = "session-state";
|
||||||
|
|
||||||
type SessionState = {
|
type SessionState = {
|
||||||
activeAccountUpn: string | null;
|
activeAccountUpn: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function readSessionState(): Promise<SessionState> {
|
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 {
|
return {
|
||||||
activeAccountUpn:
|
activeAccountUpn:
|
||||||
typeof parsed?.activeAccountUpn === "string"
|
typeof parsed?.activeAccountUpn === "string"
|
||||||
@@ -44,14 +39,14 @@ async function readSessionState(): Promise<SessionState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeSessionState(state: SessionState): Promise<void> {
|
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 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 {
|
||||||
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);
|
await unlink(sessionPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ((err as { code?: string } | null)?.code !== "ENOENT") {
|
if ((err as { code?: string } | null)?.code !== "ENOENT") {
|
||||||
@@ -109,12 +104,12 @@ function getBrowserOpenOptions(browser?: string, browserProfile?: string): Param
|
|||||||
const browserKeyword = getBrowserKeyword(browser);
|
const browserKeyword = getBrowserKeyword(browser);
|
||||||
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
|
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
|
||||||
throw new Error(
|
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) {
|
if (!browserName) {
|
||||||
throw new Error("--browser-profile requires --browser");
|
throw new Error("--browser-profile requires --browser-name");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -135,19 +130,18 @@ function validateBrowserOptions(browser?: string, browserProfile?: string): void
|
|||||||
const browserKeyword = getBrowserKeyword(browser);
|
const browserKeyword = getBrowserKeyword(browser);
|
||||||
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
|
if (!CHROMIUM_BROWSERS.has(browserKeyword)) {
|
||||||
throw new Error(
|
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[] {
|
export function parseResources(resourcesInput?: string[]): ResourceName[] {
|
||||||
if (!resourcesCsv || resourcesCsv.trim() === "") {
|
if (!resourcesInput || resourcesInput.length === 0) {
|
||||||
return [...DEFAULT_RESOURCES];
|
return [...DEFAULT_RESOURCES];
|
||||||
}
|
}
|
||||||
|
|
||||||
const resources = resourcesCsv
|
const resources = resourcesInput
|
||||||
.split(",")
|
|
||||||
.map((item) => item.trim().toLowerCase())
|
.map((item) => item.trim().toLowerCase())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
@@ -296,8 +290,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,9 +314,9 @@ export async function loginDeviceCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function login(
|
export async function login(
|
||||||
tenantId: string | undefined,
|
tenantId: string,
|
||||||
clientId: string | undefined,
|
clientId: string,
|
||||||
resourcesCsv?: string,
|
resourcesInput?: string[],
|
||||||
useDeviceCode = false,
|
useDeviceCode = false,
|
||||||
noBrowser = false,
|
noBrowser = false,
|
||||||
browser?: string,
|
browser?: string,
|
||||||
@@ -337,8 +331,8 @@ export async function login(
|
|||||||
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(resourcesInput);
|
||||||
const scopes = resources.map((resourceName) => RESOURCE_SCOPE_BY_NAME[resourceName]);
|
const scopes = translateResourceNamesToScopes(resources) as string[];
|
||||||
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 +341,23 @@ 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]];
|
if (token?.account) {
|
||||||
let token = await acquireTokenWithCache(pca, scope, selectedAccount);
|
selectedAccount = token.account;
|
||||||
|
}
|
||||||
|
|
||||||
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,18 +370,22 @@ 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!selectedAccount) {
|
||||||
|
const accounts = await pca.getTokenCache().getAllAccounts();
|
||||||
|
selectedAccount = accounts[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
const activeAccountUpn = selectedAccount?.username ?? null;
|
const activeAccountUpn = selectedAccount?.username ?? null;
|
||||||
if (activeAccountUpn) {
|
if (activeAccountUpn) {
|
||||||
await writeSessionState({ activeAccountUpn });
|
await writeSessionState({ activeAccountUpn });
|
||||||
@@ -400,20 +399,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 +419,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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
266
src/cli.ts
266
src/cli.ts
@@ -1,165 +1,141 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { parseArgs } from "node:util";
|
import { Argument, Command, Option } from "commander";
|
||||||
|
import { renderCliOutput } from "@slawek/sk-tools";
|
||||||
|
import { supportedResourceNames, ResourceName } from "./azure/index.ts";
|
||||||
|
|
||||||
import { runCommand } from "./cli/commands.ts";
|
// Commands
|
||||||
import { usageGetToken } from "./cli/commands/get-token.ts";
|
import { runGetTokenCommand } from "./cli/commands/get-token.ts";
|
||||||
import { usageListAppGrants } from "./cli/commands/list-app-grants.ts";
|
import { runListAppGrantsCommand } from "./cli/commands/list-app-grants.ts";
|
||||||
import { usageListAppPermissions } from "./cli/commands/list-app-permissions.ts";
|
import { runListAppPermissionsCommand } from "./cli/commands/list-app-permissions.ts";
|
||||||
import { usageListApps } from "./cli/commands/list-apps.ts";
|
import { runListAppsCommand } from "./cli/commands/list-apps.ts";
|
||||||
import { usageListResourcePermissions } from "./cli/commands/list-resource-permissions.ts";
|
import { runListResourcePermissionsCommand } from "./cli/commands/list-resource-permissions.ts";
|
||||||
import { usageLogin } from "./cli/commands/login.ts";
|
import { runLoginCommand } from "./cli/commands/login.ts";
|
||||||
import { usageLogout } from "./cli/commands/logout.ts";
|
import { runLogoutCommand } from "./cli/commands/logout.ts";
|
||||||
import { usageRest } from "./cli/commands/rest.ts";
|
import { runRestCommand } from "./cli/commands/rest.ts";
|
||||||
import {
|
import { runTestCommand } from "./cli/commands/test-command.ts";
|
||||||
normalizeOutputFormat,
|
|
||||||
outputFiltered,
|
|
||||||
parseHeaderSpec,
|
|
||||||
renderOutput,
|
|
||||||
} from "@slawek/sk-tools";
|
|
||||||
|
|
||||||
type CliValues = {
|
import pkg from "../package.json" with { type: "json" };
|
||||||
help?: boolean;
|
const { version: packageVersion } = pkg;
|
||||||
type?: string;
|
|
||||||
method?: string;
|
|
||||||
url?: string;
|
|
||||||
"display-name"?: string;
|
|
||||||
"app-id"?: string;
|
|
||||||
resources?: string;
|
|
||||||
"use-device-code"?: boolean;
|
|
||||||
"no-browser"?: boolean;
|
|
||||||
browser?: string;
|
|
||||||
"browser-profile"?: string;
|
|
||||||
all?: boolean;
|
|
||||||
resolve?: boolean;
|
|
||||||
short?: boolean;
|
|
||||||
filter?: string;
|
|
||||||
query?: string;
|
|
||||||
header?: string;
|
|
||||||
output?: string;
|
|
||||||
[key: string]: string | boolean | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function usage(): string {
|
|
||||||
return `Usage: sk-az-tools <command> [options]
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
login Authenticate selected resources
|
|
||||||
logout Sign out and clear login state
|
|
||||||
get-token Get access token (azurerm|devops)
|
|
||||||
rest Call REST API endpoint
|
|
||||||
list-apps List Entra applications
|
|
||||||
list-app-permissions List required permissions for an app
|
|
||||||
list-app-grants List OAuth2 grants for an app
|
|
||||||
list-resource-permissions List available permissions for a resource app
|
|
||||||
|
|
||||||
Global options (all commands):
|
|
||||||
-q, --query <jmespath>
|
|
||||||
-o, --output <format> table|t|alignedtable|at|prettytable|pt|tsv
|
|
||||||
-h, --help
|
|
||||||
|
|
||||||
Use: sk-az-tools --help <command>
|
|
||||||
or: sk-az-tools <command> --help`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function usageCommand(command: string): string {
|
|
||||||
switch (command) {
|
|
||||||
case "login":
|
|
||||||
return usageLogin();
|
|
||||||
case "list-apps":
|
|
||||||
return usageListApps();
|
|
||||||
case "logout":
|
|
||||||
return usageLogout();
|
|
||||||
case "get-token":
|
|
||||||
return usageGetToken();
|
|
||||||
case "rest":
|
|
||||||
return usageRest();
|
|
||||||
case "list-app-permissions":
|
|
||||||
return usageListAppPermissions();
|
|
||||||
case "list-app-grants":
|
|
||||||
return usageListAppGrants();
|
|
||||||
case "list-resource-permissions":
|
|
||||||
return usageListResourcePermissions();
|
|
||||||
default:
|
|
||||||
return `Unknown command: ${command}\n\n${usage()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function omitRecords(record: Record<string, unknown>, names: Set<string>): Record<string, unknown> {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(record).filter(([key]) => !names.has(key)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const argv = process.argv.slice(2);
|
const skAzTools = new Command();
|
||||||
const command = argv[0];
|
|
||||||
if (!command) {
|
|
||||||
console.log(usage());
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
if (command === "-h" || command === "--help") {
|
|
||||||
const helpCommand = argv[1];
|
|
||||||
console.log(helpCommand ? usageCommand(helpCommand) : usage());
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { values } = parseArgs({
|
skAzTools
|
||||||
args: argv.slice(1),
|
.name("sk-az-tools")
|
||||||
options: {
|
.description("A collection of tools for Azure and Microsoft Entra management")
|
||||||
help: { type: "boolean", short: "h" },
|
.version(packageVersion)
|
||||||
type: { type: "string", short: "t" },
|
.option("-q, --query <jmespath>", "JMESPath query to filter output")
|
||||||
method: { type: "string" },
|
.option("-C, --columns <definition>", "Column tokens: col (raw), col: (auto), col:Label (custom), exact via = prefix")
|
||||||
url: { type: "string" },
|
.addOption(new Option("-o, --output <format>", "Output format: table|t|alignedtable|at|prettytable|pt|tsv")
|
||||||
"display-name": { type: "string", short: "n" },
|
.choices(["table", "t", "alignedtable", "at", "prettytable", "pt", "tsv"])
|
||||||
"app-id": { type: "string", short: "i" },
|
);
|
||||||
resources: { type: "string" },
|
|
||||||
"use-device-code": { type: "boolean" },
|
skAzTools
|
||||||
"no-browser": { type: "boolean" },
|
.command("login")
|
||||||
browser: { type: "string" },
|
.description("Authenticate selected resources")
|
||||||
"browser-profile": { type: "string" },
|
.addArgument(
|
||||||
all: { type: "boolean" },
|
new Argument("[resource...]", "Resources: graph|devops|azurerm")
|
||||||
resolve: { type: "boolean", short: "r" },
|
.choices(supportedResourceNames())
|
||||||
short: { type: "boolean", short: "s" },
|
.default(["azurerm"]),
|
||||||
filter: { type: "string", short: "f" },
|
)
|
||||||
query: { type: "string", short: "q" },
|
.option("--use-device-code", "Use device code flow")
|
||||||
header: { type: "string", short: "H" },
|
.option("--no-browser", "Do not launch browser")
|
||||||
output: { type: "string", short: "o" },
|
.option("--browser-name <name>", "Browser keyword: brave|browser|browserPrivate|chrome|edge|firefox")
|
||||||
},
|
.option("--browser-profile <name>", "Chromium profile name")
|
||||||
strict: true,
|
.action(runLoginCommand);
|
||||||
allowPositionals: false,
|
|
||||||
|
skAzTools
|
||||||
|
.command("logout")
|
||||||
|
.description("Sign out and clear login state")
|
||||||
|
.option("--all", "Clear login state and remove all cached accounts")
|
||||||
|
.action(runLogoutCommand);
|
||||||
|
|
||||||
|
skAzTools
|
||||||
|
.command("get-token")
|
||||||
|
.description("Get an access token for a resource or resources.")
|
||||||
|
.addArgument(new Argument("<type>", "Token type.").choices(supportedResourceNames()))
|
||||||
|
.action(runGetTokenCommand);
|
||||||
|
|
||||||
|
skAzTools
|
||||||
|
.command("rest")
|
||||||
|
.description("Call REST API endpoint")
|
||||||
|
.argument("<url>", "Full URL to call")
|
||||||
|
.addOption(new Option("-X, --method <httpMethod>", "HTTP method")
|
||||||
|
.choices(["GET", "POST", "PUT", "PATCH", "DELETE"]))
|
||||||
|
.option("-H, --header <name: value>", "Extra request header")
|
||||||
|
.addHelpText("after", `
|
||||||
|
Authorization is added automatically for:
|
||||||
|
management.azure.com Uses azurerm token
|
||||||
|
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);
|
||||||
|
const allOptions = command.optsWithGlobals();
|
||||||
|
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||||
});
|
});
|
||||||
|
|
||||||
const typedValues = values as CliValues;
|
skAzTools
|
||||||
|
.command("list-apps")
|
||||||
|
.description("List Entra applications")
|
||||||
|
.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, command) => {
|
||||||
|
const output = await runListAppsCommand(options);
|
||||||
|
const allOptions = command.optsWithGlobals();
|
||||||
|
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||||
|
});
|
||||||
|
|
||||||
if (typedValues.help) {
|
skAzTools
|
||||||
console.log(usageCommand(command));
|
.command("list-app-permissions")
|
||||||
process.exit(0);
|
.description("List required permissions for an app")
|
||||||
}
|
.option("-i, --app-id <appId>", "Application (client) ID")
|
||||||
|
.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, command) => {
|
||||||
|
const output = await runListAppPermissionsCommand(options);
|
||||||
|
const allOptions = command.optsWithGlobals();
|
||||||
|
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||||
|
});
|
||||||
|
|
||||||
const outputFormat = normalizeOutputFormat(typedValues.output);
|
skAzTools
|
||||||
const result = await runCommand(command, typedValues);
|
.command("list-app-grants")
|
||||||
const filtered = outputFiltered(result, typedValues.query);
|
.description("List OAuth2 grants for an app")
|
||||||
let output: unknown = filtered;
|
.option("-i, --app-id <appId>", "Application (client) ID")
|
||||||
if (command === "list-app-permissions" && typedValues.short && Array.isArray(filtered) && filtered.every(isRecord)) {
|
.action(async (options, command) => {
|
||||||
const names = new Set(["resourceAppId", "permissionId"]);
|
const output = await runListAppGrantsCommand(options);
|
||||||
output = filtered.map((item) => omitRecords(item, names));
|
const allOptions = command.optsWithGlobals();
|
||||||
}
|
renderCliOutput(output, allOptions.output, allOptions.query, allOptions.columns);
|
||||||
const headerSpec = command === "rest"
|
});
|
||||||
? parseHeaderSpec(undefined)
|
|
||||||
: parseHeaderSpec(typedValues.header);
|
|
||||||
|
|
||||||
renderOutput(outputFormat, headerSpec, output);
|
skAzTools
|
||||||
|
.command("list-resource-permissions")
|
||||||
|
.description("List available permissions for a resource app")
|
||||||
|
.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, command) => {
|
||||||
|
const output = await runListResourcePermissionsCommand(options);
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err: unknown) => {
|
main().catch((err: unknown) => {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
console.error(`Error: ${error.message}`);
|
console.error(`Error: ${error.message}`);
|
||||||
console.error(usage());
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { runGetTokenCommand } from "./commands/get-token.ts";
|
|
||||||
import { runListAppGrantsCommand } from "./commands/list-app-grants.ts";
|
|
||||||
import { runListAppPermissionsCommand } from "./commands/list-app-permissions.ts";
|
|
||||||
import { runListAppsCommand } from "./commands/list-apps.ts";
|
|
||||||
import { runListResourcePermissionsCommand } from "./commands/list-resource-permissions.ts";
|
|
||||||
import { runLoginCommand } from "./commands/login.ts";
|
|
||||||
import { runLogoutCommand } from "./commands/logout.ts";
|
|
||||||
import { runRestCommand } from "./commands/rest.ts";
|
|
||||||
|
|
||||||
import type { CommandValues } from "./commands/types.ts";
|
|
||||||
|
|
||||||
export async function runCommand(command: string, values: CommandValues): Promise<unknown> {
|
|
||||||
switch (command) {
|
|
||||||
case "login":
|
|
||||||
return runLoginCommand(values);
|
|
||||||
case "logout":
|
|
||||||
return runLogoutCommand(values);
|
|
||||||
case "list-apps":
|
|
||||||
return runListAppsCommand(values);
|
|
||||||
case "list-app-permissions":
|
|
||||||
return runListAppPermissionsCommand(values);
|
|
||||||
case "list-app-grants":
|
|
||||||
return runListAppGrantsCommand(values);
|
|
||||||
case "list-resource-permissions":
|
|
||||||
return runListResourcePermissionsCommand(values);
|
|
||||||
case "get-token":
|
|
||||||
return runGetTokenCommand(values);
|
|
||||||
case "rest":
|
|
||||||
return runRestCommand(values);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown command: ${command}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
@@ -1,51 +1,21 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { acquireResourceToken } from "../../azure/index.ts";
|
import { RESOURCE_SCOPE_BY_NAME, ResourceName, supportedResourceNames, getTokenCredential } from "../../azure/index.ts";
|
||||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
|
||||||
import { loadConfig } from "../../index.ts";
|
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
export async function runGetTokenCommand(
|
||||||
|
type: ResourceName,
|
||||||
export function usageGetToken(): string {
|
): Promise<void> {
|
||||||
return `Usage: sk-az-tools get-token --type|-t <azurerm|devops> [global options]
|
if (!type || !supportedResourceNames().includes(type)) {
|
||||||
|
throw new Error(`Token type is required for get-token (allowed: ${supportedResourceNames().join(", ")})`);
|
||||||
Options:
|
|
||||||
--type, -t <value> Token type: azurerm|devops`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runGetTokenCommand(values: CommandValues): Promise<unknown> {
|
const credential = await getTokenCredential();
|
||||||
const tokenType = (values.type ?? "").toString().trim().toLowerCase();
|
|
||||||
if (!tokenType) {
|
|
||||||
throw new Error("--type is required for get-token (allowed: azurerm, devops)");
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await loadConfig("public-config");
|
const accessToken = await credential.getToken(RESOURCE_SCOPE_BY_NAME[type]);
|
||||||
|
|
||||||
if (tokenType === "azurerm") {
|
|
||||||
const result = await acquireResourceToken(
|
|
||||||
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 access token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// Output only the token string for easy consumption in scripts
|
||||||
tokenType,
|
console.log(accessToken.token);
|
||||||
accessToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenType === "devops") {
|
|
||||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
|
||||||
return {
|
|
||||||
tokenType,
|
|
||||||
accessToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Invalid --type '${values.type}'. Allowed: azurerm, devops`);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
// 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";
|
type ListAppGrantsOptions = {
|
||||||
import type { CommandValues } from "./types.ts";
|
appId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function usageListAppGrants(): string {
|
export async function runListAppGrantsCommand(options: ListAppGrantsOptions): Promise<unknown> {
|
||||||
return `Usage: sk-az-tools list-app-grants --app-id|-i <appId> [global options]
|
if (!options.appId) {
|
||||||
|
|
||||||
Options:
|
|
||||||
--app-id, -i <appId> Application (client) ID (required)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runListAppGrantsCommand(values: CommandValues): Promise<unknown> {
|
|
||||||
if (!values["app-id"]) {
|
|
||||||
throw new Error("--app-id is required for list-app-grants");
|
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, options.appId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,47 @@
|
|||||||
|
|
||||||
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 type { CommandValues } from "./types.ts";
|
import { getGraphClient } from "../../graph/index.ts";
|
||||||
|
|
||||||
export function usageListAppPermissions(): string {
|
type ListAppPermissionsOptions = {
|
||||||
return `Usage: sk-az-tools list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s] [--filter|-f <glob>] [global options]
|
appId?: string;
|
||||||
|
resolve?: boolean;
|
||||||
|
short?: boolean;
|
||||||
|
filter?: string;
|
||||||
|
};
|
||||||
|
|
||||||
Options:
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
--app-id, -i <appId> Application (client) ID (required)
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
--resolve, -r Resolve permission GUIDs to human-readable values
|
|
||||||
--short, -s Makes output more compact
|
|
||||||
--filter, -f <glob> Filter by permission name glob`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runListAppPermissionsCommand(values: CommandValues): Promise<unknown> {
|
function omitColumns(input: unknown, names: string[]): unknown {
|
||||||
if (!values["app-id"]) {
|
if (!Array.isArray(input) || !input.every(isRecord)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const namesSet = new Set(names);
|
||||||
|
return input.map((record) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(record).filter(([key]) => !namesSet.has(key)),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runListAppPermissionsCommand(options: ListAppPermissionsOptions): Promise<unknown> {
|
||||||
|
if (!options.appId) {
|
||||||
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 = values.resolve || values.filter
|
let result: unknown = options.resolve || options.filter
|
||||||
? await listAppPermissionsResolved(client, values["app-id"])
|
? await listAppPermissionsResolved(client, options.appId)
|
||||||
: await listAppPermissions(client, values["app-id"]);
|
: await listAppPermissions(client, options.appId);
|
||||||
if (values.filter) {
|
if (options.short) {
|
||||||
result = filterByPermissionName(result, values.filter);
|
result = omitColumns(result, ["resourceAppId", "permissionId"]);
|
||||||
|
}
|
||||||
|
if (options.filter) {
|
||||||
|
result = filterByPermissionName(result as Array<Record<string, unknown>>, options.filter);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
// 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 { getGraphClient } from "../../graph/index.ts";
|
||||||
|
|
||||||
import { filterByDisplayName, getGraphClientFromPublicConfig } from "./shared.ts";
|
type ListAppsOptions = {
|
||||||
import type { CommandValues } from "./types.ts";
|
displayName?: string;
|
||||||
|
appId?: string;
|
||||||
|
filter?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function usageListApps(): string {
|
export async function runListAppsCommand(options: ListAppsOptions): Promise<unknown> {
|
||||||
return `Usage: sk-az-tools list-apps [--display-name|-n <name>] [--app-id|-i <appId>] [--filter|-f <glob>] [global options]
|
const client = await getGraphClient();
|
||||||
|
|
||||||
Options:
|
let result = await listApps(client, options.displayName, options.appId);
|
||||||
--display-name, -n <name> Get app by name
|
|
||||||
--app-id, -i <appId> Get app by id
|
if (options.appId && result.length > 1) {
|
||||||
--filter, -f <glob> Filter by app display name glob`;
|
throw new Error(`Expected a single app for --app-id ${options.appId}, but got ${result.length}`);
|
||||||
}
|
}
|
||||||
|
if (options.filter) {
|
||||||
export async function runListAppsCommand(values: CommandValues): Promise<unknown> {
|
result = filterByDisplayName(result, options.filter);
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
|
||||||
let result = await listApps(client, values["display-name"], values["app-id"]);
|
|
||||||
if (values["app-id"] && result.length > 1) {
|
|
||||||
throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`);
|
|
||||||
}
|
|
||||||
if (values.filter) {
|
|
||||||
result = filterByDisplayName(result, values.filter);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
// 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 } from "./shared.ts";
|
||||||
|
|
||||||
import { filterByPermissionName, getGraphClientFromPublicConfig } from "./shared.ts";
|
type ListResourcePermissionsOptions = {
|
||||||
import type { CommandValues } from "./types.ts";
|
appId?: string;
|
||||||
|
displayName?: string;
|
||||||
|
filter?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function usageListResourcePermissions(): string {
|
export async function runListResourcePermissionsCommand(options: ListResourcePermissionsOptions): Promise<unknown> {
|
||||||
return `Usage: sk-az-tools list-resource-permissions [--app-id|-i <appId> | --display-name|-n <name>] [--filter|-f <glob>] [global options]
|
if (!options.appId && !options.displayName) {
|
||||||
|
|
||||||
Options:
|
|
||||||
--app-id, -i <appId> Resource app ID
|
|
||||||
--display-name, -n <name> Resource app display name
|
|
||||||
--filter, -f <glob> Filter by permission name glob`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runListResourcePermissionsCommand(values: CommandValues): Promise<unknown> {
|
|
||||||
if (!values["app-id"] && !values["display-name"]) {
|
|
||||||
throw new Error("--app-id or --display-name is required for list-resource-permissions");
|
throw new Error("--app-id or --display-name is required for list-resource-permissions");
|
||||||
}
|
}
|
||||||
if (values["app-id"] && values["display-name"]) {
|
if (options.appId && options.displayName) {
|
||||||
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"],
|
options.appId,
|
||||||
values["display-name"],
|
options.displayName,
|
||||||
);
|
);
|
||||||
if (values.filter) {
|
if (options.filter) {
|
||||||
result = filterByPermissionName(result, values.filter);
|
result = filterByPermissionName(result, options.filter);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,35 @@
|
|||||||
// 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 type { ResourceName } from "../../azure/index.ts";
|
||||||
|
import { loadAuthConfig } from "../../index.ts";
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
type LoginOptions = {
|
||||||
|
useDeviceCode?: boolean;
|
||||||
|
noBrowser?: boolean;
|
||||||
|
browserName?: string;
|
||||||
|
browserProfile?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function usageLogin(): string {
|
type LoginResult = {
|
||||||
return `Usage: sk-az-tools login [--resources <csv>] [--use-device-code] [--no-browser] [--browser <name>] [--browser-profile <profile>] [global options]
|
accountUpn: string | null;
|
||||||
|
resources: Array<{ resource: string; expiresOn: string | null }>;
|
||||||
|
flow: "device-code" | "interactive";
|
||||||
|
browserLaunchAttempted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
Options:
|
export async function runLoginCommand(resources: ResourceName[], options: LoginOptions): Promise<void> {
|
||||||
--resources <csv> Comma-separated resources: graph,devops,arm (default: all)
|
const config = await loadAuthConfig("public-config");
|
||||||
--use-device-code Use device code flow instead of interactive flow
|
|
||||||
--no-browser Do not launch browser; print interactive URL to stderr
|
|
||||||
--browser <name> Browser keyword: brave|browser|browserPrivate|chrome|edge|firefox
|
|
||||||
--browser-profile <name> Chromium profile name (e.g. Default, "Profile 1")`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runLoginCommand(values: CommandValues): Promise<unknown> {
|
const result = await login(
|
||||||
const config = await loadConfig("public-config");
|
|
||||||
return login(
|
|
||||||
config.tenantId,
|
config.tenantId,
|
||||||
config.clientId,
|
config.clientId,
|
||||||
values.resources,
|
resources,
|
||||||
Boolean(values["use-device-code"]),
|
Boolean(options.useDeviceCode),
|
||||||
Boolean(values["no-browser"]),
|
Boolean(options.noBrowser),
|
||||||
values.browser,
|
options.browserName,
|
||||||
values["browser-profile"],
|
options.browserProfile,
|
||||||
);
|
) as LoginResult;
|
||||||
|
|
||||||
|
console.log(`Logged in as ${result.accountUpn ?? "<unknown>"} using ${result.flow} flow for resources: ${resources.join(",")}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
// 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";
|
type LogoutOptions = {
|
||||||
|
all?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function usageLogout(): string {
|
type LogoutResult = {
|
||||||
return `Usage: sk-az-tools logout [--all] [global options]
|
clearedAll: boolean;
|
||||||
|
signedOut: string[];
|
||||||
|
};
|
||||||
|
|
||||||
Options:
|
export async function runLogoutCommand(options: LogoutOptions): Promise<void> {
|
||||||
--all Clear login state and remove all cached accounts`;
|
const config = await loadAuthConfig("public-config");
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runLogoutCommand(values: CommandValues): Promise<unknown> {
|
if (result.clearedAll) {
|
||||||
const config = await loadConfig("public-config");
|
console.log(`Cleared all cached accounts: ${result.signedOut.join(", ")}`);
|
||||||
return logout(config.tenantId, config.clientId, Boolean(values.all));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Signed out: ${result.signedOut.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,10 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { acquireResourceToken } from "../../azure/index.ts";
|
import { RESOURCE_SCOPE_BY_NAME, ResourceName, getTokenCredential } from "../../azure/index.ts";
|
||||||
import { getDevOpsApiToken } from "../../devops/index.ts";
|
|
||||||
import { loadConfig } from "../../index.ts";
|
|
||||||
|
|
||||||
import type { CommandValues } from "./types.ts";
|
function parseHeaderLine(
|
||||||
|
header?: string,
|
||||||
export function usageRest(): string {
|
): { name: string; value: string } | null {
|
||||||
return `Usage: sk-az-tools rest [--method <httpMethod>] --url <url> [--header <name: value>] [global options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--method <httpMethod> HTTP method (default: GET; examples: GET, POST, PATCH, DELETE)
|
|
||||||
--url <url> Full URL to call
|
|
||||||
--header <name: value> Extra request header; example: "Content-Type: application/json"
|
|
||||||
|
|
||||||
Authorization is added automatically for:
|
|
||||||
management.azure.com Uses azurerm token
|
|
||||||
dev.azure.com Uses devops token`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHeaderLine(header?: string): { name: string; value: string } | null {
|
|
||||||
if (!header || header.trim() === "") {
|
if (!header || header.trim() === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -48,48 +33,65 @@ function hasAuthorizationHeader(headers: Headers): boolean {
|
|||||||
return false;
|
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> {
|
async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
|
||||||
const host = url.hostname.toLowerCase();
|
const host = url.hostname.toLowerCase();
|
||||||
if (host !== "management.azure.com" && host !== "dev.azure.com") {
|
const resourceName = resolveResourceNameForHost(host);
|
||||||
|
if (!resourceName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await loadConfig("public-config");
|
const credential = await getTokenCredential();
|
||||||
|
|
||||||
if (host === "management.azure.com") {
|
const accessToken = await credential.getToken(RESOURCE_SCOPE_BY_NAME[resourceName]);
|
||||||
const result = await acquireResourceToken(
|
if (!accessToken?.token) {
|
||||||
config.tenantId,
|
throw new Error(`Failed to obtain ${resourceName} token`);
|
||||||
config.clientId,
|
|
||||||
"arm",
|
|
||||||
);
|
|
||||||
const accessToken = result?.accessToken;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error("Failed to obtain AzureRM token");
|
|
||||||
}
|
|
||||||
return `Bearer ${accessToken}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId);
|
return `Bearer ${accessToken.token}`;
|
||||||
return `Bearer ${accessToken}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runRestCommand(values: CommandValues): Promise<unknown> {
|
type httpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
const method = (values.method ?? "GET").toString().trim().toUpperCase() || "GET";
|
|
||||||
const urlValue = (values.url ?? "").toString().trim();
|
type restOptions = {
|
||||||
|
method?: httpMethod;
|
||||||
|
header?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function runRestCommand(url: string, options: restOptions): Promise<unknown> {
|
||||||
|
const method = options.method || "GET";
|
||||||
|
const urlValue = (url ?? "").toString().trim();
|
||||||
|
|
||||||
if (!urlValue) {
|
if (!urlValue) {
|
||||||
throw new Error("--url is required for rest");
|
throw new Error("URL is required for rest");
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetUrl: URL;
|
let targetUrl: URL;
|
||||||
try {
|
try {
|
||||||
targetUrl = new URL(urlValue);
|
targetUrl = new URL(urlValue);
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Invalid --url '${urlValue}'`);
|
throw new Error(`Invalid URL '${urlValue}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
const customHeader = parseHeaderLine(values.header);
|
const customHeader = parseHeaderLine(options.header);
|
||||||
if (customHeader) {
|
if (customHeader) {
|
||||||
headers.set(customHeader.name, customHeader.value);
|
headers.set(customHeader.name, customHeader.value);
|
||||||
}
|
}
|
||||||
@@ -107,9 +109,9 @@ export async function runRestCommand(values: CommandValues): Promise<unknown> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const contentType = response.headers.get("content-type") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
let body: unknown;
|
let body: string;
|
||||||
if (contentType.toLowerCase().includes("application/json")) {
|
if (contentType.toLowerCase().includes("application/json")) {
|
||||||
body = await response.json();
|
body = JSON.stringify(await response.json());
|
||||||
} else {
|
} else {
|
||||||
body = await response.text();
|
body = await response.text();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
import { minimatch } from "minimatch";
|
import { minimatch } from "minimatch";
|
||||||
|
|
||||||
import { loadConfig } from "../../index.ts";
|
|
||||||
import { getGraphClient } from "../../graph/auth.ts";
|
|
||||||
|
|
||||||
type PermissionRow = {
|
type PermissionRow = {
|
||||||
permissionValue?: string | null;
|
permissionValue?: string | null;
|
||||||
permissionDisplayName?: string | null;
|
permissionDisplayName?: string | null;
|
||||||
@@ -26,8 +23,3 @@ export function filterByDisplayName<T extends DisplayNameRow>(rows: T[], pattern
|
|||||||
minimatch(item.displayName ?? "", pattern, { nocase: true }),
|
minimatch(item.displayName ?? "", pattern, { nocase: true }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGraphClientFromPublicConfig(): Promise<{ client: any }> {
|
|
||||||
const config = await loadConfig("public-config");
|
|
||||||
return getGraphClient(config.tenantId, config.clientId);
|
|
||||||
}
|
|
||||||
|
|||||||
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.");
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
export type CommandValues = {
|
|
||||||
[key: string]: string | boolean | undefined;
|
|
||||||
type?: string;
|
|
||||||
method?: string;
|
|
||||||
url?: string;
|
|
||||||
header?: string;
|
|
||||||
resources?: string;
|
|
||||||
"use-device-code"?: boolean;
|
|
||||||
"no-browser"?: boolean;
|
|
||||||
browser?: string;
|
|
||||||
"browser-profile"?: string;
|
|
||||||
all?: boolean;
|
|
||||||
"display-name"?: string;
|
|
||||||
"app-id"?: string;
|
|
||||||
filter?: string;
|
|
||||||
resolve?: boolean;
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { parseArgs } from "node:util";
|
import { Command } from "commander";
|
||||||
|
|
||||||
type RunAzResult = {
|
type RunAzResult = {
|
||||||
status: number;
|
status: number;
|
||||||
@@ -40,50 +40,17 @@ function runAz(args: string[], quiet = false, allowFailure = false): RunAzResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const usageText = `Usage: ${path.basename(process.argv[1])} [options] <app-name>
|
const program = new Command(path.basename(process.argv[1]));
|
||||||
Options:
|
program
|
||||||
-c, --config <path> Write JSON config to file (optional)
|
.description("Create or update public client app and print config template")
|
||||||
-h, --help Show this help message and exit`;
|
.argument("<app-name>", "Application name")
|
||||||
|
.option("-c, --config <path>", "Write JSON config to file (optional)")
|
||||||
|
.allowExcessArguments(false)
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
let values: Record<string, string | boolean | undefined>;
|
const appName = program.args[0] as string;
|
||||||
let positionals: string[];
|
const options = program.opts<{ config?: string }>();
|
||||||
try {
|
const configPath = options.config ?? "";
|
||||||
({ values, positionals } = parseArgs({
|
|
||||||
args: process.argv.slice(2),
|
|
||||||
options: {
|
|
||||||
help: { type: "boolean", short: "h" },
|
|
||||||
config: { type: "string", short: "c" },
|
|
||||||
},
|
|
||||||
strict: true,
|
|
||||||
allowPositionals: true,
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error: ${(err as Error).message}`);
|
|
||||||
console.error(usageText);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.help) {
|
|
||||||
console.log(usageText);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positionals.length > 1) {
|
|
||||||
console.error(
|
|
||||||
"Error: Too many positional arguments. Only one app name positional argument is allowed.",
|
|
||||||
);
|
|
||||||
console.error(usageText);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const appName = positionals[0] || "";
|
|
||||||
const configPath = typeof values.config === "string" ? values.config : "";
|
|
||||||
|
|
||||||
if (!appName) {
|
|
||||||
console.error("Error: Application name is required.");
|
|
||||||
console.error(usageText);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let appId = runAz([
|
let appId = runAz([
|
||||||
"ad",
|
"ad",
|
||||||
|
|||||||
@@ -4,39 +4,27 @@
|
|||||||
* A DevOps helpers module.
|
* 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";
|
import * as azdev from "azure-devops-node-api";
|
||||||
|
|
||||||
const AZURE_DEVOPS_SCOPES = ["https://app.vssps.visualstudio.com/.default"];
|
export type DevOpsClients = {
|
||||||
|
coreClient: Awaited<ReturnType<azdev.WebApi["getCoreApi"]>>;
|
||||||
type LoginInteractiveResult = {
|
gitClient: Awaited<ReturnType<azdev.WebApi["getGitApi"]>>;
|
||||||
accessToken?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getDevOpsApiToken(tenantId: string, clientId: string): Promise<string> {
|
export async function getDevOpsClients(orgUrl: string, tenantId?: string, clientId?: string): Promise<DevOpsClients> {
|
||||||
const result = await loginInteractive(
|
return getTokenCredential(tenantId, clientId)
|
||||||
tenantId,
|
.then((credential) => credential.getToken(RESOURCE_SCOPE_BY_NAME.devops))
|
||||||
clientId,
|
.then(async (accessToken) => {
|
||||||
AZURE_DEVOPS_SCOPES,
|
if (!accessToken?.token) {
|
||||||
) as LoginInteractiveResult;
|
|
||||||
|
|
||||||
const accessToken = result?.accessToken;
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error("Failed to obtain Azure DevOps API token");
|
throw new Error("Failed to obtain Azure DevOps API token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return accessToken;
|
const connection = new azdev.WebApi(orgUrl, azdev.getBearerHandler(accessToken.token));
|
||||||
}
|
const [coreClient, gitClient] = await Promise.all([
|
||||||
|
connection.getCoreApi(),
|
||||||
export async function getDevOpsClients(orgUrl: string, tenantId: string, clientId: string): Promise<{ coreClient: unknown; gitClient: unknown }> {
|
connection.getGitApi(),
|
||||||
const accessToken = await getDevOpsApiToken(tenantId, clientId);
|
]);
|
||||||
|
|
||||||
const authHandler = azdev.getBearerHandler(accessToken);
|
|
||||||
const connection = new azdev.WebApi(orgUrl, authHandler);
|
|
||||||
|
|
||||||
const coreClient = await connection.getCoreApi();
|
|
||||||
const gitClient = await connection.getGitApi();
|
|
||||||
|
|
||||||
return { coreClient, gitClient };
|
return { coreClient, gitClient };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { Client } from "@microsoft/microsoft-graph-client";
|
|
||||||
import { acquireResourceToken } from "../azure/index.ts";
|
|
||||||
|
|
||||||
type GraphApiToken = {
|
|
||||||
accessToken: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getGraphClient(
|
|
||||||
tenantId: string,
|
|
||||||
clientId: string,
|
|
||||||
): Promise<{ graphApiToken: GraphApiToken; client: any }> {
|
|
||||||
const graphApiToken = await acquireResourceToken(tenantId, clientId, "graph") as GraphApiToken;
|
|
||||||
|
|
||||||
const client = Client.init({
|
|
||||||
authProvider: (done) => {
|
|
||||||
done(null, graphApiToken.accessToken);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { graphApiToken, client };
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
export * from "./auth.ts";
|
|
||||||
export * from "./app.ts";
|
export * from "./app.ts";
|
||||||
export * from "./sp.ts";
|
export * from "./sp.ts";
|
||||||
|
|
||||||
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
|
import { RESOURCE_SCOPE_BY_NAME, getTokenCredential } from "../azure/index.ts";
|
||||||
|
|
||||||
|
export async function getGraphClient(): Promise<Client> {
|
||||||
|
return Client.init({
|
||||||
|
authProvider: (done) => {
|
||||||
|
void getTokenCredential()
|
||||||
|
.then((credential) => credential.getToken(RESOURCE_SCOPE_BY_NAME.graph))
|
||||||
|
.then((accessToken) => done(null, accessToken?.token ?? null))
|
||||||
|
.catch((err) => done(err as Error, null));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
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