From 61111cc082c4203a8a1b6cc4a9c84ed95d423f25 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sun, 8 Feb 2026 13:19:50 +0100 Subject: [PATCH] feat(graph): add --resolve for list-app-permissions --- src/cli.js | 15 +++++++-- src/graph/app.js | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/cli.js b/src/cli.js index 1fbe9f7..f134708 100755 --- a/src/cli.js +++ b/src/cli.js @@ -7,7 +7,12 @@ import jmespath from "jmespath"; import { loadPublicConfig } from "./index.js"; import { getGraphClient } from "./graph/auth.js"; -import { listApps, listAppPermissions, listAppGrants } from "./graph/app.js"; +import { + listApps, + listAppPermissions, + listAppPermissionsResolved, + listAppGrants, +} from "./graph/app.js"; import { toMarkdownTable } from "./markdown.js"; function usage() { @@ -15,13 +20,14 @@ function usage() { Commands: list-apps [--display-name ] - list-app-permissions --app-id + list-app-permissions --app-id [--resolve] list-app-grants --app-id table [--pretty] Options: --display-name Filter apps by exact display name --app-id Application (client) ID + --resolve Resolve permission GUIDs to human-readable values --query Filter output JSON using JMESPath --pretty Use normalized column widths for Markdown table output --output Output format: json|table|prettytable (default: json) @@ -73,6 +79,7 @@ async function main() { help: { type: "boolean", short: "h" }, "display-name": { type: "string" }, "app-id": { type: "string" }, + resolve: { type: "boolean" }, query: { type: "string" }, pretty: { type: "boolean" }, output: { type: "string" }, @@ -117,7 +124,9 @@ async function main() { tenantId: config.tenantId, clientId: config.clientId, }); - result = await listAppPermissions(client, values["app-id"]); + result = values.resolve + ? await listAppPermissionsResolved(client, values["app-id"]) + : await listAppPermissions(client, values["app-id"]); } break; case "list-app-grants": diff --git a/src/graph/app.js b/src/graph/app.js index f7e0c89..0051d87 100644 --- a/src/graph/app.js +++ b/src/graph/app.js @@ -84,6 +84,87 @@ export async function listAppPermissions(client, appId) { : []; } +/** + * List required resource access in a resolved, human-readable form. + * + * @param { Object } client + * @param { string } appId + * @returns { Promise } + */ +export async function listAppPermissionsResolved(client, appId) { + const requiredResourceAccess = await listAppPermissions(client, appId); + if (!Array.isArray(requiredResourceAccess) || requiredResourceAccess.length === 0) { + return []; + } + + const resourceAppIds = [...new Set( + requiredResourceAccess + .map((entry) => entry?.resourceAppId) + .filter(Boolean), + )]; + + const resourceDefinitions = await Promise.all(resourceAppIds.map(async (resourceAppId) => { + const result = await client + .api("/servicePrincipals") + .filter(`appId eq '${resourceAppId}'`) + .select("appId,displayName,oauth2PermissionScopes,appRoles") + .get(); + + const sp = Array.isArray(result?.value) && result.value.length > 0 + ? result.value[0] + : null; + + const scopesById = new Map( + (sp?.oauth2PermissionScopes ?? []).map((scope) => [scope.id, scope]), + ); + const rolesById = new Map( + (sp?.appRoles ?? []).map((role) => [role.id, role]), + ); + + return { + resourceAppId, + resourceDisplayName: sp?.displayName ?? null, + scopesById, + rolesById, + }; + })); + + const byResourceAppId = new Map( + resourceDefinitions.map((entry) => [entry.resourceAppId, entry]), + ); + + const rows = []; + for (const resourceEntry of requiredResourceAccess) { + const resourceMeta = byResourceAppId.get(resourceEntry.resourceAppId); + const resourceAccessItems = Array.isArray(resourceEntry?.resourceAccess) + ? resourceEntry.resourceAccess + : []; + + for (const item of resourceAccessItems) { + const permissionType = item?.type ?? null; + const permissionId = item?.id ?? null; + const resolved = permissionType === "Scope" + ? resourceMeta?.scopesById.get(permissionId) + : resourceMeta?.rolesById.get(permissionId); + + rows.push({ + resourceAppId: resourceEntry.resourceAppId ?? null, + resourceDisplayName: resourceMeta?.resourceDisplayName ?? null, + permissionId, + permissionType, + permissionValue: resolved?.value ?? null, + permissionDisplayName: + resolved?.adminConsentDisplayName ?? + resolved?.userConsentDisplayName ?? + resolved?.displayName ?? + null, + }); + } + } + + return rows; +} + /** * List delegated OAuth2 permission grants for an application by appId. *