Add explicit login/logout flows and browser selection
This commit is contained in:
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM node:24-alpine AS package
|
||||||
|
|
||||||
|
WORKDIR /package
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY src ./src
|
||||||
|
COPY README.md LICENSE ./
|
||||||
|
|
||||||
|
RUN npm pack --silent
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:24-alpine
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
COPY --from=package /package/*.tgz /tmp/sk-az-tools.tgz
|
||||||
|
|
||||||
|
RUN npm install --global /tmp/sk-az-tools.tgz \
|
||||||
|
&& rm /tmp/sk-az-tools.tgz \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
ENTRYPOINT ["sk-az-tools"]
|
||||||
|
CMD ["--help"]
|
||||||
@@ -11,5 +11,8 @@ export { getCredential } from "./client-auth.js";
|
|||||||
export {
|
export {
|
||||||
loginInteractive,
|
loginInteractive,
|
||||||
loginDeviceCode,
|
loginDeviceCode,
|
||||||
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
parseResources,
|
||||||
|
acquireResourceTokenFromLogin,
|
||||||
} from "./pca-auth.js";
|
} from "./pca-auth.js";
|
||||||
|
|||||||
@@ -1,13 +1,125 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
|
||||||
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 path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { PublicClientApplication } from "@azure/msal-node";
|
import { PublicClientApplication } from "@azure/msal-node";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
|
const RESOURCE_SCOPE_BY_NAME = {
|
||||||
|
graph: "https://graph.microsoft.com/.default",
|
||||||
|
devops: "499b84ac-1321-427f-aa17-267ca6975798/.default",
|
||||||
|
arm: "https://management.azure.com/.default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_RESOURCES = ["graph", "devops", "arm"];
|
||||||
|
const LOGIN_REQUIRED_MESSAGE = "Login required. Run: sk-az-tools login";
|
||||||
|
const BROWSER_KEYWORDS = Object.keys(apps).sort();
|
||||||
|
|
||||||
|
function getCacheRoot() {
|
||||||
|
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() {
|
||||||
|
return path.join(getCacheRoot(), "login-session.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSessionState() {
|
||||||
|
try {
|
||||||
|
const sessionJson = await readFile(getSessionFilePath(), "utf8");
|
||||||
|
const parsed = JSON.parse(sessionJson);
|
||||||
|
return {
|
||||||
|
activeAccountUpn:
|
||||||
|
typeof parsed?.activeAccountUpn === "string"
|
||||||
|
? parsed.activeAccountUpn
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.code === "ENOENT") {
|
||||||
|
return { activeAccountUpn: null };
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSessionState(state) {
|
||||||
|
const sessionPath = getSessionFilePath();
|
||||||
|
await mkdir(path.dirname(sessionPath), { recursive: true });
|
||||||
|
await writeFile(sessionPath, JSON.stringify(state, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearSessionState() {
|
||||||
|
try {
|
||||||
|
await unlink(getSessionFilePath());
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.code !== "ENOENT") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUpn(upn) {
|
||||||
|
return typeof upn === "string" ? upn.trim().toLowerCase() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStderr(message) {
|
||||||
|
process.stderr.write(`${message}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowserAppName(browser) {
|
||||||
|
if (!browser || browser.trim() === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requested = browser.trim().toLowerCase();
|
||||||
|
if (requested === "default") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = BROWSER_KEYWORDS.find((name) => name.toLowerCase() === requested);
|
||||||
|
if (!keyword) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid browser '${browser}'. Allowed: default, ${BROWSER_KEYWORDS.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps[keyword];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseResources(resourcesCsv) {
|
||||||
|
if (!resourcesCsv || resourcesCsv.trim() === "") {
|
||||||
|
return [...DEFAULT_RESOURCES];
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = resourcesCsv
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const unique = [...new Set(resources)];
|
||||||
|
const invalid = unique.filter((name) => !RESOURCE_SCOPE_BY_NAME[name]);
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid resource name(s): ${invalid.join(", ")}. Allowed: ${DEFAULT_RESOURCES.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScopesForResources(resources) {
|
||||||
|
return resources.map((resourceName) => RESOURCE_SCOPE_BY_NAME[resourceName]);
|
||||||
|
}
|
||||||
|
|
||||||
function fileCachePlugin(cachePath) {
|
function fileCachePlugin(cachePath) {
|
||||||
return {
|
return {
|
||||||
beforeCacheAccess: async (ctx) => {
|
beforeCacheAccess: async (ctx) => {
|
||||||
@@ -25,14 +137,7 @@ function fileCachePlugin(cachePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createPca({ tenantId, clientId }) {
|
async function createPca({ tenantId, clientId }) {
|
||||||
const isWindows = process.platform === "win32";
|
const cacheRoot = getCacheRoot();
|
||||||
const userRoot = isWindows
|
|
||||||
? process.env.LOCALAPPDATA || os.homedir()
|
|
||||||
: os.homedir();
|
|
||||||
const cacheRoot = isWindows
|
|
||||||
? path.join(userRoot, "sk-az-tools")
|
|
||||||
: path.join(userRoot, ".config", "sk-az-tools");
|
|
||||||
|
|
||||||
const cachePath = path.join(cacheRoot, `${clientId}-msal.cache`);
|
const cachePath = path.join(cacheRoot, `${clientId}-msal.cache`);
|
||||||
let cachePlugin;
|
let cachePlugin;
|
||||||
try {
|
try {
|
||||||
@@ -66,28 +171,52 @@ async function createPca({ tenantId, clientId }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acquireTokenWithCache({ pca, scopes }) {
|
async function acquireTokenWithCache({ pca, scopes, account }) {
|
||||||
const accounts = await pca.getTokenCache().getAllAccounts();
|
if (account) {
|
||||||
|
|
||||||
if (accounts.length > 0) {
|
|
||||||
try {
|
try {
|
||||||
return await pca.acquireTokenSilent({
|
return await pca.acquireTokenSilent({
|
||||||
account: accounts[0],
|
account,
|
||||||
scopes,
|
scopes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
// proceed to interactive/device code login
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await pca.getTokenCache().getAllAccounts();
|
||||||
|
for (const cachedAccount of accounts) {
|
||||||
|
try {
|
||||||
|
return await pca.acquireTokenSilent({
|
||||||
|
account: cachedAccount,
|
||||||
|
scopes,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Try next cached account.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findAccountByUpn({ pca, upn }) {
|
||||||
|
const normalized = normalizeUpn(upn);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await pca.getTokenCache().getAllAccounts();
|
||||||
|
return (
|
||||||
|
accounts.find((account) => normalizeUpn(account?.username) === normalized) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function loginInteractive({
|
export async function loginInteractive({
|
||||||
tenantId,
|
tenantId,
|
||||||
clientId,
|
clientId,
|
||||||
scopes,
|
scopes,
|
||||||
showAuthUrlOnly = false,
|
showAuthUrlOnly = false,
|
||||||
|
browser,
|
||||||
}) {
|
}) {
|
||||||
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");
|
||||||
@@ -103,26 +232,17 @@ export async function loginInteractive({
|
|||||||
scopes,
|
scopes,
|
||||||
openBrowser: async (url) => {
|
openBrowser: async (url) => {
|
||||||
if (showAuthUrlOnly) {
|
if (showAuthUrlOnly) {
|
||||||
console.log("Visit:\n" + url);
|
writeStderr(`Visit:\n${url}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const browserName = getBrowserAppName(browser);
|
||||||
// To enforce Microsoft Edge instead of the default browser, use:
|
const options = browserName
|
||||||
// return open(url, {
|
? { wait: false, app: { name: browserName } }
|
||||||
// wait: false,
|
: { wait: false };
|
||||||
// app: {
|
return open(url, options).catch(() => {
|
||||||
// name: apps.edge,
|
writeStderr(`Visit:\n${url}`);
|
||||||
// },
|
|
||||||
// }).catch(() => {
|
|
||||||
// // If auto-open fails, provide URL for manual copy/paste.
|
|
||||||
// console.log("Visit:\n" + url);
|
|
||||||
// });
|
|
||||||
|
|
||||||
return open(url, { wait: false }).catch(() => {
|
|
||||||
// If auto-open fails, provide URL for manual copy/paste.
|
|
||||||
console.log("Visit:\n" + url);
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,32 +260,170 @@ export async function loginDeviceCode({ tenantId, clientId, scopes }) {
|
|||||||
return await pca.acquireTokenByDeviceCode({
|
return await pca.acquireTokenByDeviceCode({
|
||||||
scopes,
|
scopes,
|
||||||
deviceCodeCallback: (response) => {
|
deviceCodeCallback: (response) => {
|
||||||
console.log(response.message);
|
writeStderr(response.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout({ tenantId, clientId, userPrincipalName }) {
|
export async function login({
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
resourcesCsv,
|
||||||
|
useDeviceCode = false,
|
||||||
|
noBrowser = false,
|
||||||
|
browser,
|
||||||
|
}) {
|
||||||
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 (!userPrincipalName)
|
|
||||||
throw new Error("userPrincipalName is required");
|
const resources = parseResources(resourcesCsv);
|
||||||
|
const scopes = getScopesForResources(resources);
|
||||||
|
const pca = await createPca({ tenantId, clientId });
|
||||||
|
const session = await readSessionState();
|
||||||
|
const preferredAccount = await findAccountByUpn({
|
||||||
|
pca,
|
||||||
|
upn: session.activeAccountUpn,
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
let selectedAccount = preferredAccount;
|
||||||
|
for (let index = 0; index < resources.length; index += 1) {
|
||||||
|
const resource = resources[index];
|
||||||
|
const scope = [scopes[index]];
|
||||||
|
let token = await acquireTokenWithCache({
|
||||||
|
pca,
|
||||||
|
scopes: scope,
|
||||||
|
account: selectedAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
if (useDeviceCode) {
|
||||||
|
token = await pca.acquireTokenByDeviceCode({
|
||||||
|
scopes: scope,
|
||||||
|
deviceCodeCallback: (response) => {
|
||||||
|
writeStderr(response.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
token = await pca.acquireTokenInteractive({
|
||||||
|
scopes: scope,
|
||||||
|
openBrowser: async (url) => {
|
||||||
|
if (noBrowser) {
|
||||||
|
writeStderr(`Visit:\n${url}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const browserName = getBrowserAppName(browser);
|
||||||
|
const options = browserName
|
||||||
|
? { wait: false, app: { name: browserName } }
|
||||||
|
: { wait: false };
|
||||||
|
return open(url, options).catch(() => {
|
||||||
|
writeStderr(`Visit:\n${url}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token?.account) {
|
||||||
|
selectedAccount = token.account;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
resource,
|
||||||
|
expiresOn: token?.expiresOn?.toISOString?.() ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeAccountUpn = selectedAccount?.username ?? null;
|
||||||
|
if (activeAccountUpn) {
|
||||||
|
await writeSessionState({ activeAccountUpn });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountUpn: activeAccountUpn,
|
||||||
|
resources: results,
|
||||||
|
flow: useDeviceCode ? "device-code" : "interactive",
|
||||||
|
browserLaunchAttempted: !useDeviceCode && !noBrowser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function acquireResourceTokenFromLogin({
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
resource,
|
||||||
|
}) {
|
||||||
|
if (!tenantId) throw new Error("tenantId is required");
|
||||||
|
if (!clientId) throw new Error("clientId is required");
|
||||||
|
if (!resource) throw new Error("resource is required");
|
||||||
|
|
||||||
|
const scope = RESOURCE_SCOPE_BY_NAME[resource];
|
||||||
|
if (!scope) {
|
||||||
|
throw new Error(`Invalid resource '${resource}'. Allowed: ${DEFAULT_RESOURCES.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await readSessionState();
|
||||||
|
if (!session.activeAccountUpn) {
|
||||||
|
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
const pca = await createPca({ tenantId, clientId });
|
const pca = await createPca({ tenantId, clientId });
|
||||||
|
const account = await findAccountByUpn({
|
||||||
|
pca,
|
||||||
|
upn: session.activeAccountUpn,
|
||||||
|
});
|
||||||
|
if (!account) {
|
||||||
|
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
const accounts = await pca.getTokenCache().getAllAccounts();
|
try {
|
||||||
|
return await pca.acquireTokenSilent({
|
||||||
|
account,
|
||||||
|
scopes: [scope],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new Error(LOGIN_REQUIRED_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout({
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
clearAll = false,
|
||||||
|
userPrincipalName,
|
||||||
|
}) {
|
||||||
|
if (!tenantId) throw new Error("tenantId is required");
|
||||||
|
if (!clientId) throw new Error("clientId is required");
|
||||||
|
|
||||||
|
const pca = await createPca({ tenantId, clientId });
|
||||||
|
const tokenCache = pca.getTokenCache();
|
||||||
|
const accounts = await tokenCache.getAllAccounts();
|
||||||
|
const session = await readSessionState();
|
||||||
|
|
||||||
|
if (clearAll) {
|
||||||
|
for (const account of accounts) {
|
||||||
|
await tokenCache.removeAccount(account);
|
||||||
|
}
|
||||||
|
await clearSessionState();
|
||||||
|
return {
|
||||||
|
clearedAll: true,
|
||||||
|
signedOut: accounts.map((account) => account.username).filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUpn = normalizeUpn(userPrincipalName) || normalizeUpn(session.activeAccountUpn);
|
||||||
const accountToSignOut = accounts.find(
|
const accountToSignOut = accounts.find(
|
||||||
(acct) =>
|
(account) => normalizeUpn(account.username) === targetUpn,
|
||||||
acct.username?.toLowerCase() === userPrincipalName.toLowerCase(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!accountToSignOut) return false;
|
if (!accountToSignOut) {
|
||||||
|
await clearSessionState();
|
||||||
|
return { clearedAll: false, signedOut: [] };
|
||||||
|
}
|
||||||
|
|
||||||
pca.signOut({ account: accountToSignOut }).then(() => {
|
await tokenCache.removeAccount(accountToSignOut);
|
||||||
console.log(`Signed out ${userPrincipalName}`);
|
await clearSessionState();
|
||||||
return true;
|
return {
|
||||||
}).catch((err) => {
|
clearedAll: false,
|
||||||
console.error(`Failed to sign out ${userPrincipalName}:`, err);
|
signedOut: [accountToSignOut.username].filter(Boolean),
|
||||||
return false;
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/cli.js
28
src/cli.js
@@ -16,6 +16,8 @@ function usage() {
|
|||||||
return `Usage: sk-az-tools <command> [options]
|
return `Usage: sk-az-tools <command> [options]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
login Authenticate selected resources
|
||||||
|
logout Sign out and clear login state
|
||||||
list-apps List Entra applications
|
list-apps List Entra applications
|
||||||
list-app-permissions List required permissions for an app
|
list-app-permissions List required permissions for an app
|
||||||
list-app-grants List OAuth2 grants for an app
|
list-app-grants List OAuth2 grants for an app
|
||||||
@@ -40,6 +42,23 @@ Options:
|
|||||||
-f, --filter <glob> Filter by app display name glob`;
|
-f, --filter <glob> Filter by app display name glob`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function usageLogin() {
|
||||||
|
return `Usage: sk-az-tools login [--resources <csv>] [--use-device-code] [--no-browser] [--browser <name>] [global options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--resources <csv> Comma-separated resources: graph,devops,arm (default: all)
|
||||||
|
--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: default|brave|browser|browserPrivate|chrome|edge|firefox`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usageLogout() {
|
||||||
|
return `Usage: sk-az-tools logout [--all] [global options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--all Clear login state and remove all cached accounts`;
|
||||||
|
}
|
||||||
|
|
||||||
function usageListAppPermissions() {
|
function usageListAppPermissions() {
|
||||||
return `Usage: sk-az-tools list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s] [--filter|-f <glob>] [global options]
|
return `Usage: sk-az-tools list-app-permissions --app-id|-i <appId> [--resolve|-r] [--short|-s] [--filter|-f <glob>] [global options]
|
||||||
|
|
||||||
@@ -75,8 +94,12 @@ Options:
|
|||||||
|
|
||||||
function usageCommand(command) {
|
function usageCommand(command) {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
|
case "login":
|
||||||
|
return usageLogin();
|
||||||
case "list-apps":
|
case "list-apps":
|
||||||
return usageListApps();
|
return usageListApps();
|
||||||
|
case "logout":
|
||||||
|
return usageLogout();
|
||||||
case "list-app-permissions":
|
case "list-app-permissions":
|
||||||
return usageListAppPermissions();
|
return usageListAppPermissions();
|
||||||
case "list-app-grants":
|
case "list-app-grants":
|
||||||
@@ -109,6 +132,11 @@ async function main() {
|
|||||||
help: { type: "boolean", short: "h" },
|
help: { type: "boolean", short: "h" },
|
||||||
"display-name": { type: "string", short: "n" },
|
"display-name": { type: "string", short: "n" },
|
||||||
"app-id": { type: "string", short: "i" },
|
"app-id": { type: "string", short: "i" },
|
||||||
|
resources: { type: "string" },
|
||||||
|
"use-device-code": { type: "boolean" },
|
||||||
|
"no-browser": { type: "boolean" },
|
||||||
|
browser: { type: "string" },
|
||||||
|
all: { type: "boolean" },
|
||||||
resolve: { type: "boolean", short: "r" },
|
resolve: { type: "boolean", short: "r" },
|
||||||
short: { type: "boolean", short: "s" },
|
short: { type: "boolean", short: "s" },
|
||||||
filter: { type: "string", short: "f" },
|
filter: { type: "string", short: "f" },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { minimatch } from "minimatch";
|
|||||||
|
|
||||||
import { loadPublicConfig } from "../index.js";
|
import { loadPublicConfig } from "../index.js";
|
||||||
import { getGraphClient } from "../graph/auth.js";
|
import { getGraphClient } from "../graph/auth.js";
|
||||||
|
import { login, logout } from "../azure/index.js";
|
||||||
import {
|
import {
|
||||||
listApps,
|
listApps,
|
||||||
listAppPermissions,
|
listAppPermissions,
|
||||||
@@ -38,6 +39,27 @@ async function runTableCommand() {
|
|||||||
return readJsonFromStdin();
|
return readJsonFromStdin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runLoginCommand(values) {
|
||||||
|
const config = await loadPublicConfig();
|
||||||
|
return login({
|
||||||
|
tenantId: config.tenantId,
|
||||||
|
clientId: config.clientId,
|
||||||
|
resourcesCsv: values.resources,
|
||||||
|
useDeviceCode: Boolean(values["use-device-code"]),
|
||||||
|
noBrowser: Boolean(values["no-browser"]),
|
||||||
|
browser: values.browser,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runLogoutCommand(values) {
|
||||||
|
const config = await loadPublicConfig();
|
||||||
|
return logout({
|
||||||
|
tenantId: config.tenantId,
|
||||||
|
clientId: config.clientId,
|
||||||
|
clearAll: Boolean(values.all),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function runListAppsCommand(values) {
|
async function runListAppsCommand(values) {
|
||||||
const { client } = await getGraphClientFromPublicConfig();
|
const { client } = await getGraphClientFromPublicConfig();
|
||||||
let result = await listApps(client, {
|
let result = await listApps(client, {
|
||||||
@@ -98,6 +120,10 @@ async function runListResourcePermissionsCommand(values) {
|
|||||||
|
|
||||||
export async function runCommand(command, values) {
|
export async function runCommand(command, values) {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
|
case "login":
|
||||||
|
return runLoginCommand(values);
|
||||||
|
case "logout":
|
||||||
|
return runLogoutCommand(values);
|
||||||
case "table":
|
case "table":
|
||||||
return runTableCommand();
|
return runTableCommand();
|
||||||
case "list-apps":
|
case "list-apps":
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { loginInteractive } from "../azure/index.js";
|
|
||||||
import { Client } from "@microsoft/microsoft-graph-client";
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
|
import { acquireResourceTokenFromLogin } from "../azure/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize and return a Microsoft Graph client
|
* Initialize and return a Microsoft Graph client
|
||||||
@@ -13,10 +13,10 @@ import { Client } from "@microsoft/microsoft-graph-client";
|
|||||||
* @returns { Promise<{ graphApiToken: Object, client: Object }> } An object containing the Graph API token and client
|
* @returns { Promise<{ graphApiToken: Object, client: Object }> } An object containing the Graph API token and client
|
||||||
*/
|
*/
|
||||||
export async function getGraphClient({ tenantId, clientId }) {
|
export async function getGraphClient({ tenantId, clientId }) {
|
||||||
const graphApiToken = await loginInteractive({
|
const graphApiToken = await acquireResourceTokenFromLogin({
|
||||||
tenantId,
|
tenantId,
|
||||||
clientId,
|
clientId,
|
||||||
scopes: ["https://graph.microsoft.com/.default"],
|
resource: "graph",
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = Client.init({
|
const client = Client.init({
|
||||||
|
|||||||
Reference in New Issue
Block a user