@knowttl/mg-axi 0.1.1 → 0.1.3
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 +251 -77
- package/dist/api.js +253 -8
- package/dist/bin/mg-axi-hook.js +31 -0
- package/dist/catalogue.js +693 -2
- package/dist/cli.js +155 -17
- package/dist/docs.js +175 -46
- package/dist/doctor.js +2 -2
- package/dist/entra-access-reviews.js +11 -4
- package/dist/entra-apps.js +171 -14
- package/dist/entra-audit-logs.js +101 -16
- package/dist/entra-auth-methods.js +8 -4
- package/dist/entra-certificate-auth.js +12 -8
- package/dist/entra-conditional-access.js +425 -15
- package/dist/entra-contacts.js +19 -12
- package/dist/entra-contracts.js +7 -4
- package/dist/entra-custom-security-attributes.js +19 -12
- package/dist/entra-data-policy-operations.js +7 -4
- package/dist/entra-delegated-admin.js +55 -6
- package/dist/entra-deleted-items.js +6 -4
- package/dist/entra-directory-objects.js +6 -4
- package/dist/entra-directory.js +19 -12
- package/dist/entra-domains.js +13 -8
- package/dist/entra-entitlement-management.js +12 -4
- package/dist/entra-federation-configurations.js +7 -4
- package/dist/entra-grants.js +8 -4
- package/dist/entra-group-lifecycle.js +13 -8
- package/dist/entra-group-pim.js +431 -0
- package/dist/entra-groups.js +13 -8
- package/dist/entra-identity-providers.js +7 -4
- package/dist/entra-lifecycle-workflows.js +152 -13
- package/dist/entra-multi-tenant-organization.js +7 -4
- package/dist/entra-on-premises-synchronization.js +6 -4
- package/dist/entra-organization.js +13 -8
- package/dist/entra-risk-prevention.js +442 -0
- package/dist/entra-risk.js +224 -9
- package/dist/entra-roles.js +204 -12
- package/dist/entra-subscriptions.js +6 -4
- package/dist/entra-terms-of-use.js +13 -8
- package/dist/entra-users.js +7 -4
- package/dist/graph-session.js +30 -5
- package/dist/hook.js +84 -0
- package/dist/list-totals.js +52 -0
- package/dist/setup.js +9 -1
- package/docs/coverage.md +743 -610
- package/inventory/operations.json +2 -2
- package/inventory/schema.json +1 -1
- package/package.json +4 -3
- package/skills/mg-axi/SKILL.md +86 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-01 group lifecycle subfamily: the read mapping behind
|
|
3
4
|
// `mg-axi entra group-lifecycle-policy list/show/count` and
|
|
4
5
|
// `mg-axi entra group-setting-template list/show/count`. Operation
|
|
@@ -241,20 +242,22 @@ export async function listLifecyclePolicies(session, flags, profile, operation,
|
|
|
241
242
|
if (!result.complete) {
|
|
242
243
|
return {
|
|
243
244
|
groupLifecyclePolicies: policies,
|
|
244
|
-
|
|
245
|
+
...listTotals(policies.length, result.total, "lifecycle policies", false),
|
|
246
|
+
complete: false,
|
|
247
|
+
reason: result.reason,
|
|
245
248
|
cursor: result.cursor,
|
|
246
249
|
help: [...truncationHints, resumeHint(profileName), showHint],
|
|
247
250
|
};
|
|
248
251
|
}
|
|
249
|
-
const count = { returned: policies.length, complete: true };
|
|
250
252
|
if (!policies.length) {
|
|
251
253
|
return {
|
|
252
254
|
groupLifecyclePolicies: policies,
|
|
253
|
-
|
|
255
|
+
...listTotals(policies.length, result.total, "lifecycle policies", true),
|
|
256
|
+
complete: true,
|
|
254
257
|
help: ["0 lifecycle policies matched; the absence of results is the answer, not an error"],
|
|
255
258
|
};
|
|
256
259
|
}
|
|
257
|
-
return { groupLifecyclePolicies: policies,
|
|
260
|
+
return { groupLifecyclePolicies: policies, ...listTotals(policies.length, result.total, "lifecycle policies", true), complete: true, help: [...truncationHints, showHint] };
|
|
258
261
|
}
|
|
259
262
|
export async function showLifecyclePolicy(session, flags, profile, operation, help, profileName) {
|
|
260
263
|
const { select, fields } = selectedFields(flags, KNOWN_LIFECYCLE, KNOWN_LIFECYCLE_FIELDS, DEFAULT_LIFECYCLE_SHOW_SELECT, help);
|
|
@@ -305,20 +308,22 @@ export async function listSettingTemplates(session, flags, profile, operation, h
|
|
|
305
308
|
if (!result.complete) {
|
|
306
309
|
return {
|
|
307
310
|
groupSettingTemplates: templates,
|
|
308
|
-
|
|
311
|
+
...listTotals(templates.length, result.total, "setting templates", false),
|
|
312
|
+
complete: false,
|
|
313
|
+
reason: result.reason,
|
|
309
314
|
cursor: result.cursor,
|
|
310
315
|
help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
|
|
311
316
|
};
|
|
312
317
|
}
|
|
313
|
-
const count = { returned: templates.length, complete: true };
|
|
314
318
|
if (!templates.length) {
|
|
315
319
|
return {
|
|
316
320
|
groupSettingTemplates: templates,
|
|
317
|
-
|
|
321
|
+
...listTotals(templates.length, result.total, "setting templates", true),
|
|
322
|
+
complete: true,
|
|
318
323
|
help: [filterNote, "0 setting templates matched; the absence of results is the answer, not an error"],
|
|
319
324
|
};
|
|
320
325
|
}
|
|
321
|
-
return { groupSettingTemplates: templates,
|
|
326
|
+
return { groupSettingTemplates: templates, ...listTotals(templates.length, result.total, "setting templates", true), complete: true, help: [...truncationHints, showHint, filterNote] };
|
|
322
327
|
}
|
|
323
328
|
export async function showSettingTemplate(session, flags, profile, operation, help, profileName) {
|
|
324
329
|
const { select, fields } = selectedFields(flags, KNOWN_TEMPLATES, KNOWN_TEMPLATE_FIELDS, DEFAULT_TEMPLATE_SHOW_SELECT, help);
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
3
|
+
// EXT-02 group PIM schedule reads: the read-only schedule and instance
|
|
4
|
+
// mapping behind `mg-axi entra pim group-assignment-schedule list/show`,
|
|
5
|
+
// `mg-axi entra pim group-assignment-instance list/show`,
|
|
6
|
+
// `mg-axi entra pim group-eligibility-schedule list/show`,
|
|
7
|
+
// `mg-axi entra pim group-eligibility-instance list/show` and
|
|
8
|
+
// `mg-axi entra pim group-eligibility-request list/show`.
|
|
9
|
+
// Operation construction stays beside its command; the shared session owns
|
|
10
|
+
// URLs, credentials, paging, retries and error translation, and the SDK owns
|
|
11
|
+
// TOON rendering. This module only maps flags to session calls and projects
|
|
12
|
+
// rows for compact output.
|
|
13
|
+
//
|
|
14
|
+
// Reviewed against the v1.0 privilegedaccessgroup-list-assignmentschedules,
|
|
15
|
+
// privilegedaccessgroupassignmentschedule-get,
|
|
16
|
+
// privilegedaccessgroup-list-assignmentscheduleinstances,
|
|
17
|
+
// privilegedaccessgroupassignmentscheduleinstance-get,
|
|
18
|
+
// privilegedaccessgroup-list-eligibilityschedules,
|
|
19
|
+
// privilegedaccessgroupeligibilityschedule-get,
|
|
20
|
+
// privilegedaccessgroup-list-eligibilityscheduleinstances,
|
|
21
|
+
// privilegedaccessgroupeligibilityscheduleinstance-get,
|
|
22
|
+
// privilegedaccessgroup-list-eligibilityschedulerequests and
|
|
23
|
+
// privilegedaccessgroupeligibilityschedulerequest-get operation
|
|
24
|
+
// documentation and the privilegedAccessGroupAssignmentSchedule,
|
|
25
|
+
// privilegedAccessGroupAssignmentScheduleInstance,
|
|
26
|
+
// privilegedAccessGroupEligibilitySchedule,
|
|
27
|
+
// privilegedAccessGroupEligibilityScheduleInstance and
|
|
28
|
+
// privilegedAccessGroupEligibilityScheduleRequest resource contracts on
|
|
29
|
+
// 2026-10-06, with a second check against the Graph permissions reference
|
|
30
|
+
// and the ID Governance licensing fundamentals. Assignment reads take D/A
|
|
31
|
+
// PrivilegedAssignmentSchedule.Read.AzureADGroup; eligibility reads take
|
|
32
|
+
// D/A PrivilegedEligibilitySchedule.Read.AzureADGroup. Delegated callers
|
|
33
|
+
// additionally need owner/member of the group or a supported directory
|
|
34
|
+
// role (Global Reader or Privileged Role Administrator for role-assignable
|
|
35
|
+
// groups; Global Reader, Directory Writer, Groups Administrator, Identity
|
|
36
|
+
// Governance Administrator or User Administrator otherwise), scoped at
|
|
37
|
+
// directory level; delegated personal Microsoft accounts are not
|
|
38
|
+
// supported. PIM for Groups needs P2 or ID Governance.
|
|
39
|
+
//
|
|
40
|
+
// Every list requires $filter (eq) scoped to a groupId or a principalId,
|
|
41
|
+
// so --filter is required and must name one of them; the requirement is
|
|
42
|
+
// validated before credentials. Lists document $select/$filter/$expand, so
|
|
43
|
+
// --filter passes through as plain $filter with no $count or
|
|
44
|
+
// ConsistencyLevel attached; singles document $select with $expand.
|
|
45
|
+
// These collections document no $count contract, so totals stay honest as
|
|
46
|
+
// count { returned, complete } like the extended role PIM reads rather
|
|
47
|
+
// than the shared list-totals helper, which applies where $count=true is
|
|
48
|
+
// sent. Deferred with reason, never built on a write scope:
|
|
49
|
+
// assignmentScheduleRequests list/get take
|
|
50
|
+
// PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup as least privilege.
|
|
51
|
+
// Out of scope: approvals, filterByCurrentUser functions, nested
|
|
52
|
+
// group/principal/activatedUsing/targetSchedule navigation (scalar linkage
|
|
53
|
+
// ids such as assignmentScheduleId stay in), resources, every
|
|
54
|
+
// count/ref/cast tail, all mutations and beta.
|
|
55
|
+
// Schedules carry the full lifecycle (scheduleInfo, status, audit stamps);
|
|
56
|
+
// instances carry one provisioned window plus its parent schedule linkage;
|
|
57
|
+
// requests carry the ask (action, justification, scheduleInfo, ticketInfo)
|
|
58
|
+
// plus its outcome (status, approvalId, targetScheduleId).
|
|
59
|
+
export const KNOWN_ASSIGNMENT_SCHEDULE_FIELDS = [
|
|
60
|
+
"id",
|
|
61
|
+
"accessId",
|
|
62
|
+
"assignmentType",
|
|
63
|
+
"memberType",
|
|
64
|
+
"principalId",
|
|
65
|
+
"groupId",
|
|
66
|
+
"status",
|
|
67
|
+
"scheduleInfo",
|
|
68
|
+
"createdDateTime",
|
|
69
|
+
"modifiedDateTime",
|
|
70
|
+
"createdUsing",
|
|
71
|
+
];
|
|
72
|
+
export const KNOWN_ASSIGNMENT_INSTANCE_FIELDS = [
|
|
73
|
+
"id",
|
|
74
|
+
"accessId",
|
|
75
|
+
"assignmentType",
|
|
76
|
+
"memberType",
|
|
77
|
+
"principalId",
|
|
78
|
+
"groupId",
|
|
79
|
+
"assignmentScheduleId",
|
|
80
|
+
"startDateTime",
|
|
81
|
+
"endDateTime",
|
|
82
|
+
];
|
|
83
|
+
export const KNOWN_ELIGIBILITY_SCHEDULE_FIELDS = [
|
|
84
|
+
"id",
|
|
85
|
+
"accessId",
|
|
86
|
+
"memberType",
|
|
87
|
+
"principalId",
|
|
88
|
+
"groupId",
|
|
89
|
+
"status",
|
|
90
|
+
"scheduleInfo",
|
|
91
|
+
"createdDateTime",
|
|
92
|
+
"modifiedDateTime",
|
|
93
|
+
"createdUsing",
|
|
94
|
+
];
|
|
95
|
+
export const KNOWN_ELIGIBILITY_INSTANCE_FIELDS = [
|
|
96
|
+
"id",
|
|
97
|
+
"accessId",
|
|
98
|
+
"memberType",
|
|
99
|
+
"principalId",
|
|
100
|
+
"groupId",
|
|
101
|
+
"eligibilityScheduleId",
|
|
102
|
+
"startDateTime",
|
|
103
|
+
"endDateTime",
|
|
104
|
+
];
|
|
105
|
+
// Requests narrow to the ask plus its outcome: createdBy is an identitySet
|
|
106
|
+
// for a later slice and customData is documented as unused, so neither is
|
|
107
|
+
// selectable here. Justification is free text and rides only behind an
|
|
108
|
+
// explicit --select, never in a default.
|
|
109
|
+
export const KNOWN_ELIGIBILITY_REQUEST_FIELDS = [
|
|
110
|
+
"id",
|
|
111
|
+
"accessId",
|
|
112
|
+
"action",
|
|
113
|
+
"status",
|
|
114
|
+
"principalId",
|
|
115
|
+
"groupId",
|
|
116
|
+
"justification",
|
|
117
|
+
"scheduleInfo",
|
|
118
|
+
"ticketInfo",
|
|
119
|
+
"createdDateTime",
|
|
120
|
+
"completedDateTime",
|
|
121
|
+
"approvalId",
|
|
122
|
+
"targetScheduleId",
|
|
123
|
+
"isValidationOnly",
|
|
124
|
+
];
|
|
125
|
+
const ASSIGNMENT_SCHEDULE_KNOWN = new Set(KNOWN_ASSIGNMENT_SCHEDULE_FIELDS);
|
|
126
|
+
const ASSIGNMENT_INSTANCE_KNOWN = new Set(KNOWN_ASSIGNMENT_INSTANCE_FIELDS);
|
|
127
|
+
const ELIGIBILITY_SCHEDULE_KNOWN = new Set(KNOWN_ELIGIBILITY_SCHEDULE_FIELDS);
|
|
128
|
+
const ELIGIBILITY_INSTANCE_KNOWN = new Set(KNOWN_ELIGIBILITY_INSTANCE_FIELDS);
|
|
129
|
+
const ELIGIBILITY_REQUEST_KNOWN = new Set(KNOWN_ELIGIBILITY_REQUEST_FIELDS);
|
|
130
|
+
// Compact rows: identifiers plus the correlation keys. groupId and
|
|
131
|
+
// principalId are the required scoping keys; accessId names member versus
|
|
132
|
+
// owner; assignmentType names Assigned versus Activated; memberType names
|
|
133
|
+
// how the row reaches the principal (for example Direct).
|
|
134
|
+
const DEFAULT_ASSIGNMENT_SCHEDULE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "assignmentType"];
|
|
135
|
+
const DEFAULT_ASSIGNMENT_INSTANCE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "assignmentType"];
|
|
136
|
+
const DEFAULT_ELIGIBILITY_SCHEDULE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "memberType"];
|
|
137
|
+
const DEFAULT_ELIGIBILITY_INSTANCE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "memberType"];
|
|
138
|
+
const DEFAULT_ELIGIBILITY_REQUEST_LIST_SELECT = ["id", "action", "status", "principalId", "groupId", "accessId"];
|
|
139
|
+
const DEFAULT_ELIGIBILITY_REQUEST_SHOW_SELECT = KNOWN_ELIGIBILITY_REQUEST_FIELDS.filter(field => field !== "justification");
|
|
140
|
+
export const DEFAULT_ASSIGNMENT_SCOPES = ["https://graph.microsoft.com/PrivilegedAssignmentSchedule.Read.AzureADGroup"];
|
|
141
|
+
export const DEFAULT_ELIGIBILITY_SCOPES = ["https://graph.microsoft.com/PrivilegedEligibilitySchedule.Read.AzureADGroup"];
|
|
142
|
+
const TRUNCATE_AT = 500;
|
|
143
|
+
function fieldList(raw, flag, known, knownList, help) {
|
|
144
|
+
const parts = String(raw)
|
|
145
|
+
.split(",")
|
|
146
|
+
.map(part => part.trim())
|
|
147
|
+
.filter(part => part.length > 0);
|
|
148
|
+
if (!parts.length)
|
|
149
|
+
throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
|
|
150
|
+
const fields = [];
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
if (!known.has(part)) {
|
|
153
|
+
throw new AxiError(`Unknown property ${part} in --${flag}`, "VALIDATION_ERROR", [
|
|
154
|
+
help,
|
|
155
|
+
`Known properties: ${knownList.join(", ")}`,
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
if (!fields.includes(part))
|
|
159
|
+
fields.push(part);
|
|
160
|
+
}
|
|
161
|
+
return fields;
|
|
162
|
+
}
|
|
163
|
+
function scopesFor(flags, profile, defaults, help) {
|
|
164
|
+
if (profile.mode === "application") {
|
|
165
|
+
if (flags.scopes !== undefined) {
|
|
166
|
+
throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
if (flags.scopes === undefined)
|
|
171
|
+
return [...defaults];
|
|
172
|
+
const scopes = String(flags.scopes)
|
|
173
|
+
.split(",")
|
|
174
|
+
.map(scope => scope.trim())
|
|
175
|
+
.filter(scope => scope.length > 0);
|
|
176
|
+
return scopes;
|
|
177
|
+
}
|
|
178
|
+
// Request justification is free text and scheduleInfo/ticketInfo are nested
|
|
179
|
+
// objects, so truncation walks values recursively; --full returns them whole.
|
|
180
|
+
function truncateValue(value, full) {
|
|
181
|
+
if (full)
|
|
182
|
+
return { value, truncated: false };
|
|
183
|
+
if (typeof value === "string" && value.length > TRUNCATE_AT) {
|
|
184
|
+
return { value: `${value.slice(0, TRUNCATE_AT)}... (truncated, ${value.length} chars total)`, truncated: true };
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
const results = value.map(item => truncateValue(item, full));
|
|
188
|
+
return { value: results.map(result => result.value), truncated: results.some(result => result.truncated) };
|
|
189
|
+
}
|
|
190
|
+
if (value !== null && typeof value === "object") {
|
|
191
|
+
let truncated = false;
|
|
192
|
+
const entries = Object.entries(value).map(([key, item]) => {
|
|
193
|
+
const result = truncateValue(item, full);
|
|
194
|
+
truncated = truncated || result.truncated;
|
|
195
|
+
return [key, result.value];
|
|
196
|
+
});
|
|
197
|
+
return { value: Object.fromEntries(entries), truncated };
|
|
198
|
+
}
|
|
199
|
+
return { value, truncated: false };
|
|
200
|
+
}
|
|
201
|
+
// Local projection preserves Graph's null/missing distinction: an explicit
|
|
202
|
+
// null stays null, an absent property stays absent and is never synthesized.
|
|
203
|
+
function project(row, fields, full) {
|
|
204
|
+
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
205
|
+
const projected = {};
|
|
206
|
+
let truncated = false;
|
|
207
|
+
for (const field of fields) {
|
|
208
|
+
if (!Object.hasOwn(source, field))
|
|
209
|
+
continue;
|
|
210
|
+
const result = truncateValue(source[field], full);
|
|
211
|
+
projected[field] = result.value;
|
|
212
|
+
truncated = truncated || result.truncated;
|
|
213
|
+
}
|
|
214
|
+
return { row: projected, truncated };
|
|
215
|
+
}
|
|
216
|
+
function selectedFields(flags, defaults, known, knownList, help) {
|
|
217
|
+
const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, "select", known, knownList, help);
|
|
218
|
+
const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, "fields", known, knownList, help);
|
|
219
|
+
const missing = fields.find(field => !select.includes(field));
|
|
220
|
+
if (missing) {
|
|
221
|
+
throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
|
|
222
|
+
}
|
|
223
|
+
return { select, fields };
|
|
224
|
+
}
|
|
225
|
+
function profileHint(profileName) {
|
|
226
|
+
return `--profile ${shellValue(profileName)}`;
|
|
227
|
+
}
|
|
228
|
+
function shellValue(value) {
|
|
229
|
+
return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
230
|
+
}
|
|
231
|
+
function fullHint(command, flags, profileName) {
|
|
232
|
+
const args = Object.entries({ ...flags, profile: profileName, full: true })
|
|
233
|
+
.map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
|
|
234
|
+
return `mg-axi ${command} ${args.join(" ")}`;
|
|
235
|
+
}
|
|
236
|
+
// Denials carry the session's generic grant/role/licence cause; each
|
|
237
|
+
// family adds the scope, roles and licensing that actually unlock it,
|
|
238
|
+
// because a 403 alone never says which prerequisite is missing.
|
|
239
|
+
function withGuidance(hints, run) {
|
|
240
|
+
return run().catch(error => {
|
|
241
|
+
if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
|
|
242
|
+
throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const ASSIGNMENT_DENIAL_HINTS = [
|
|
248
|
+
"Group PIM assignment reads need PrivilegedAssignmentSchedule.Read.AzureADGroup plus owner/member of the group or a supported directory role (Global Reader or Privileged Role Administrator for role-assignable groups; Global Reader, Directory Writer, Groups Administrator, Identity Governance Administrator or User Administrator otherwise, scoped at directory level) for delegated access, or admin-consented PrivilegedAssignmentSchedule.Read.AzureADGroup for application access; delegated personal Microsoft accounts are not supported",
|
|
249
|
+
"PIM for Groups needs P2 or ID Governance, not only P2",
|
|
250
|
+
];
|
|
251
|
+
const ELIGIBILITY_DENIAL_HINTS = [
|
|
252
|
+
"Group PIM eligibility reads need PrivilegedEligibilitySchedule.Read.AzureADGroup plus owner/member of the group or a supported directory role (Global Reader or Privileged Role Administrator for role-assignable groups; Global Reader, Directory Writer, Groups Administrator, Identity Governance Administrator or User Administrator otherwise, scoped at directory level) for delegated access, or admin-consented PrivilegedEligibilitySchedule.Read.AzureADGroup for application access; delegated personal Microsoft accounts are not supported",
|
|
253
|
+
"PIM for Groups needs P2 or ID Governance, not only P2",
|
|
254
|
+
];
|
|
255
|
+
const ASSIGNMENT_SCHEDULE_LIST = {
|
|
256
|
+
command: "entra pim group-assignment-schedule list",
|
|
257
|
+
key: "assignmentSchedules",
|
|
258
|
+
noun: "assignment schedules",
|
|
259
|
+
known: ASSIGNMENT_SCHEDULE_KNOWN,
|
|
260
|
+
knownList: KNOWN_ASSIGNMENT_SCHEDULE_FIELDS,
|
|
261
|
+
defaultSelect: DEFAULT_ASSIGNMENT_SCHEDULE_LIST_SELECT,
|
|
262
|
+
defaultScopes: DEFAULT_ASSIGNMENT_SCOPES,
|
|
263
|
+
denialHints: ASSIGNMENT_DENIAL_HINTS,
|
|
264
|
+
scopeNote: "Assignment schedules govern group membership or ownership over time; instances are the provisioned windows, never the schedule itself",
|
|
265
|
+
emptyNote: "0 assignment schedules matched; the absence of results is the answer, not an error",
|
|
266
|
+
};
|
|
267
|
+
const ASSIGNMENT_INSTANCE_LIST = {
|
|
268
|
+
command: "entra pim group-assignment-instance list",
|
|
269
|
+
key: "assignmentScheduleInstances",
|
|
270
|
+
noun: "assignment instances",
|
|
271
|
+
known: ASSIGNMENT_INSTANCE_KNOWN,
|
|
272
|
+
knownList: KNOWN_ASSIGNMENT_INSTANCE_FIELDS,
|
|
273
|
+
defaultSelect: DEFAULT_ASSIGNMENT_INSTANCE_LIST_SELECT,
|
|
274
|
+
defaultScopes: DEFAULT_ASSIGNMENT_SCOPES,
|
|
275
|
+
denialHints: ASSIGNMENT_DENIAL_HINTS,
|
|
276
|
+
scopeNote: "Assignment instances are provisioned membership or ownership windows; assignmentType Assigned names direct grants and Activated names activated eligible grants",
|
|
277
|
+
emptyNote: "0 assignment instances matched; the absence of results is the answer, not an error",
|
|
278
|
+
};
|
|
279
|
+
const ELIGIBILITY_SCHEDULE_LIST = {
|
|
280
|
+
command: "entra pim group-eligibility-schedule list",
|
|
281
|
+
key: "eligibilitySchedules",
|
|
282
|
+
noun: "eligibility schedules",
|
|
283
|
+
known: ELIGIBILITY_SCHEDULE_KNOWN,
|
|
284
|
+
knownList: KNOWN_ELIGIBILITY_SCHEDULE_FIELDS,
|
|
285
|
+
defaultSelect: DEFAULT_ELIGIBILITY_SCHEDULE_LIST_SELECT,
|
|
286
|
+
defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
|
|
287
|
+
denialHints: ELIGIBILITY_DENIAL_HINTS,
|
|
288
|
+
scopeNote: "Eligibility schedules govern who may activate group membership or ownership; they are not active grants",
|
|
289
|
+
emptyNote: "0 eligibility schedules matched; the absence of results is the answer, not an error",
|
|
290
|
+
};
|
|
291
|
+
const ELIGIBILITY_INSTANCE_LIST = {
|
|
292
|
+
command: "entra pim group-eligibility-instance list",
|
|
293
|
+
key: "eligibilityScheduleInstances",
|
|
294
|
+
noun: "eligibility instances",
|
|
295
|
+
known: ELIGIBILITY_INSTANCE_KNOWN,
|
|
296
|
+
knownList: KNOWN_ELIGIBILITY_INSTANCE_FIELDS,
|
|
297
|
+
defaultSelect: DEFAULT_ELIGIBILITY_INSTANCE_LIST_SELECT,
|
|
298
|
+
defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
|
|
299
|
+
denialHints: ELIGIBILITY_DENIAL_HINTS,
|
|
300
|
+
scopeNote: "Eligibility instances are provisioned eligibility windows; activation itself is a PIM workflow outside these reads",
|
|
301
|
+
emptyNote: "0 eligibility instances matched; the absence of results is the answer, not an error",
|
|
302
|
+
};
|
|
303
|
+
const ELIGIBILITY_REQUEST_LIST = {
|
|
304
|
+
command: "entra pim group-eligibility-request list",
|
|
305
|
+
key: "eligibilityScheduleRequests",
|
|
306
|
+
noun: "eligibility requests",
|
|
307
|
+
known: ELIGIBILITY_REQUEST_KNOWN,
|
|
308
|
+
knownList: KNOWN_ELIGIBILITY_REQUEST_FIELDS,
|
|
309
|
+
defaultSelect: DEFAULT_ELIGIBILITY_REQUEST_LIST_SELECT,
|
|
310
|
+
defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
|
|
311
|
+
denialHints: ELIGIBILITY_DENIAL_HINTS,
|
|
312
|
+
scopeNote: "Eligibility requests carry the ask and its outcome; justification text rides only behind an explicit --select",
|
|
313
|
+
emptyNote: "0 eligibility requests matched; the absence of results is the answer, not an error",
|
|
314
|
+
};
|
|
315
|
+
// Graph requires every one of these lists to be scoped to a groupId or a
|
|
316
|
+
// principalId, so --filter must name one of them; validated before any
|
|
317
|
+
// credential or transport is touched, with cursor resumes replaying the
|
|
318
|
+
// stored filter.
|
|
319
|
+
function requiredFilter(flags, savedFilter, help) {
|
|
320
|
+
const filter = flags.filter === undefined ? savedFilter : String(flags.filter);
|
|
321
|
+
if (filter === undefined || (!filter.includes("groupId") && !filter.includes("principalId"))) {
|
|
322
|
+
throw new AxiError("--filter needs a groupId or principalId eq clause scoping the list", "VALIDATION_ERROR", [
|
|
323
|
+
help,
|
|
324
|
+
"For example --filter \"groupId eq '<group-id>'\" or --filter \"principalId eq '<principal-id>'\"",
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
return filter;
|
|
328
|
+
}
|
|
329
|
+
async function listCollection(shape, session, flags, profile, operation, help, profileName) {
|
|
330
|
+
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
331
|
+
if (cursor !== undefined && !cursor.trim())
|
|
332
|
+
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
333
|
+
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
334
|
+
const savedSelect = saved?.$select;
|
|
335
|
+
const { select, fields } = selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help);
|
|
336
|
+
const filter = requiredFilter(flags, saved?.$filter, help);
|
|
337
|
+
const scopes = scopesFor(flags, profile, shape.defaultScopes, help);
|
|
338
|
+
const full = flags.full === true;
|
|
339
|
+
// These lists document $select/$filter/$expand, so --filter passes
|
|
340
|
+
// through as plain $filter with no $count or ConsistencyLevel attached;
|
|
341
|
+
// $expand stays unreviewed and is never sent.
|
|
342
|
+
const query = { $select: select.join(","), $filter: filter };
|
|
343
|
+
const args = { profile, operation, query, scopes };
|
|
344
|
+
if (cursor !== undefined)
|
|
345
|
+
args.cursor = cursor;
|
|
346
|
+
if (flags.all === true) {
|
|
347
|
+
if (flags.limit !== undefined)
|
|
348
|
+
throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
|
|
352
|
+
}
|
|
353
|
+
const result = await withGuidance(shape.denialHints, () => session.collect(args));
|
|
354
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? shape.defaultSelect.join(",") };
|
|
355
|
+
if (result.query.$filter !== undefined)
|
|
356
|
+
effectiveFlags.filter = result.query.$filter;
|
|
357
|
+
const rows = [];
|
|
358
|
+
let truncated = false;
|
|
359
|
+
for (const row of result.value) {
|
|
360
|
+
const projected = project(row, fields, full);
|
|
361
|
+
rows.push(projected.row);
|
|
362
|
+
truncated = truncated || projected.truncated;
|
|
363
|
+
}
|
|
364
|
+
const truncationHints = truncated ? [fullHint(shape.command, effectiveFlags, profileName)] : [];
|
|
365
|
+
const standing = [shape.scopeNote];
|
|
366
|
+
if (!result.complete) {
|
|
367
|
+
return {
|
|
368
|
+
[shape.key]: rows,
|
|
369
|
+
...listTotals(rows.length, result.total, shape.noun, false),
|
|
370
|
+
complete: false,
|
|
371
|
+
reason: result.reason,
|
|
372
|
+
cursor: result.cursor,
|
|
373
|
+
help: [...truncationHints, `Resume losslessly with the same --filter and flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, ...standing],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (!rows.length) {
|
|
377
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
|
|
378
|
+
}
|
|
379
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
|
|
380
|
+
}
|
|
381
|
+
async function showOne(key, command, known, knownList, defaultSelect, defaultScopes, denialHints, session, flags, profile, operation, help, profileName, params) {
|
|
382
|
+
const { select, fields } = selectedFields(flags, defaultSelect, known, knownList, help);
|
|
383
|
+
const scopes = scopesFor(flags, profile, defaultScopes, help);
|
|
384
|
+
const full = flags.full === true;
|
|
385
|
+
const raw = await withGuidance(denialHints, () => session.execute({
|
|
386
|
+
profile,
|
|
387
|
+
operation,
|
|
388
|
+
params,
|
|
389
|
+
query: { $select: select.join(",") },
|
|
390
|
+
scopes,
|
|
391
|
+
}));
|
|
392
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
393
|
+
throw new AxiError(`Graph returned a malformed ${key} body`, "GRAPH_ERROR", [
|
|
394
|
+
"Single-object reads carry one object; treat anything else as unknown, not empty",
|
|
395
|
+
]);
|
|
396
|
+
}
|
|
397
|
+
const { row, truncated } = project(raw, fields, full);
|
|
398
|
+
if (truncated)
|
|
399
|
+
return { [key]: row, help: [fullHint(command, flags, profileName)] };
|
|
400
|
+
return { [key]: row };
|
|
401
|
+
}
|
|
402
|
+
export async function listGroupAssignmentSchedules(session, flags, profile, operation, help, profileName) {
|
|
403
|
+
return listCollection(ASSIGNMENT_SCHEDULE_LIST, session, flags, profile, operation, help, profileName);
|
|
404
|
+
}
|
|
405
|
+
export async function showGroupAssignmentSchedule(session, flags, profile, operation, help, profileName) {
|
|
406
|
+
return showOne("assignmentSchedule", "entra pim group-assignment-schedule show", ASSIGNMENT_SCHEDULE_KNOWN, KNOWN_ASSIGNMENT_SCHEDULE_FIELDS, [...KNOWN_ASSIGNMENT_SCHEDULE_FIELDS], DEFAULT_ASSIGNMENT_SCOPES, ASSIGNMENT_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupAssignmentSchedule-id": String(flags.id) });
|
|
407
|
+
}
|
|
408
|
+
export async function listGroupAssignmentInstances(session, flags, profile, operation, help, profileName) {
|
|
409
|
+
return listCollection(ASSIGNMENT_INSTANCE_LIST, session, flags, profile, operation, help, profileName);
|
|
410
|
+
}
|
|
411
|
+
export async function showGroupAssignmentInstance(session, flags, profile, operation, help, profileName) {
|
|
412
|
+
return showOne("assignmentScheduleInstance", "entra pim group-assignment-instance show", ASSIGNMENT_INSTANCE_KNOWN, KNOWN_ASSIGNMENT_INSTANCE_FIELDS, [...KNOWN_ASSIGNMENT_INSTANCE_FIELDS], DEFAULT_ASSIGNMENT_SCOPES, ASSIGNMENT_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupAssignmentScheduleInstance-id": String(flags.id) });
|
|
413
|
+
}
|
|
414
|
+
export async function listGroupEligibilitySchedules(session, flags, profile, operation, help, profileName) {
|
|
415
|
+
return listCollection(ELIGIBILITY_SCHEDULE_LIST, session, flags, profile, operation, help, profileName);
|
|
416
|
+
}
|
|
417
|
+
export async function showGroupEligibilitySchedule(session, flags, profile, operation, help, profileName) {
|
|
418
|
+
return showOne("eligibilitySchedule", "entra pim group-eligibility-schedule show", ELIGIBILITY_SCHEDULE_KNOWN, KNOWN_ELIGIBILITY_SCHEDULE_FIELDS, [...KNOWN_ELIGIBILITY_SCHEDULE_FIELDS], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilitySchedule-id": String(flags.id) });
|
|
419
|
+
}
|
|
420
|
+
export async function listGroupEligibilityInstances(session, flags, profile, operation, help, profileName) {
|
|
421
|
+
return listCollection(ELIGIBILITY_INSTANCE_LIST, session, flags, profile, operation, help, profileName);
|
|
422
|
+
}
|
|
423
|
+
export async function showGroupEligibilityInstance(session, flags, profile, operation, help, profileName) {
|
|
424
|
+
return showOne("eligibilityScheduleInstance", "entra pim group-eligibility-instance show", ELIGIBILITY_INSTANCE_KNOWN, KNOWN_ELIGIBILITY_INSTANCE_FIELDS, [...KNOWN_ELIGIBILITY_INSTANCE_FIELDS], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilityScheduleInstance-id": String(flags.id) });
|
|
425
|
+
}
|
|
426
|
+
export async function listGroupEligibilityRequests(session, flags, profile, operation, help, profileName) {
|
|
427
|
+
return listCollection(ELIGIBILITY_REQUEST_LIST, session, flags, profile, operation, help, profileName);
|
|
428
|
+
}
|
|
429
|
+
export async function showGroupEligibilityRequest(session, flags, profile, operation, help, profileName) {
|
|
430
|
+
return showOne("eligibilityScheduleRequest", "entra pim group-eligibility-request show", ELIGIBILITY_REQUEST_KNOWN, KNOWN_ELIGIBILITY_REQUEST_FIELDS, [...DEFAULT_ELIGIBILITY_REQUEST_SHOW_SELECT], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilityScheduleRequest-id": String(flags.id) });
|
|
431
|
+
}
|
package/dist/entra-groups.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// READ-02: the Entra group read mapping behind `mg-axi entra group list/show`
|
|
3
4
|
// and `mg-axi entra group member list` / `member-of list` (each with an
|
|
4
5
|
// opt-in `--transitive` flat view). Operation construction stays beside its
|
|
@@ -226,23 +227,25 @@ export async function listGroups(session, flags, profile, operation, help, profi
|
|
|
226
227
|
if (!result.complete) {
|
|
227
228
|
return {
|
|
228
229
|
groups,
|
|
229
|
-
|
|
230
|
+
...listTotals(groups.length, result.total, "groups", false),
|
|
231
|
+
complete: false,
|
|
232
|
+
reason: result.reason,
|
|
230
233
|
cursor: result.cursor,
|
|
231
234
|
help: [...truncationHints, resumeHint(profileName), showHint],
|
|
232
235
|
};
|
|
233
236
|
}
|
|
234
|
-
const count = { returned: groups.length, complete: true };
|
|
235
237
|
if (!groups.length) {
|
|
236
238
|
return {
|
|
237
239
|
groups,
|
|
238
|
-
|
|
240
|
+
...listTotals(groups.length, result.total, "groups", true),
|
|
241
|
+
complete: true,
|
|
239
242
|
help: [
|
|
240
243
|
`mg-axi entra group list --filter <odata-filter> ${profileHint(profileName)}`,
|
|
241
244
|
"0 groups matched; the absence of results is the answer, not an error",
|
|
242
245
|
],
|
|
243
246
|
};
|
|
244
247
|
}
|
|
245
|
-
return { groups,
|
|
248
|
+
return { groups, ...listTotals(groups.length, result.total, "groups", true), complete: true, help: [...truncationHints, showHint] };
|
|
246
249
|
}
|
|
247
250
|
export async function showGroup(session, flags, profile, operation, help, profileName) {
|
|
248
251
|
const { select, fields } = selectedFields(flags, KNOWN_GROUPS, KNOWN_GROUP_FIELDS, DEFAULT_SHOW_SELECT, help);
|
|
@@ -302,17 +305,19 @@ async function listRelationship(session, flags, profile, operation, help, profil
|
|
|
302
305
|
if (!result.complete) {
|
|
303
306
|
return {
|
|
304
307
|
[key]: rows,
|
|
305
|
-
|
|
308
|
+
...listTotals(rows.length, result.total, emptyNoun, false),
|
|
309
|
+
complete: false,
|
|
310
|
+
reason: result.reason,
|
|
306
311
|
cursor: result.cursor,
|
|
307
312
|
...warnings,
|
|
308
313
|
help: [...truncationHints, ...limitedHints, hiddenHint, resumeHint(profileName), scopeHint],
|
|
309
314
|
};
|
|
310
315
|
}
|
|
311
|
-
const count = { returned: rows.length, complete: true };
|
|
312
316
|
if (!rows.length) {
|
|
313
317
|
return {
|
|
314
318
|
[key]: rows,
|
|
315
|
-
|
|
319
|
+
...listTotals(rows.length, result.total, emptyNoun, true),
|
|
320
|
+
complete: true,
|
|
316
321
|
...warnings,
|
|
317
322
|
help: [
|
|
318
323
|
...limitedHints,
|
|
@@ -322,7 +327,7 @@ async function listRelationship(session, flags, profile, operation, help, profil
|
|
|
322
327
|
],
|
|
323
328
|
};
|
|
324
329
|
}
|
|
325
|
-
return { [key]: rows,
|
|
330
|
+
return { [key]: rows, ...listTotals(rows.length, result.total, emptyNoun, true), complete: true, ...warnings, help: [...truncationHints, ...limitedHints, hiddenHint, scopeHint] };
|
|
326
331
|
}
|
|
327
332
|
export async function listGroupMembers(session, flags, profile, operation, help, profileName) {
|
|
328
333
|
const transitive = flags.transitive === true;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-03 workforce identity-providers subfamily: the read mapping behind
|
|
3
4
|
// `mg-axi entra identity-provider list/show/count/available-types`. Operation construction
|
|
4
5
|
// stays beside its command; the shared session owns URLs, credentials,
|
|
@@ -217,20 +218,22 @@ export async function listIdentityProviders(session, flags, profile, operation,
|
|
|
217
218
|
if (!result.complete) {
|
|
218
219
|
return {
|
|
219
220
|
identityProviders: providers,
|
|
220
|
-
|
|
221
|
+
...listTotals(providers.length, result.total, "identity providers", false),
|
|
222
|
+
complete: false,
|
|
223
|
+
reason: result.reason,
|
|
221
224
|
cursor: result.cursor,
|
|
222
225
|
help: [...truncationHints, `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, showHint, WORKFORCE_NOTE],
|
|
223
226
|
};
|
|
224
227
|
}
|
|
225
|
-
const count = { returned: providers.length, complete: true };
|
|
226
228
|
if (!providers.length) {
|
|
227
229
|
return {
|
|
228
230
|
identityProviders: providers,
|
|
229
|
-
|
|
231
|
+
...listTotals(providers.length, result.total, "identity providers", true),
|
|
232
|
+
complete: true,
|
|
230
233
|
help: [WORKFORCE_NOTE, "0 identity providers matched; the absence of results is the answer, not an error"],
|
|
231
234
|
};
|
|
232
235
|
}
|
|
233
|
-
return { identityProviders: providers,
|
|
236
|
+
return { identityProviders: providers, ...listTotals(providers.length, result.total, "identity providers", true), complete: true, help: [...truncationHints, showHint, WORKFORCE_NOTE] };
|
|
234
237
|
}
|
|
235
238
|
export async function showIdentityProvider(session, flags, profile, operation, help, profileName) {
|
|
236
239
|
const { select, fields } = selectedFields(flags, DEFAULT_SHOW_SELECT, help);
|