feat(graph): add --resolve for list-app-permissions

This commit is contained in:
2026-02-08 13:19:50 +01:00
parent 2474fa79d3
commit 61111cc082
2 changed files with 93 additions and 3 deletions

View File

@@ -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 <name>]
list-app-permissions --app-id <appId>
list-app-permissions --app-id <appId> [--resolve]
list-app-grants --app-id <appId>
table [--pretty]
Options:
--display-name <name> Filter apps by exact display name
--app-id <appId> Application (client) ID
--resolve Resolve permission GUIDs to human-readable values
--query <jmespath> Filter output JSON using JMESPath
--pretty Use normalized column widths for Markdown table output
--output <format> 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":

View File

@@ -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<Array> }
*/
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.
*