@knowttl/mg-axi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1138 -0
- package/dist/api.js +953 -0
- package/dist/app-auth.js +47 -0
- package/dist/auth.js +59 -0
- package/dist/bin/mg-axi.js +7 -0
- package/dist/catalogue.js +2191 -0
- package/dist/cli.js +1314 -0
- package/dist/docs.js +559 -0
- package/dist/doctor.js +75 -0
- package/dist/entra-access-reviews.js +425 -0
- package/dist/entra-apps.js +374 -0
- package/dist/entra-audit-logs.js +381 -0
- package/dist/entra-auth-methods.js +296 -0
- package/dist/entra-ca-policy-update.js +353 -0
- package/dist/entra-certificate-auth.js +384 -0
- package/dist/entra-conditional-access.js +317 -0
- package/dist/entra-contacts.js +827 -0
- package/dist/entra-contracts.js +252 -0
- package/dist/entra-custom-security-attributes.js +464 -0
- package/dist/entra-data-policy-operations.js +274 -0
- package/dist/entra-delegated-admin.js +468 -0
- package/dist/entra-deleted-items.js +461 -0
- package/dist/entra-directory-objects.js +274 -0
- package/dist/entra-directory.js +411 -0
- package/dist/entra-domains.js +399 -0
- package/dist/entra-entitlement-management.js +645 -0
- package/dist/entra-federation-configurations.js +290 -0
- package/dist/entra-grants.js +272 -0
- package/dist/entra-group-lifecycle.js +349 -0
- package/dist/entra-group-member-add.js +278 -0
- package/dist/entra-groups.js +336 -0
- package/dist/entra-identity-providers.js +297 -0
- package/dist/entra-lifecycle-workflows.js +673 -0
- package/dist/entra-multi-tenant-organization.js +309 -0
- package/dist/entra-on-premises-synchronization.js +231 -0
- package/dist/entra-organization.js +398 -0
- package/dist/entra-risk-dismiss.js +191 -0
- package/dist/entra-risk.js +404 -0
- package/dist/entra-roles.js +310 -0
- package/dist/entra-subscriptions.js +289 -0
- package/dist/entra-tenant-information.js +152 -0
- package/dist/entra-terms-of-use.js +380 -0
- package/dist/entra-user-revoke-sessions.js +170 -0
- package/dist/entra-user-update.js +170 -0
- package/dist/entra-users.js +211 -0
- package/dist/graph-session.js +1034 -0
- package/dist/msal-app-provider.js +42 -0
- package/dist/msal-provider.js +82 -0
- package/dist/mutations.js +529 -0
- package/dist/profiles.js +139 -0
- package/dist/setup.js +116 -0
- package/dist/version.js +2 -0
- package/docs/coverage.md +1028 -0
- package/inventory/operations.json +4 -0
- package/inventory/schema.json +71 -0
- package/package.json +32 -0
- package/skills/mg-axi/SKILL.md +293 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
// EXT-02 entitlement-management reads: the read-only catalog and
|
|
3
|
+
// access-package mapping plus the assignment and request reads. See
|
|
4
|
+
// README.md for the supported commands and usage. Operation construction
|
|
5
|
+
// stays beside its command; the shared session owns URLs, credentials,
|
|
6
|
+
// paging, retries and error translation, and the SDK owns TOON rendering.
|
|
7
|
+
// This module only maps flags to session calls and projects rows for
|
|
8
|
+
// compact output.
|
|
9
|
+
//
|
|
10
|
+
// Reviewed against the v1.0 entitlementmanagement-list-catalogs,
|
|
11
|
+
// accesspackagecatalog-get, entitlementmanagement-list-accesspackages,
|
|
12
|
+
// accesspackage-get and entitlementmanagement-list-assignmentpolicies
|
|
13
|
+
// operation documentation, the accessPackageAssignmentPolicy resource
|
|
14
|
+
// contract, and the pinned v1.0 metadata property sets for
|
|
15
|
+
// accessPackageCatalog, accessPackage, accessPackageAssignmentPolicy and
|
|
16
|
+
// accessPackageResourceRoleScope, on 2026-10-05. All twelve reads take D/A
|
|
17
|
+
// EntitlementManagement.Read.All as the least privileged choice; delegated
|
|
18
|
+
// personal Microsoft accounts are not supported. Delegated callers
|
|
19
|
+
// additionally need a supported Entra role with catalog visibility (Global
|
|
20
|
+
// Reader and Identity Governance Administrator are among the supported
|
|
21
|
+
// roles). The four lists document the general OData query parameters, so
|
|
22
|
+
// --filter passes through as plain $filter with no $count or
|
|
23
|
+
// ConsistencyLevel contract ($search/$expand/$orderby stay unreviewed and
|
|
24
|
+
// $top/$skip stay server-side paging concerns); singles document $select
|
|
25
|
+
// only and $count routes take no query at all. Navigation properties
|
|
26
|
+
// (a catalog's accessPackages/resources/scopes/roles, a package's catalog
|
|
27
|
+
// and incompatible sets, a policy's questions/extensions, a role scope's
|
|
28
|
+
// role/scope links) need $expand and stay out: they belong to later slices,
|
|
29
|
+
// never to these reads. Approval and subject reads carry personal data and
|
|
30
|
+
// belong to later parts, never to these reads. Entitlement management needs
|
|
31
|
+
// P2 or ID Governance depending on capability, never one uniform licence.
|
|
32
|
+
//
|
|
33
|
+
// The assignment and assignment-request reads below are the personal-data
|
|
34
|
+
// part deferred above. Reviewed against the v1.0
|
|
35
|
+
// entitlementmanagement-list-assignments, accesspackageassignment-get,
|
|
36
|
+
// entitlementmanagement-list-assignmentrequests and
|
|
37
|
+
// accesspackageassignmentrequest-get operation documentation and the
|
|
38
|
+
// accessPackageAssignment and accessPackageAssignmentRequest resource
|
|
39
|
+
// contracts, on 2026-10-05. All six reads take D/A
|
|
40
|
+
// EntitlementManagement.Read.All as the least privileged choice; delegated
|
|
41
|
+
// personal Microsoft accounts are not supported. Delegated callers
|
|
42
|
+
// additionally need Catalog reader (least privileged), Catalog creator or
|
|
43
|
+
// Access package manager, or a supported Entra role (Security Reader,
|
|
44
|
+
// Global Reader, Compliance Administrator, Security Administrator or
|
|
45
|
+
// Identity Governance Administrator). The two lists document $select,
|
|
46
|
+
// $filter and $expand, so --filter passes through as plain $filter with no
|
|
47
|
+
// $count or ConsistencyLevel contract; the singles document $select with
|
|
48
|
+
// $expand, and the $count routes take no query at all. The wire resources
|
|
49
|
+
// carry no target or access-package linkage as scalars, so each list and
|
|
50
|
+
// show sends one fixed documented $expand set in code (target and
|
|
51
|
+
// accessPackage for assignments; accessPackage and assignment for
|
|
52
|
+
// requests) and projects only flattened linkage identifiers from it:
|
|
53
|
+
// targetId, targetDisplayName and accessPackageId on assignments,
|
|
54
|
+
// accessPackageId and assignmentId on requests. No user-supplied $expand
|
|
55
|
+
// exists anywhere: --select names only reviewed scalar or flattened fields,
|
|
56
|
+
// and navigation names (target, accessPackage, assignment, requestor,
|
|
57
|
+
// assignmentPolicy) fail as unknown properties. The expanded payloads
|
|
58
|
+
// arrive whole and are projected locally, so defaults stay minimal:
|
|
59
|
+
// request justification text and answers ride only behind an explicit
|
|
60
|
+
// --select, and the requestor subject, the assignment policy link and the
|
|
61
|
+
// filterByCurrentUser/additionalAccess functions belong to later slices.
|
|
62
|
+
// No mutation (no request creation, cancellation, approval or reprocessing)
|
|
63
|
+
// and no beta.
|
|
64
|
+
// Catalogs expose the package containers; serviceDefault catalogs are
|
|
65
|
+
// platform-owned while userManaged catalogs are tenant-created. Only scalar
|
|
66
|
+
// properties carry a reviewed $select contract; accessPackages, resources,
|
|
67
|
+
// resourceScopes, resourceRoles and customWorkflowExtensions are navigation
|
|
68
|
+
// properties and stay out.
|
|
69
|
+
export const KNOWN_CATALOG_FIELDS = [
|
|
70
|
+
"id",
|
|
71
|
+
"displayName",
|
|
72
|
+
"description",
|
|
73
|
+
"catalogType",
|
|
74
|
+
"state",
|
|
75
|
+
"isExternallyVisible",
|
|
76
|
+
"createdDateTime",
|
|
77
|
+
"modifiedDateTime",
|
|
78
|
+
];
|
|
79
|
+
// Access packages expose the assignable bundles; isHidden marks packages
|
|
80
|
+
// withheld from request listings. The catalog link and incompatible sets
|
|
81
|
+
// are navigation properties and stay out.
|
|
82
|
+
export const KNOWN_ACCESS_PACKAGE_FIELDS = [
|
|
83
|
+
"id",
|
|
84
|
+
"displayName",
|
|
85
|
+
"description",
|
|
86
|
+
"isHidden",
|
|
87
|
+
"createdDateTime",
|
|
88
|
+
"modifiedDateTime",
|
|
89
|
+
];
|
|
90
|
+
// Assignment policies expose who may request, approval and review settings
|
|
91
|
+
// for one access package. The nested settings objects project as values;
|
|
92
|
+
// questions, custom-extension stages, accessPackage and catalog links are
|
|
93
|
+
// navigation properties and stay out.
|
|
94
|
+
export const KNOWN_ASSIGNMENT_POLICY_FIELDS = [
|
|
95
|
+
"id",
|
|
96
|
+
"displayName",
|
|
97
|
+
"description",
|
|
98
|
+
"allowedTargetScope",
|
|
99
|
+
"automaticRequestSettings",
|
|
100
|
+
"createdDateTime",
|
|
101
|
+
"expiration",
|
|
102
|
+
"modifiedDateTime",
|
|
103
|
+
"notificationSettings",
|
|
104
|
+
"requestApprovalSettings",
|
|
105
|
+
"requestorSettings",
|
|
106
|
+
"reviewSettings",
|
|
107
|
+
"specificAllowedTargets",
|
|
108
|
+
];
|
|
109
|
+
// Resource-role scopes expose the role-plus-scope pairs one access package
|
|
110
|
+
// grants. Only the pairing identity carries a reviewed $select contract;
|
|
111
|
+
// the role and scope links are navigation properties and stay out.
|
|
112
|
+
export const KNOWN_RESOURCE_ROLE_SCOPE_FIELDS = [
|
|
113
|
+
"id",
|
|
114
|
+
"createdDateTime",
|
|
115
|
+
];
|
|
116
|
+
// Assignments expose one subject's delivered package grant. The wire
|
|
117
|
+
// resource carries only scalars; target and access-package linkage arrives
|
|
118
|
+
// through the fixed $expand and is projected as the flattened targetId,
|
|
119
|
+
// targetDisplayName and accessPackageId fields below, never as nested
|
|
120
|
+
// subject or package objects.
|
|
121
|
+
export const KNOWN_ASSIGNMENT_FIELDS = [
|
|
122
|
+
"id",
|
|
123
|
+
"state",
|
|
124
|
+
"status",
|
|
125
|
+
"expiredDateTime",
|
|
126
|
+
"schedule",
|
|
127
|
+
"customExtensionCalloutInstances",
|
|
128
|
+
"targetId",
|
|
129
|
+
"targetDisplayName",
|
|
130
|
+
"accessPackageId",
|
|
131
|
+
];
|
|
132
|
+
// Assignment requests expose who asked for what and how processing ended.
|
|
133
|
+
// Justification text and answers are personal-data payloads: reviewed here
|
|
134
|
+
// so an explicit --select can fetch them, never in any default. The
|
|
135
|
+
// requestor subject, the resulting-assignment detail beyond its id and the
|
|
136
|
+
// access-package detail beyond its id belong to later slices.
|
|
137
|
+
export const KNOWN_ASSIGNMENT_REQUEST_FIELDS = [
|
|
138
|
+
"id",
|
|
139
|
+
"requestType",
|
|
140
|
+
"state",
|
|
141
|
+
"status",
|
|
142
|
+
"justification",
|
|
143
|
+
"schedule",
|
|
144
|
+
"createdDateTime",
|
|
145
|
+
"completedDateTime",
|
|
146
|
+
"answers",
|
|
147
|
+
"customExtensionCalloutInstances",
|
|
148
|
+
"accessPackageId",
|
|
149
|
+
"assignmentId",
|
|
150
|
+
];
|
|
151
|
+
const CATALOG_KNOWN = new Set(KNOWN_CATALOG_FIELDS);
|
|
152
|
+
const ACCESS_PACKAGE_KNOWN = new Set(KNOWN_ACCESS_PACKAGE_FIELDS);
|
|
153
|
+
const ASSIGNMENT_POLICY_KNOWN = new Set(KNOWN_ASSIGNMENT_POLICY_FIELDS);
|
|
154
|
+
const RESOURCE_ROLE_SCOPE_KNOWN = new Set(KNOWN_RESOURCE_ROLE_SCOPE_FIELDS);
|
|
155
|
+
const ASSIGNMENT_KNOWN = new Set(KNOWN_ASSIGNMENT_FIELDS);
|
|
156
|
+
const ASSIGNMENT_REQUEST_KNOWN = new Set(KNOWN_ASSIGNMENT_REQUEST_FIELDS);
|
|
157
|
+
// Flattened linkage identifiers are always fetched through the fixed
|
|
158
|
+
// $expand, never through server $select, so they are stripped from the
|
|
159
|
+
// request selection but always count as fetched for --fields.
|
|
160
|
+
const ASSIGNMENT_SYNTHETIC = new Set(["targetId", "targetDisplayName", "accessPackageId"]);
|
|
161
|
+
const ASSIGNMENT_REQUEST_SYNTHETIC = new Set(["accessPackageId", "assignmentId"]);
|
|
162
|
+
// Compact rows: identifiers plus container state, package visibility,
|
|
163
|
+
// policy scope or pairing identity.
|
|
164
|
+
const DEFAULT_CATALOG_LIST_SELECT = ["id", "displayName", "state", "catalogType"];
|
|
165
|
+
const DEFAULT_CATALOG_SHOW_SELECT = [...KNOWN_CATALOG_FIELDS];
|
|
166
|
+
const DEFAULT_ACCESS_PACKAGE_LIST_SELECT = ["id", "displayName", "isHidden"];
|
|
167
|
+
const DEFAULT_ACCESS_PACKAGE_SHOW_SELECT = [...KNOWN_ACCESS_PACKAGE_FIELDS];
|
|
168
|
+
const DEFAULT_ASSIGNMENT_POLICY_LIST_SELECT = ["id", "displayName", "allowedTargetScope"];
|
|
169
|
+
const DEFAULT_ASSIGNMENT_POLICY_SHOW_SELECT = [...KNOWN_ASSIGNMENT_POLICY_FIELDS];
|
|
170
|
+
const DEFAULT_RESOURCE_ROLE_SCOPE_LIST_SELECT = ["id", "createdDateTime"];
|
|
171
|
+
const DEFAULT_RESOURCE_ROLE_SCOPE_SHOW_SELECT = [...KNOWN_RESOURCE_ROLE_SCOPE_FIELDS];
|
|
172
|
+
// Assignment lists default to the linkage that makes a grant row actionable
|
|
173
|
+
// (which subject holds which package, and in what state) plus its dates;
|
|
174
|
+
// lifecycle detail stays behind an explicit --select.
|
|
175
|
+
const DEFAULT_ASSIGNMENT_LIST_SELECT = ["id", "state", "targetId", "targetDisplayName", "accessPackageId", "expiredDateTime", "schedule"];
|
|
176
|
+
const DEFAULT_ASSIGNMENT_SHOW_SELECT = [...KNOWN_ASSIGNMENT_FIELDS];
|
|
177
|
+
// Request lists default to the linkage plus the request-type discriminator:
|
|
178
|
+
// without userAdd/adminAdd/remove context a request row is uninterpretable.
|
|
179
|
+
// Justification text and answers stay behind an explicit --select, and the
|
|
180
|
+
// show default keeps that exclusion even though it is the full row view.
|
|
181
|
+
const DEFAULT_ASSIGNMENT_REQUEST_LIST_SELECT = ["id", "requestType", "state", "accessPackageId", "assignmentId", "createdDateTime", "completedDateTime", "schedule"];
|
|
182
|
+
const DEFAULT_ASSIGNMENT_REQUEST_SHOW_SELECT = ["id", "requestType", "state", "status", "schedule", "createdDateTime", "completedDateTime", "customExtensionCalloutInstances", "accessPackageId", "assignmentId"];
|
|
183
|
+
// One fixed documented $expand per family, set in code and never from user
|
|
184
|
+
// input. The list and get pages document exactly these expansions.
|
|
185
|
+
const ASSIGNMENT_EXPAND = "target,accessPackage";
|
|
186
|
+
const ASSIGNMENT_REQUEST_EXPAND = "accessPackage,assignment";
|
|
187
|
+
export const DEFAULT_ENTITLEMENT_MANAGEMENT_SCOPES = ["https://graph.microsoft.com/EntitlementManagement.Read.All"];
|
|
188
|
+
const TRUNCATE_AT = 500;
|
|
189
|
+
function fieldList(raw, flag, known, knownList, help) {
|
|
190
|
+
const parts = String(raw)
|
|
191
|
+
.split(",")
|
|
192
|
+
.map(part => part.trim())
|
|
193
|
+
.filter(part => part.length > 0);
|
|
194
|
+
if (!parts.length)
|
|
195
|
+
throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
|
|
196
|
+
const fields = [];
|
|
197
|
+
for (const part of parts) {
|
|
198
|
+
if (!known.has(part)) {
|
|
199
|
+
throw new AxiError(`Unknown property ${part} in --${flag}`, "VALIDATION_ERROR", [
|
|
200
|
+
help,
|
|
201
|
+
`Known properties: ${knownList.join(", ")}`,
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
if (!fields.includes(part))
|
|
205
|
+
fields.push(part);
|
|
206
|
+
}
|
|
207
|
+
return fields;
|
|
208
|
+
}
|
|
209
|
+
function scopesFor(flags, profile, defaults, help) {
|
|
210
|
+
if (profile.mode === "application") {
|
|
211
|
+
if (flags.scopes !== undefined) {
|
|
212
|
+
throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
if (flags.scopes === undefined)
|
|
217
|
+
return [...defaults];
|
|
218
|
+
const scopes = String(flags.scopes)
|
|
219
|
+
.split(",")
|
|
220
|
+
.map(scope => scope.trim())
|
|
221
|
+
.filter(scope => scope.length > 0);
|
|
222
|
+
return scopes;
|
|
223
|
+
}
|
|
224
|
+
function truncateValue(value, full) {
|
|
225
|
+
if (full)
|
|
226
|
+
return { value, truncated: false };
|
|
227
|
+
if (typeof value === "string" && value.length > TRUNCATE_AT) {
|
|
228
|
+
return { value: `${value.slice(0, TRUNCATE_AT)}... (truncated, ${value.length} chars total)`, truncated: true };
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
const results = value.map(item => truncateValue(item, full));
|
|
232
|
+
return { value: results.map(result => result.value), truncated: results.some(result => result.truncated) };
|
|
233
|
+
}
|
|
234
|
+
if (value !== null && typeof value === "object") {
|
|
235
|
+
let truncated = false;
|
|
236
|
+
const entries = Object.entries(value).map(([key, item]) => {
|
|
237
|
+
const result = truncateValue(item, full);
|
|
238
|
+
truncated = truncated || result.truncated;
|
|
239
|
+
return [key, result.value];
|
|
240
|
+
});
|
|
241
|
+
return { value: Object.fromEntries(entries), truncated };
|
|
242
|
+
}
|
|
243
|
+
return { value, truncated: false };
|
|
244
|
+
}
|
|
245
|
+
// Local projection preserves Graph's null/missing distinction: an explicit
|
|
246
|
+
// null stays null, an absent property stays absent and is never synthesized.
|
|
247
|
+
// The optional links view merges flattened $expand linkage beside the
|
|
248
|
+
// top-level scalars before that check, so absent linkage stays absent.
|
|
249
|
+
function project(row, fields, full, links) {
|
|
250
|
+
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
251
|
+
const view = links === undefined ? source : { ...source, ...links(source) };
|
|
252
|
+
const projected = {};
|
|
253
|
+
let truncated = false;
|
|
254
|
+
for (const field of fields) {
|
|
255
|
+
if (!Object.hasOwn(view, field))
|
|
256
|
+
continue;
|
|
257
|
+
const result = truncateValue(view[field], full);
|
|
258
|
+
projected[field] = result.value;
|
|
259
|
+
truncated = truncated || result.truncated;
|
|
260
|
+
}
|
|
261
|
+
return { row: projected, truncated };
|
|
262
|
+
}
|
|
263
|
+
function selectedFields(flags, defaults, known, knownList, help, synthetic) {
|
|
264
|
+
const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, "select", known, knownList, help);
|
|
265
|
+
const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, "fields", known, knownList, help);
|
|
266
|
+
// Flattened linkage rides the fixed $expand, so it always counts as
|
|
267
|
+
// fetched even when the narrowed --select omits it.
|
|
268
|
+
const missing = fields.find(field => !select.includes(field) && !(synthetic?.has(field) ?? false));
|
|
269
|
+
if (missing) {
|
|
270
|
+
throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
|
|
271
|
+
}
|
|
272
|
+
// Synthetic names are never server $select properties; an all-linkage
|
|
273
|
+
// selection omits $select and the server returns its default shape.
|
|
274
|
+
const fetch = synthetic === undefined ? [...select] : select.filter(field => !synthetic.has(field));
|
|
275
|
+
return { select, fields, fetch };
|
|
276
|
+
}
|
|
277
|
+
function profileHint(profileName) {
|
|
278
|
+
return `--profile ${shellValue(profileName)}`;
|
|
279
|
+
}
|
|
280
|
+
function shellValue(value) {
|
|
281
|
+
return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
282
|
+
}
|
|
283
|
+
function fullHint(command, flags, profileName) {
|
|
284
|
+
const args = Object.entries({ ...flags, ...(flags.cursor === undefined ? {} : { cursor: "-" }), profile: profileName, full: true })
|
|
285
|
+
.map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
|
|
286
|
+
return `mg-axi ${command} ${args.join(" ")}`;
|
|
287
|
+
}
|
|
288
|
+
// Denials carry the session's generic grant/role/licence cause;
|
|
289
|
+
// entitlement management adds the scope, roles and licensing that actually
|
|
290
|
+
// unlock these reads, because a 403 alone never says which prerequisite is
|
|
291
|
+
// missing.
|
|
292
|
+
function withGuidance(run, hints = ENTITLEMENT_MANAGEMENT_DENIAL_HINTS) {
|
|
293
|
+
return run().catch(error => {
|
|
294
|
+
if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
|
|
295
|
+
throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const ENTITLEMENT_MANAGEMENT_DENIAL_HINTS = [
|
|
301
|
+
"Entitlement-management reads need EntitlementManagement.Read.All plus a supported Entra role with catalog visibility for delegated access (Global Reader and Identity Governance Administrator are among the supported roles), or admin-consented EntitlementManagement.Read.All for application access; delegated personal Microsoft accounts are not supported",
|
|
302
|
+
"Entitlement management needs P2 or ID Governance depending on capability, not one uniform licence",
|
|
303
|
+
];
|
|
304
|
+
// Assignment and request reads document their own least-privileged
|
|
305
|
+
// delegation roles, so denials name Catalog reader first rather than the
|
|
306
|
+
// catalog-visibility roles above.
|
|
307
|
+
const ASSIGNMENT_DENIAL_HINTS = [
|
|
308
|
+
"Assignment and request reads need EntitlementManagement.Read.All plus Catalog reader (least privileged), Catalog creator or Access package manager, or a supported Entra role (Security Reader, Global Reader, Compliance Administrator, Security Administrator or Identity Governance Administrator) for delegated access, or admin-consented EntitlementManagement.Read.All (a supported role alone also works) for application access; delegated personal Microsoft accounts are not supported",
|
|
309
|
+
"Entitlement management needs P2 or ID Governance depending on capability, not one uniform licence",
|
|
310
|
+
];
|
|
311
|
+
function objectField(value) {
|
|
312
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
313
|
+
? value
|
|
314
|
+
: undefined;
|
|
315
|
+
}
|
|
316
|
+
function stringField(value) {
|
|
317
|
+
return typeof value === "string" ? value : undefined;
|
|
318
|
+
}
|
|
319
|
+
// The fixed $expand returns whole target and access-package objects; only
|
|
320
|
+
// the flattened linkage identifiers below survive projection, never subject
|
|
321
|
+
// contact detail or package contents.
|
|
322
|
+
function flattenAssignmentLinks(source) {
|
|
323
|
+
const links = {};
|
|
324
|
+
const target = objectField(source.target);
|
|
325
|
+
const targetId = target === undefined ? undefined : stringField(target.id);
|
|
326
|
+
if (targetId !== undefined)
|
|
327
|
+
links.targetId = targetId;
|
|
328
|
+
const targetDisplayName = target === undefined ? undefined : stringField(target.displayName);
|
|
329
|
+
if (targetDisplayName !== undefined)
|
|
330
|
+
links.targetDisplayName = targetDisplayName;
|
|
331
|
+
const accessPackageId = stringField(objectField(source.accessPackage)?.id);
|
|
332
|
+
if (accessPackageId !== undefined)
|
|
333
|
+
links.accessPackageId = accessPackageId;
|
|
334
|
+
return links;
|
|
335
|
+
}
|
|
336
|
+
// Requests flatten to bare linkage ids: the requestor subject and the
|
|
337
|
+
// expanded assignment and package detail stay out.
|
|
338
|
+
function flattenRequestLinks(source) {
|
|
339
|
+
const links = {};
|
|
340
|
+
const accessPackageId = stringField(objectField(source.accessPackage)?.id);
|
|
341
|
+
if (accessPackageId !== undefined)
|
|
342
|
+
links.accessPackageId = accessPackageId;
|
|
343
|
+
const assignmentId = stringField(objectField(source.assignment)?.id);
|
|
344
|
+
if (assignmentId !== undefined)
|
|
345
|
+
links.assignmentId = assignmentId;
|
|
346
|
+
return links;
|
|
347
|
+
}
|
|
348
|
+
const CATALOG_LIST = {
|
|
349
|
+
command: "entra entitlement catalog list",
|
|
350
|
+
key: "catalogs",
|
|
351
|
+
known: CATALOG_KNOWN,
|
|
352
|
+
knownList: KNOWN_CATALOG_FIELDS,
|
|
353
|
+
defaultSelect: DEFAULT_CATALOG_LIST_SELECT,
|
|
354
|
+
emptyNote: "0 entitlement catalogs matched; the absence of results is the answer, not an error",
|
|
355
|
+
standing: profileName => [
|
|
356
|
+
"Catalogs are package containers: a catalog never carries its access packages; list those on the package reads",
|
|
357
|
+
`Show one catalog: mg-axi entra entitlement catalog show --id <catalog-id> ${profileHint(profileName)}`,
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
const ACCESS_PACKAGE_LIST = {
|
|
361
|
+
command: "entra entitlement access-package list",
|
|
362
|
+
key: "accessPackages",
|
|
363
|
+
known: ACCESS_PACKAGE_KNOWN,
|
|
364
|
+
knownList: KNOWN_ACCESS_PACKAGE_FIELDS,
|
|
365
|
+
defaultSelect: DEFAULT_ACCESS_PACKAGE_LIST_SELECT,
|
|
366
|
+
emptyNote: "0 access packages matched; the absence of results is the answer, not an error",
|
|
367
|
+
standing: profileName => [
|
|
368
|
+
"Access packages are the assignable bundles; assignment and request detail lives on the assignment reads, never on these reads",
|
|
369
|
+
`Show one access package: mg-axi entra entitlement access-package show --id <access-package-id> ${profileHint(profileName)}`,
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
const ASSIGNMENT_POLICY_LIST = {
|
|
373
|
+
command: "entra entitlement assignment-policy list",
|
|
374
|
+
key: "assignmentPolicies",
|
|
375
|
+
known: ASSIGNMENT_POLICY_KNOWN,
|
|
376
|
+
knownList: KNOWN_ASSIGNMENT_POLICY_FIELDS,
|
|
377
|
+
defaultSelect: DEFAULT_ASSIGNMENT_POLICY_LIST_SELECT,
|
|
378
|
+
emptyNote: "0 assignment policies matched; an access package without policies has none, which is the answer, not an error",
|
|
379
|
+
standing: profileName => [
|
|
380
|
+
"Assignment policies name who may request and how approval and review run; questions and custom-extension stages belong to a later slice",
|
|
381
|
+
`Show one policy: mg-axi entra entitlement assignment-policy show --access-package <access-package-id> --id <policy-id> ${profileHint(profileName)}`,
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
const RESOURCE_ROLE_SCOPE_LIST = {
|
|
385
|
+
command: "entra entitlement resource-role-scope list",
|
|
386
|
+
key: "resourceRoleScopes",
|
|
387
|
+
known: RESOURCE_ROLE_SCOPE_KNOWN,
|
|
388
|
+
knownList: KNOWN_RESOURCE_ROLE_SCOPE_FIELDS,
|
|
389
|
+
defaultSelect: DEFAULT_RESOURCE_ROLE_SCOPE_LIST_SELECT,
|
|
390
|
+
emptyNote: "0 resource-role scopes matched; the absence of results is the answer, not an error",
|
|
391
|
+
standing: profileName => [
|
|
392
|
+
"Resource-role scopes are the role-plus-scope pairs a package grants; the linked role and scope detail needs $expand and belongs to a later slice",
|
|
393
|
+
`Show one resource-role scope: mg-axi entra entitlement resource-role-scope show --access-package <access-package-id> --id <scope-id> ${profileHint(profileName)}`,
|
|
394
|
+
],
|
|
395
|
+
};
|
|
396
|
+
const ASSIGNMENT_LIST = {
|
|
397
|
+
command: "entra entitlement assignment list",
|
|
398
|
+
key: "assignments",
|
|
399
|
+
known: ASSIGNMENT_KNOWN,
|
|
400
|
+
knownList: KNOWN_ASSIGNMENT_FIELDS,
|
|
401
|
+
defaultSelect: DEFAULT_ASSIGNMENT_LIST_SELECT,
|
|
402
|
+
synthetic: ASSIGNMENT_SYNTHETIC,
|
|
403
|
+
expand: ASSIGNMENT_EXPAND,
|
|
404
|
+
links: flattenAssignmentLinks,
|
|
405
|
+
hints: ASSIGNMENT_DENIAL_HINTS,
|
|
406
|
+
emptyNote: "0 assignments matched; the absence of results is the answer, not an error",
|
|
407
|
+
standing: profileName => [
|
|
408
|
+
"Assignments are delivered package grants: rows carry flattened linkage (targetId, targetDisplayName, accessPackageId) from one fixed documented $expand, never nested subject or package detail",
|
|
409
|
+
"Callers with only catalog-scoped roles must filter to one access package, e.g. --filter \"accessPackage/id eq '<access-package-id>'\"",
|
|
410
|
+
`Show one assignment: mg-axi entra entitlement assignment show --id <assignment-id> ${profileHint(profileName)}`,
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
const ASSIGNMENT_REQUEST_LIST = {
|
|
414
|
+
command: "entra entitlement assignment-request list",
|
|
415
|
+
key: "assignmentRequests",
|
|
416
|
+
known: ASSIGNMENT_REQUEST_KNOWN,
|
|
417
|
+
knownList: KNOWN_ASSIGNMENT_REQUEST_FIELDS,
|
|
418
|
+
defaultSelect: DEFAULT_ASSIGNMENT_REQUEST_LIST_SELECT,
|
|
419
|
+
synthetic: ASSIGNMENT_REQUEST_SYNTHETIC,
|
|
420
|
+
expand: ASSIGNMENT_REQUEST_EXPAND,
|
|
421
|
+
links: flattenRequestLinks,
|
|
422
|
+
hints: ASSIGNMENT_DENIAL_HINTS,
|
|
423
|
+
emptyNote: "0 assignment requests matched; the absence of results is the answer, not an error",
|
|
424
|
+
standing: profileName => [
|
|
425
|
+
"Requests carry flattened linkage (accessPackageId, assignmentId) from one fixed documented $expand; justification text and answers ride only behind an explicit --select, and the requestor subject belongs to a later slice",
|
|
426
|
+
"Callers with only catalog-scoped roles must filter to one access package, e.g. --filter \"accessPackage/id eq '<access-package-id>'\"",
|
|
427
|
+
`Show one request: mg-axi entra entitlement assignment-request show --id <request-id> ${profileHint(profileName)}`,
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
function accessPackageId(flags, help) {
|
|
431
|
+
const value = String(flags["access-package"] ?? "");
|
|
432
|
+
if (!value.trim())
|
|
433
|
+
throw new AxiError("--access-package needs the parent access-package ID", "VALIDATION_ERROR", [help]);
|
|
434
|
+
return value;
|
|
435
|
+
}
|
|
436
|
+
async function listCollection(shape, session, flags, profile, operation, help, profileName, params, resumeHint) {
|
|
437
|
+
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
438
|
+
if (cursor !== undefined && !cursor.trim())
|
|
439
|
+
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
440
|
+
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
441
|
+
const savedSelect = saved?.$select;
|
|
442
|
+
const isSyntheticResume = shape.synthetic !== undefined && cursor !== undefined && flags.select === undefined && flags.fields === undefined;
|
|
443
|
+
if (isSyntheticResume && savedSelect === undefined)
|
|
444
|
+
throw new AxiError("Repeat the original --select/--fields with --cursor for this resume", "VALIDATION_ERROR", [help, resumeHint]);
|
|
445
|
+
const { select, fields, fetch } = selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help, shape.synthetic);
|
|
446
|
+
if (isSyntheticResume && savedSelect !== undefined) {
|
|
447
|
+
const stripped = shape.defaultSelect.filter(field => !shape.synthetic.has(field));
|
|
448
|
+
const stored = fieldList(savedSelect, "select", shape.known, shape.knownList, help);
|
|
449
|
+
if (stored.length === stripped.length && stripped.every(field => stored.includes(field))) {
|
|
450
|
+
for (const field of shape.defaultSelect)
|
|
451
|
+
if (!select.includes(field))
|
|
452
|
+
select.push(field);
|
|
453
|
+
fields.splice(0, fields.length, ...select);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const savedFilter = saved?.$filter;
|
|
457
|
+
const filter = flags.filter === undefined ? savedFilter : String(flags.filter);
|
|
458
|
+
const scopes = scopesFor(flags, profile, DEFAULT_ENTITLEMENT_MANAGEMENT_SCOPES, help);
|
|
459
|
+
const full = flags.full === true;
|
|
460
|
+
// These lists document the general OData query parameters, so --filter
|
|
461
|
+
// passes through as plain $filter with no $count or ConsistencyLevel;
|
|
462
|
+
// $search/$expand/$orderby stay unreviewed and $top/$skip stay
|
|
463
|
+
// server-side paging concerns. Assignment and request lists additionally
|
|
464
|
+
// send their one fixed documented $expand (never a user flag); resumed
|
|
465
|
+
// queries replay the stored $select verbatim so resume never conflicts.
|
|
466
|
+
const sendSelect = cursor === undefined
|
|
467
|
+
? fetch
|
|
468
|
+
: saved?.$select === undefined ? [] : fieldList(saved.$select, "select", shape.known, shape.knownList, help);
|
|
469
|
+
const query = {};
|
|
470
|
+
if (sendSelect.length)
|
|
471
|
+
query.$select = sendSelect.join(",");
|
|
472
|
+
if (shape.expand !== undefined)
|
|
473
|
+
query.$expand = shape.expand;
|
|
474
|
+
if (filter !== undefined)
|
|
475
|
+
query.$filter = filter;
|
|
476
|
+
const args = { profile, operation, params, query, scopes };
|
|
477
|
+
if (cursor !== undefined)
|
|
478
|
+
args.cursor = cursor;
|
|
479
|
+
if (flags.all === true) {
|
|
480
|
+
if (flags.limit !== undefined)
|
|
481
|
+
throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
|
|
485
|
+
}
|
|
486
|
+
const result = await withGuidance(() => session.collect(args), shape.hints);
|
|
487
|
+
const effectiveFlags = { ...flags, ...(result.query.$select === undefined ? {} : { select: select.join(",") }) };
|
|
488
|
+
if (result.query.$filter !== undefined)
|
|
489
|
+
effectiveFlags.filter = result.query.$filter;
|
|
490
|
+
const rows = [];
|
|
491
|
+
let truncated = false;
|
|
492
|
+
for (const row of result.value) {
|
|
493
|
+
const projected = project(row, fields, full, shape.links);
|
|
494
|
+
rows.push(projected.row);
|
|
495
|
+
truncated = truncated || projected.truncated;
|
|
496
|
+
}
|
|
497
|
+
const truncationHints = truncated ? [fullHint(shape.command, effectiveFlags, profileName)] : [];
|
|
498
|
+
if (truncated && cursor !== undefined)
|
|
499
|
+
truncationHints.push("Supply the original input cursor on stdin to replay this result with --full");
|
|
500
|
+
const standing = [...shape.standing(profileName)];
|
|
501
|
+
if (!result.complete) {
|
|
502
|
+
return {
|
|
503
|
+
[shape.key]: rows,
|
|
504
|
+
count: { returned: rows.length, complete: false, reason: result.reason },
|
|
505
|
+
cursor: result.cursor,
|
|
506
|
+
help: [...truncationHints, resumeHint, ...standing],
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const count = { returned: rows.length, complete: true };
|
|
510
|
+
if (!rows.length) {
|
|
511
|
+
return { [shape.key]: rows, count, help: [shape.emptyNote, ...standing] };
|
|
512
|
+
}
|
|
513
|
+
return { [shape.key]: rows, count, help: [...truncationHints, ...standing] };
|
|
514
|
+
}
|
|
515
|
+
async function showOne(key, command, known, knownList, defaultSelect, session, flags, profile, operation, help, profileName, params, malformed, extra) {
|
|
516
|
+
const { select, fields, fetch } = selectedFields(flags, defaultSelect, known, knownList, help, extra?.synthetic);
|
|
517
|
+
const scopes = scopesFor(flags, profile, DEFAULT_ENTITLEMENT_MANAGEMENT_SCOPES, help);
|
|
518
|
+
const full = flags.full === true;
|
|
519
|
+
const query = {};
|
|
520
|
+
if (fetch.length)
|
|
521
|
+
query.$select = fetch.join(",");
|
|
522
|
+
if (extra?.expand !== undefined)
|
|
523
|
+
query.$expand = extra.expand;
|
|
524
|
+
const raw = await withGuidance(() => session.execute({
|
|
525
|
+
profile,
|
|
526
|
+
operation,
|
|
527
|
+
params,
|
|
528
|
+
query,
|
|
529
|
+
scopes,
|
|
530
|
+
}), extra?.hints);
|
|
531
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
532
|
+
throw new AxiError(malformed, "GRAPH_ERROR", [
|
|
533
|
+
"Single-object reads carry one object; treat anything else as unknown, not empty",
|
|
534
|
+
]);
|
|
535
|
+
}
|
|
536
|
+
const { row, truncated } = project(raw, fields, full, extra?.links);
|
|
537
|
+
if (truncated)
|
|
538
|
+
return { [key]: row, help: [fullHint(command, flags, profileName)] };
|
|
539
|
+
return { [key]: row };
|
|
540
|
+
}
|
|
541
|
+
async function countCollection(key, emptyNote, session, flags, profile, operation, help, profileName, hints) {
|
|
542
|
+
void profileName;
|
|
543
|
+
const scopes = scopesFor(flags, profile, DEFAULT_ENTITLEMENT_MANAGEMENT_SCOPES, help);
|
|
544
|
+
// The $count route returns a text/plain integer scalar rather than a
|
|
545
|
+
// JSON collection, so the leaf reads it through session.execute with
|
|
546
|
+
// scalar mode and accepts only a non-negative integer. The $count route
|
|
547
|
+
// carries no operation-level documentation page and takes no
|
|
548
|
+
// --filter/--select/--limit/--cursor.
|
|
549
|
+
const raw = await withGuidance(() => session.execute({ profile, operation, scopes, scalar: true }), hints);
|
|
550
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
551
|
+
throw new AxiError(`Graph returned a malformed ${key} count body`, "GRAPH_ERROR", [
|
|
552
|
+
"Counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
|
|
553
|
+
]);
|
|
554
|
+
}
|
|
555
|
+
const count = { returned: raw, complete: true };
|
|
556
|
+
return raw === 0 ? { count, help: [emptyNote] } : { count };
|
|
557
|
+
}
|
|
558
|
+
export async function listCatalogs(session, flags, profile, operation, help, profileName) {
|
|
559
|
+
return listCollection(CATALOG_LIST, session, flags, profile, operation, help, profileName, {}, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
|
|
560
|
+
}
|
|
561
|
+
export async function showCatalog(session, flags, profile, operation, help, profileName) {
|
|
562
|
+
return showOne("catalog", "entra entitlement catalog show", CATALOG_KNOWN, KNOWN_CATALOG_FIELDS, DEFAULT_CATALOG_SHOW_SELECT, session, flags, profile, operation, help, profileName, { "accessPackageCatalog-id": String(flags.id) }, "Graph returned a malformed entitlement catalog body");
|
|
563
|
+
}
|
|
564
|
+
export async function countCatalogs(session, flags, profile, operation, help, profileName) {
|
|
565
|
+
return countCollection("catalog", CATALOG_LIST.emptyNote, session, flags, profile, operation, help, profileName);
|
|
566
|
+
}
|
|
567
|
+
export async function listAccessPackages(session, flags, profile, operation, help, profileName) {
|
|
568
|
+
return listCollection(ACCESS_PACKAGE_LIST, session, flags, profile, operation, help, profileName, {}, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
|
|
569
|
+
}
|
|
570
|
+
export async function showAccessPackage(session, flags, profile, operation, help, profileName) {
|
|
571
|
+
return showOne("accessPackage", "entra entitlement access-package show", ACCESS_PACKAGE_KNOWN, KNOWN_ACCESS_PACKAGE_FIELDS, DEFAULT_ACCESS_PACKAGE_SHOW_SELECT, session, flags, profile, operation, help, profileName, { "accessPackage-id": String(flags.id) }, "Graph returned a malformed access package body");
|
|
572
|
+
}
|
|
573
|
+
export async function countAccessPackages(session, flags, profile, operation, help, profileName) {
|
|
574
|
+
return countCollection("access package", ACCESS_PACKAGE_LIST.emptyNote, session, flags, profile, operation, help, profileName);
|
|
575
|
+
}
|
|
576
|
+
export async function listAssignmentPolicies(session, flags, profile, operation, help, profileName) {
|
|
577
|
+
const accessPackage = accessPackageId(flags, help);
|
|
578
|
+
return listCollection(ASSIGNMENT_POLICY_LIST, session, flags, profile, operation, help, profileName, { "accessPackage-id": accessPackage }, `Resume losslessly with the same --access-package and flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
|
|
579
|
+
}
|
|
580
|
+
export async function showAssignmentPolicy(session, flags, profile, operation, help, profileName) {
|
|
581
|
+
const accessPackage = accessPackageId(flags, help);
|
|
582
|
+
return showOne("assignmentPolicy", "entra entitlement assignment-policy show", ASSIGNMENT_POLICY_KNOWN, KNOWN_ASSIGNMENT_POLICY_FIELDS, DEFAULT_ASSIGNMENT_POLICY_SHOW_SELECT, session, flags, profile, operation, help, profileName, { "accessPackage-id": accessPackage, "accessPackageAssignmentPolicy-id": String(flags.id) }, "Graph returned a malformed assignment policy body");
|
|
583
|
+
}
|
|
584
|
+
export async function countAssignmentPolicies(session, flags, profile, operation, help, profileName) {
|
|
585
|
+
const accessPackage = accessPackageId(flags, help);
|
|
586
|
+
const scopes = scopesFor(flags, profile, DEFAULT_ENTITLEMENT_MANAGEMENT_SCOPES, help);
|
|
587
|
+
const raw = await withGuidance(() => session.execute({
|
|
588
|
+
profile,
|
|
589
|
+
operation,
|
|
590
|
+
params: { "accessPackage-id": accessPackage },
|
|
591
|
+
scopes,
|
|
592
|
+
scalar: true,
|
|
593
|
+
}));
|
|
594
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
595
|
+
throw new AxiError("Graph returned a malformed assignment policy count body", "GRAPH_ERROR", [
|
|
596
|
+
"Counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
|
|
597
|
+
]);
|
|
598
|
+
}
|
|
599
|
+
const count = { returned: raw, complete: true };
|
|
600
|
+
return raw === 0 ? { count, help: [ASSIGNMENT_POLICY_LIST.emptyNote] } : { count };
|
|
601
|
+
}
|
|
602
|
+
export async function listResourceRoleScopes(session, flags, profile, operation, help, profileName) {
|
|
603
|
+
const accessPackage = accessPackageId(flags, help);
|
|
604
|
+
return listCollection(RESOURCE_ROLE_SCOPE_LIST, session, flags, profile, operation, help, profileName, { "accessPackage-id": accessPackage }, `Resume losslessly with the same --access-package and flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
|
|
605
|
+
}
|
|
606
|
+
export async function showResourceRoleScope(session, flags, profile, operation, help, profileName) {
|
|
607
|
+
const accessPackage = accessPackageId(flags, help);
|
|
608
|
+
return showOne("resourceRoleScope", "entra entitlement resource-role-scope show", RESOURCE_ROLE_SCOPE_KNOWN, KNOWN_RESOURCE_ROLE_SCOPE_FIELDS, DEFAULT_RESOURCE_ROLE_SCOPE_SHOW_SELECT, session, flags, profile, operation, help, profileName, { "accessPackage-id": accessPackage, "accessPackageResourceRoleScope-id": String(flags.id) }, "Graph returned a malformed resource-role scope body");
|
|
609
|
+
}
|
|
610
|
+
export async function countResourceRoleScopes(session, flags, profile, operation, help, profileName) {
|
|
611
|
+
const accessPackage = accessPackageId(flags, help);
|
|
612
|
+
const scopes = scopesFor(flags, profile, DEFAULT_ENTITLEMENT_MANAGEMENT_SCOPES, help);
|
|
613
|
+
const raw = await withGuidance(() => session.execute({
|
|
614
|
+
profile,
|
|
615
|
+
operation,
|
|
616
|
+
params: { "accessPackage-id": accessPackage },
|
|
617
|
+
scopes,
|
|
618
|
+
scalar: true,
|
|
619
|
+
}));
|
|
620
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
621
|
+
throw new AxiError("Graph returned a malformed resource-role scope count body", "GRAPH_ERROR", [
|
|
622
|
+
"Counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
|
|
623
|
+
]);
|
|
624
|
+
}
|
|
625
|
+
const count = { returned: raw, complete: true };
|
|
626
|
+
return raw === 0 ? { count, help: [RESOURCE_ROLE_SCOPE_LIST.emptyNote] } : { count };
|
|
627
|
+
}
|
|
628
|
+
export async function listAssignments(session, flags, profile, operation, help, profileName) {
|
|
629
|
+
return listCollection(ASSIGNMENT_LIST, session, flags, profile, operation, help, profileName, {}, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
|
|
630
|
+
}
|
|
631
|
+
export async function showAssignment(session, flags, profile, operation, help, profileName) {
|
|
632
|
+
return showOne("assignment", "entra entitlement assignment show", ASSIGNMENT_KNOWN, KNOWN_ASSIGNMENT_FIELDS, DEFAULT_ASSIGNMENT_SHOW_SELECT, session, flags, profile, operation, help, profileName, { "accessPackageAssignment-id": String(flags.id) }, "Graph returned a malformed assignment body", { expand: ASSIGNMENT_EXPAND, links: flattenAssignmentLinks, synthetic: ASSIGNMENT_SYNTHETIC, hints: ASSIGNMENT_DENIAL_HINTS });
|
|
633
|
+
}
|
|
634
|
+
export async function countAssignments(session, flags, profile, operation, help, profileName) {
|
|
635
|
+
return countCollection("assignment", ASSIGNMENT_LIST.emptyNote, session, flags, profile, operation, help, profileName, ASSIGNMENT_DENIAL_HINTS);
|
|
636
|
+
}
|
|
637
|
+
export async function listAssignmentRequests(session, flags, profile, operation, help, profileName) {
|
|
638
|
+
return listCollection(ASSIGNMENT_REQUEST_LIST, session, flags, profile, operation, help, profileName, {}, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
|
|
639
|
+
}
|
|
640
|
+
export async function showAssignmentRequest(session, flags, profile, operation, help, profileName) {
|
|
641
|
+
return showOne("assignmentRequest", "entra entitlement assignment-request show", ASSIGNMENT_REQUEST_KNOWN, KNOWN_ASSIGNMENT_REQUEST_FIELDS, DEFAULT_ASSIGNMENT_REQUEST_SHOW_SELECT, session, flags, profile, operation, help, profileName, { "accessPackageAssignmentRequest-id": String(flags.id) }, "Graph returned a malformed assignment request body", { expand: ASSIGNMENT_REQUEST_EXPAND, links: flattenRequestLinks, synthetic: ASSIGNMENT_REQUEST_SYNTHETIC, hints: ASSIGNMENT_DENIAL_HINTS });
|
|
642
|
+
}
|
|
643
|
+
export async function countAssignmentRequests(session, flags, profile, operation, help, profileName) {
|
|
644
|
+
return countCollection("assignment request", ASSIGNMENT_REQUEST_LIST.emptyNote, session, flags, profile, operation, help, profileName, ASSIGNMENT_DENIAL_HINTS);
|
|
645
|
+
}
|