@knowttl/mg-axi 0.1.2 → 0.1.4
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 +127 -45
- package/dist/api.js +119 -0
- package/dist/app-auth.js +5 -1
- package/dist/catalogue.js +311 -3
- package/dist/cli.js +44 -12
- package/dist/docs.js +139 -47
- package/dist/entra-apps.js +158 -6
- package/dist/entra-audit-logs.js +93 -12
- package/dist/entra-conditional-access.js +416 -11
- 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-identity-providers.js +7 -4
- package/dist/entra-lifecycle-workflows.js +16 -4
- package/dist/entra-multi-tenant-organization.js +7 -4
- package/dist/entra-on-premises-synchronization.js +6 -4
- package/dist/entra-risk-prevention.js +10 -4
- package/dist/entra-risk.js +11 -4
- package/dist/entra-roles.js +194 -8
- package/dist/entra-terms-of-use.js +13 -8
- package/dist/graph-session.js +1 -0
- package/dist/msal-app-provider.js +96 -13
- package/dist/profiles.js +55 -9
- package/dist/setup.js +5 -2
- package/docs/coverage.md +721 -654
- package/inventory/operations.json +2 -2
- package/inventory/schema.json +1 -1
- package/package.json +1 -1
- package/skills/mg-axi/SKILL.md +34 -1
|
@@ -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);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-02 lifecycle-workflows reads: the read-only workflow, workflow
|
|
3
4
|
// template, task definition and tenant-settings first part, the
|
|
4
5
|
// second-part top-level run and user/subject processing-result reads, the
|
|
@@ -403,6 +404,7 @@ function runId(flags, help) {
|
|
|
403
404
|
const WORKFLOW_LIST = {
|
|
404
405
|
command: "entra lifecycle workflow list",
|
|
405
406
|
key: "workflows",
|
|
407
|
+
totalsNoun: "workflows",
|
|
406
408
|
known: WORKFLOW_KNOWN,
|
|
407
409
|
knownList: KNOWN_WORKFLOW_FIELDS,
|
|
408
410
|
defaultSelect: DEFAULT_WORKFLOW_LIST_SELECT,
|
|
@@ -417,6 +419,7 @@ const WORKFLOW_LIST = {
|
|
|
417
419
|
const WORKFLOW_TEMPLATE_LIST = {
|
|
418
420
|
command: "entra lifecycle workflow-template list",
|
|
419
421
|
key: "workflowTemplates",
|
|
422
|
+
totalsNoun: "workflow templates",
|
|
420
423
|
known: WORKFLOW_TEMPLATE_KNOWN,
|
|
421
424
|
knownList: KNOWN_WORKFLOW_TEMPLATE_FIELDS,
|
|
422
425
|
defaultSelect: DEFAULT_WORKFLOW_TEMPLATE_LIST_SELECT,
|
|
@@ -431,6 +434,7 @@ const WORKFLOW_TEMPLATE_LIST = {
|
|
|
431
434
|
const TASK_DEFINITION_LIST = {
|
|
432
435
|
command: "entra lifecycle task-definition list",
|
|
433
436
|
key: "taskDefinitions",
|
|
437
|
+
totalsNoun: "task definitions",
|
|
434
438
|
known: TASK_DEFINITION_KNOWN,
|
|
435
439
|
knownList: KNOWN_TASK_DEFINITION_FIELDS,
|
|
436
440
|
defaultSelect: DEFAULT_TASK_DEFINITION_LIST_SELECT,
|
|
@@ -445,6 +449,7 @@ const TASK_DEFINITION_LIST = {
|
|
|
445
449
|
const RUN_LIST = {
|
|
446
450
|
command: "entra lifecycle run list",
|
|
447
451
|
key: "runs",
|
|
452
|
+
totalsNoun: "runs",
|
|
448
453
|
known: RUN_KNOWN,
|
|
449
454
|
knownList: KNOWN_RUN_FIELDS,
|
|
450
455
|
defaultSelect: DEFAULT_RUN_LIST_SELECT,
|
|
@@ -459,6 +464,7 @@ const RUN_LIST = {
|
|
|
459
464
|
const USER_PROCESSING_LIST = {
|
|
460
465
|
command: "entra lifecycle user-processing-result list",
|
|
461
466
|
key: "userProcessingResults",
|
|
467
|
+
totalsNoun: "user processing results",
|
|
462
468
|
known: USER_PROCESSING_KNOWN,
|
|
463
469
|
knownList: KNOWN_USER_PROCESSING_FIELDS,
|
|
464
470
|
defaultSelect: DEFAULT_USER_PROCESSING_LIST_SELECT,
|
|
@@ -473,6 +479,7 @@ const USER_PROCESSING_LIST = {
|
|
|
473
479
|
const SUBJECT_PROCESSING_LIST = {
|
|
474
480
|
command: "entra lifecycle subject-processing-result list",
|
|
475
481
|
key: "subjectProcessingResults",
|
|
482
|
+
totalsNoun: "subject processing results",
|
|
476
483
|
known: SUBJECT_PROCESSING_KNOWN,
|
|
477
484
|
knownList: KNOWN_SUBJECT_PROCESSING_FIELDS,
|
|
478
485
|
defaultSelect: DEFAULT_SUBJECT_PROCESSING_FIELDS,
|
|
@@ -488,6 +495,7 @@ const SUBJECT_PROCESSING_LIST = {
|
|
|
488
495
|
const TASK_REPORT_LIST = {
|
|
489
496
|
command: "entra lifecycle task-report list",
|
|
490
497
|
key: "taskReports",
|
|
498
|
+
totalsNoun: "task reports",
|
|
491
499
|
known: TASK_REPORT_KNOWN,
|
|
492
500
|
knownList: KNOWN_TASK_REPORT_FIELDS,
|
|
493
501
|
defaultSelect: DEFAULT_TASK_REPORT_LIST_SELECT,
|
|
@@ -502,6 +510,7 @@ const TASK_REPORT_LIST = {
|
|
|
502
510
|
const RUN_USER_PROCESSING_LIST = {
|
|
503
511
|
command: "entra lifecycle run user-processing-result list",
|
|
504
512
|
key: "userProcessingResults",
|
|
513
|
+
totalsNoun: "user processing results",
|
|
505
514
|
known: USER_PROCESSING_KNOWN,
|
|
506
515
|
knownList: KNOWN_USER_PROCESSING_FIELDS,
|
|
507
516
|
defaultSelect: DEFAULT_USER_PROCESSING_LIST_SELECT,
|
|
@@ -516,6 +525,7 @@ const RUN_USER_PROCESSING_LIST = {
|
|
|
516
525
|
const RUN_SUBJECT_PROCESSING_LIST = {
|
|
517
526
|
command: "entra lifecycle run subject-processing-result list",
|
|
518
527
|
key: "subjectProcessingResults",
|
|
528
|
+
totalsNoun: "subject processing results",
|
|
519
529
|
known: SUBJECT_PROCESSING_KNOWN,
|
|
520
530
|
knownList: KNOWN_SUBJECT_PROCESSING_FIELDS,
|
|
521
531
|
defaultSelect: DEFAULT_SUBJECT_PROCESSING_FIELDS,
|
|
@@ -531,6 +541,7 @@ const RUN_SUBJECT_PROCESSING_LIST = {
|
|
|
531
541
|
const RUN_TASK_PROCESSING_LIST = {
|
|
532
542
|
command: "entra lifecycle run task-processing-result list",
|
|
533
543
|
key: "taskProcessingResults",
|
|
544
|
+
totalsNoun: "task processing results",
|
|
534
545
|
known: TASK_PROCESSING_KNOWN,
|
|
535
546
|
knownList: KNOWN_TASK_PROCESSING_FIELDS,
|
|
536
547
|
defaultSelect: DEFAULT_TASK_PROCESSING_LIST_SELECT,
|
|
@@ -605,16 +616,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
|
|
|
605
616
|
if (!result.complete) {
|
|
606
617
|
return {
|
|
607
618
|
[shape.key]: rows,
|
|
608
|
-
|
|
619
|
+
...listTotals(rows.length, result.total, shape.totalsNoun, false),
|
|
620
|
+
complete: false,
|
|
621
|
+
reason: result.reason,
|
|
609
622
|
cursor: result.cursor,
|
|
610
623
|
help: [...truncationHints, resume, ...standing],
|
|
611
624
|
};
|
|
612
625
|
}
|
|
613
|
-
const count = { returned: rows.length, complete: true };
|
|
614
626
|
if (!rows.length) {
|
|
615
|
-
return { [shape.key]: rows,
|
|
627
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.totalsNoun, true), complete: true, help: [shape.emptyNote, ...standing] };
|
|
616
628
|
}
|
|
617
|
-
return { [shape.key]: rows,
|
|
629
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.totalsNoun, true), complete: true, help: [...truncationHints, ...standing] };
|
|
618
630
|
}
|
|
619
631
|
async function showOne(key, command, known, knownList, defaultSelect, defaultScopes, hints, session, flags, profile, operation, help, profileName, params, malformed, queryless = false) {
|
|
620
632
|
if (queryless && flags.select !== undefined) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-04 multi-tenant-organization subfamily: the read mapping behind
|
|
3
4
|
// `mg-axi entra multi-tenant-organization show`,
|
|
4
5
|
// `mg-axi entra multi-tenant-organization join-request show`,
|
|
@@ -276,20 +277,22 @@ export async function listMultiTenantOrganizationTenants(session, flags, profile
|
|
|
276
277
|
if (!result.complete) {
|
|
277
278
|
return {
|
|
278
279
|
multiTenantOrganizationTenants: rows,
|
|
279
|
-
|
|
280
|
+
...listTotals(rows.length, result.total, "tenants", false),
|
|
281
|
+
complete: false,
|
|
282
|
+
reason: result.reason,
|
|
280
283
|
cursor: result.cursor,
|
|
281
284
|
help: [...truncationHints, resumeHint(profileName), countHint, MTO_NOTE],
|
|
282
285
|
};
|
|
283
286
|
}
|
|
284
|
-
const count = { returned: rows.length, complete: true };
|
|
285
287
|
if (!rows.length) {
|
|
286
288
|
return {
|
|
287
289
|
multiTenantOrganizationTenants: rows,
|
|
288
|
-
|
|
290
|
+
...listTotals(rows.length, result.total, "tenants", true),
|
|
291
|
+
complete: true,
|
|
289
292
|
help: ["0 tenants matched; the absence of results is the answer, not an error", MTO_NOTE],
|
|
290
293
|
};
|
|
291
294
|
}
|
|
292
|
-
return { multiTenantOrganizationTenants: rows,
|
|
295
|
+
return { multiTenantOrganizationTenants: rows, ...listTotals(rows.length, result.total, "tenants", true), complete: true, help: [...truncationHints, countHint, MTO_NOTE] };
|
|
293
296
|
}
|
|
294
297
|
// The $count route returns a text/plain integer scalar rather than a JSON
|
|
295
298
|
// collection, so the leaf reads it through session.execute and accepts only
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-01 on-premises-synchronization subfamily: the read mapping behind
|
|
3
4
|
// `mg-axi entra on-premises-synchronization list/show`. Operation
|
|
4
5
|
// construction stays beside its command; the shared session owns URLs,
|
|
@@ -201,16 +202,17 @@ export async function listSynchronizations(session, flags, profile, operation, h
|
|
|
201
202
|
if (!result.complete) {
|
|
202
203
|
return {
|
|
203
204
|
synchronizations,
|
|
204
|
-
|
|
205
|
+
...listTotals(synchronizations.length, result.total, "synchronizations", false),
|
|
206
|
+
complete: false,
|
|
207
|
+
reason: result.reason,
|
|
205
208
|
cursor: result.cursor,
|
|
206
209
|
help: [...truncationHints, resumeHint(profileName), showHint],
|
|
207
210
|
};
|
|
208
211
|
}
|
|
209
|
-
const count = { returned: synchronizations.length, complete: true };
|
|
210
212
|
if (!synchronizations.length) {
|
|
211
|
-
return { synchronizations,
|
|
213
|
+
return { synchronizations, ...listTotals(synchronizations.length, result.total, "synchronizations", true), complete: true, help: [EMPTY_NOTE] };
|
|
212
214
|
}
|
|
213
|
-
return { synchronizations,
|
|
215
|
+
return { synchronizations, ...listTotals(synchronizations.length, result.total, "synchronizations", true), complete: true, help: [...truncationHints, showHint] };
|
|
214
216
|
}
|
|
215
217
|
export async function showSynchronization(session, flags, profile, operation, help, profileName) {
|
|
216
218
|
const { select, fields } = selectedFields(flags, DEFAULT_SHOW_SELECT, help);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-03 risk-prevention subfamily: the read mapping behind
|
|
3
4
|
// `mg-axi entra fraud-protection-provider list/show/count`,
|
|
4
5
|
// `mg-axi entra web-application-firewall-provider list/show/count` and
|
|
@@ -127,6 +128,7 @@ export const DEFAULT_RISK_SCOPES = ["https://graph.microsoft.com/RiskPreventionP
|
|
|
127
128
|
const TRUNCATE_AT = 500;
|
|
128
129
|
const FRAUD_KIND = {
|
|
129
130
|
noun: "fraud-protection-provider",
|
|
131
|
+
totalsNoun: "fraud protection providers",
|
|
130
132
|
known: KNOWN_FRAUD,
|
|
131
133
|
knownList: KNOWN_FRAUD_FIELDS,
|
|
132
134
|
listSelect: DEFAULT_FRAUD_LIST_SELECT,
|
|
@@ -137,6 +139,7 @@ const FRAUD_KIND = {
|
|
|
137
139
|
};
|
|
138
140
|
const WAF_KIND = {
|
|
139
141
|
noun: "web-application-firewall-provider",
|
|
142
|
+
totalsNoun: "web application firewall providers",
|
|
140
143
|
known: KNOWN_WAF,
|
|
141
144
|
knownList: KNOWN_WAF_FIELDS,
|
|
142
145
|
listSelect: DEFAULT_WAF_LIST_SELECT,
|
|
@@ -147,6 +150,7 @@ const WAF_KIND = {
|
|
|
147
150
|
};
|
|
148
151
|
const VERIFICATION_KIND = {
|
|
149
152
|
noun: "web-application-firewall-verification",
|
|
153
|
+
totalsNoun: "web application firewall verifications",
|
|
150
154
|
known: KNOWN_VERIFICATIONS,
|
|
151
155
|
knownList: KNOWN_VERIFICATION_FIELDS,
|
|
152
156
|
listSelect: DEFAULT_VERIFICATION_LIST_SELECT,
|
|
@@ -344,20 +348,22 @@ async function collectKind(session, flags, kind, command, showCommand, profile,
|
|
|
344
348
|
if (!result.complete) {
|
|
345
349
|
return {
|
|
346
350
|
[kind.collectionKey]: rows,
|
|
347
|
-
|
|
351
|
+
...listTotals(rows.length, result.total, kind.totalsNoun, false),
|
|
352
|
+
complete: false,
|
|
353
|
+
reason: result.reason,
|
|
348
354
|
cursor: result.cursor,
|
|
349
355
|
help: [...truncationHints, resumeHint(profileName), showHint, WORKFORCE_NOTE],
|
|
350
356
|
};
|
|
351
357
|
}
|
|
352
|
-
const count = { returned: rows.length, complete: true };
|
|
353
358
|
if (!rows.length) {
|
|
354
359
|
return {
|
|
355
360
|
[kind.collectionKey]: rows,
|
|
356
|
-
|
|
361
|
+
...listTotals(rows.length, result.total, kind.totalsNoun, true),
|
|
362
|
+
complete: true,
|
|
357
363
|
help: [`0 ${kind.noun}s matched; the absence of results is the answer, not an error`, WORKFORCE_NOTE],
|
|
358
364
|
};
|
|
359
365
|
}
|
|
360
|
-
return { [kind.collectionKey]: rows,
|
|
366
|
+
return { [kind.collectionKey]: rows, ...listTotals(rows.length, result.total, kind.totalsNoun, true), complete: true, help: [...truncationHints, showHint, WORKFORCE_NOTE] };
|
|
361
367
|
}
|
|
362
368
|
async function showKind(session, flags, kind, command, param, profile, operation, help, profileName) {
|
|
363
369
|
const { select, fields } = selectedFields(flags, kind, kind.showSelect, help);
|
package/dist/entra-risk.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// READ-06: the Entra risky-user and risk-detection read mapping behind
|
|
3
4
|
// `mg-axi entra risky-user list/show` and `mg-axi entra risk-detection
|
|
4
5
|
// list/show`, extended with the workload-identity surface behind
|
|
@@ -313,6 +314,7 @@ function parentServicePrincipal(flags, help) {
|
|
|
313
314
|
const RISKY_USER_LIST = {
|
|
314
315
|
noun: "risky-user",
|
|
315
316
|
key: "riskyUsers",
|
|
317
|
+
totalsNoun: "risky users",
|
|
316
318
|
known: RISKY_USER_KNOWN,
|
|
317
319
|
knownList: KNOWN_RISKY_USER_FIELDS,
|
|
318
320
|
defaultSelect: DEFAULT_RISKY_USER_LIST_SELECT,
|
|
@@ -329,6 +331,7 @@ const RISKY_USER_LIST = {
|
|
|
329
331
|
const RISK_DETECTION_LIST = {
|
|
330
332
|
noun: "risk-detection",
|
|
331
333
|
key: "riskDetections",
|
|
334
|
+
totalsNoun: "risk detections",
|
|
332
335
|
dateField: "detectedDateTime",
|
|
333
336
|
known: RISK_DETECTION_KNOWN,
|
|
334
337
|
knownList: KNOWN_RISK_DETECTION_FIELDS,
|
|
@@ -354,6 +357,7 @@ const DEFAULT_SP_DETECTION_SHOW_SELECT = [...KNOWN_SP_DETECTION_FIELDS];
|
|
|
354
357
|
const RISKY_SP_LIST = {
|
|
355
358
|
noun: "risky-service-principal",
|
|
356
359
|
key: "riskyServicePrincipals",
|
|
360
|
+
totalsNoun: "risky service principals",
|
|
357
361
|
known: RISKY_SP_KNOWN,
|
|
358
362
|
knownList: KNOWN_RISKY_SP_FIELDS,
|
|
359
363
|
defaultSelect: DEFAULT_RISKY_SP_LIST_SELECT,
|
|
@@ -370,6 +374,7 @@ const RISKY_SP_LIST = {
|
|
|
370
374
|
const RISKY_SP_HISTORY_LIST = {
|
|
371
375
|
noun: "risky-service-principal history",
|
|
372
376
|
key: "riskyServicePrincipalHistory",
|
|
377
|
+
totalsNoun: "history items",
|
|
373
378
|
pathParams: parentServicePrincipal,
|
|
374
379
|
known: RISKY_SP_HISTORY_KNOWN,
|
|
375
380
|
knownList: KNOWN_RISKY_SP_HISTORY_FIELDS,
|
|
@@ -386,6 +391,7 @@ const RISKY_SP_HISTORY_LIST = {
|
|
|
386
391
|
const SP_DETECTION_LIST = {
|
|
387
392
|
noun: "service-principal-risk-detection",
|
|
388
393
|
key: "servicePrincipalRiskDetections",
|
|
394
|
+
totalsNoun: "service principal risk detections",
|
|
389
395
|
dateField: "detectedDateTime",
|
|
390
396
|
known: SP_DETECTION_KNOWN,
|
|
391
397
|
knownList: KNOWN_SP_DETECTION_FIELDS,
|
|
@@ -468,16 +474,17 @@ async function listRisk(shape, session, flags, profile, operation, help, profile
|
|
|
468
474
|
if (!result.complete) {
|
|
469
475
|
return {
|
|
470
476
|
[shape.key]: rows,
|
|
471
|
-
|
|
477
|
+
...listTotals(rows.length, result.total, shape.totalsNoun, false),
|
|
478
|
+
complete: false,
|
|
479
|
+
reason: result.reason,
|
|
472
480
|
cursor: result.cursor,
|
|
473
481
|
help: [...truncationHints, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`, showHint, ...shape.extraHelp(profileName)],
|
|
474
482
|
};
|
|
475
483
|
}
|
|
476
|
-
const count = { returned: rows.length, complete: true };
|
|
477
484
|
if (!rows.length) {
|
|
478
|
-
return { [shape.key]: rows,
|
|
485
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.totalsNoun, true), complete: true, help: shape.emptyHints(profileName) };
|
|
479
486
|
}
|
|
480
|
-
return { [shape.key]: rows,
|
|
487
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.totalsNoun, true), complete: true, help: [...truncationHints, showHint, ...shape.extraHelp(profileName)] };
|
|
481
488
|
}
|
|
482
489
|
const RISKY_USER_SHOW = {
|
|
483
490
|
noun: "risky-user",
|
package/dist/entra-roles.js
CHANGED
|
@@ -1,27 +1,41 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
2
|
import { listTotals } from "./list-totals.js";
|
|
3
3
|
// READ-09: the Entra directory-role and PIM read mapping behind
|
|
4
|
-
// `mg-axi entra directory-role list/show`, `mg-axi entra role-
|
|
5
|
-
// list`, `mg-axi entra
|
|
4
|
+
// `mg-axi entra directory-role list/show`, `mg-axi entra directory-role-template
|
|
5
|
+
// list/show`, `mg-axi entra directory-role member list`, `mg-axi entra
|
|
6
|
+
// directory-role scoped-member list/show`, `mg-axi entra scoped-role-membership
|
|
7
|
+
// list/show`, `mg-axi entra role-assignment list`, `mg-axi entra pim eligible
|
|
8
|
+
// list` and `mg-axi entra pim active list`.
|
|
6
9
|
// Operation construction stays beside its command; the shared session owns
|
|
7
10
|
// URLs, credentials, paging, retries and error translation, and the SDK owns
|
|
8
11
|
// TOON rendering. This module only maps flags to session calls and projects
|
|
9
12
|
// rows for compact output.
|
|
10
13
|
//
|
|
11
14
|
// Reviewed against the v1.0 directoryrole-list, directoryrole-get,
|
|
15
|
+
// directoryroletemplate-list, directoryroletemplate-get,
|
|
16
|
+
// directoryrole-list-members, directoryrole-list-scopedmembers,
|
|
17
|
+
// scopedrolemembership-list, scopedrolemembership-get,
|
|
12
18
|
// rbacapplication-list-roleassignments,
|
|
13
19
|
// rbacapplication-list-roleeligibilityscheduleinstances and
|
|
14
20
|
// rbacapplication-list-roleassignmentscheduleinstances operation
|
|
15
|
-
// documentation on 2026-10-04
|
|
16
|
-
//
|
|
21
|
+
// documentation on 2026-10-04 (roles, assignments and PIM) and 2026-10-06
|
|
22
|
+
// (templates, members, scoped members and scoped memberships). Directory-role reads, role
|
|
23
|
+
// assignments, templates, members, scoped members and scoped memberships
|
|
24
|
+
// read D/A RoleManagement.Read.Directory; eligible PIM reads
|
|
17
25
|
// D/A RoleEligibilitySchedule.Read.Directory; active PIM reads D/A
|
|
18
26
|
// RoleAssignmentSchedule.Read.Directory. Delegated callers additionally need
|
|
19
27
|
// a supported administrator role per operation. None of these collections
|
|
20
28
|
// documents an advanced-query contract, so --filter passes through as plain
|
|
21
29
|
// $filter with no $count or ConsistencyLevel attached.
|
|
22
30
|
//
|
|
23
|
-
// The
|
|
31
|
+
// The views cover: directoryRoles are activated role
|
|
24
32
|
// instances only (a role appears here after activation, never before);
|
|
33
|
+
// directoryRoleTemplates describe every role definition whether activated or
|
|
34
|
+
// not; members are the principals assigned to one activated role (@odata.type
|
|
35
|
+
// names each member kind: user, group, device or service principal); scoped
|
|
36
|
+
// members bind one role to one principal within one directory scope;
|
|
37
|
+
// scopedRoleMemberships are the administrative-unit-scoped admin assignments
|
|
38
|
+
// (directory-wide assignments live on roleAssignments);
|
|
25
39
|
// roleAssignments include direct and PIM-activated assignments; eligible schedule
|
|
26
40
|
// instances are PIM-eligible but not active; active schedule instances cover
|
|
27
41
|
// both directly assigned (assignmentType Assigned) and activated eligible
|
|
@@ -57,10 +71,31 @@ export const KNOWN_ELIGIBLE_FIELDS = [
|
|
|
57
71
|
"endDateTime",
|
|
58
72
|
];
|
|
59
73
|
export const KNOWN_ACTIVE_FIELDS = [...KNOWN_ELIGIBLE_FIELDS, "assignmentType"];
|
|
74
|
+
export const KNOWN_TEMPLATE_FIELDS = [
|
|
75
|
+
"id",
|
|
76
|
+
"displayName",
|
|
77
|
+
"description",
|
|
78
|
+
];
|
|
79
|
+
export const KNOWN_ROLE_MEMBER_FIELDS = [
|
|
80
|
+
"id",
|
|
81
|
+
"displayName",
|
|
82
|
+
"mail",
|
|
83
|
+
"userPrincipalName",
|
|
84
|
+
];
|
|
85
|
+
export const KNOWN_SCOPED_MEMBER_FIELDS = [
|
|
86
|
+
"id",
|
|
87
|
+
"principalId",
|
|
88
|
+
"roleId",
|
|
89
|
+
"directoryScopeId",
|
|
90
|
+
"administrativeUnitId",
|
|
91
|
+
];
|
|
60
92
|
const ROLE_KNOWN = new Set(KNOWN_ROLE_FIELDS);
|
|
61
93
|
const ASSIGNMENT_KNOWN = new Set(KNOWN_ASSIGNMENT_FIELDS);
|
|
62
94
|
const ELIGIBLE_KNOWN = new Set(KNOWN_ELIGIBLE_FIELDS);
|
|
63
95
|
const ACTIVE_KNOWN = new Set(KNOWN_ACTIVE_FIELDS);
|
|
96
|
+
const TEMPLATE_KNOWN = new Set(KNOWN_TEMPLATE_FIELDS);
|
|
97
|
+
const MEMBER_KNOWN = new Set(KNOWN_ROLE_MEMBER_FIELDS);
|
|
98
|
+
const SCOPED_KNOWN = new Set(KNOWN_SCOPED_MEMBER_FIELDS);
|
|
64
99
|
// Compact rows: identifiers plus the correlation keys. For built-in roles
|
|
65
100
|
// the unified roleDefinitionId matches the directory-role roleTemplateId.
|
|
66
101
|
const DEFAULT_ROLE_LIST_SELECT = ["id", "displayName", "description", "roleTemplateId"];
|
|
@@ -68,6 +103,11 @@ const DEFAULT_ROLE_SHOW_SELECT = [...KNOWN_ROLE_FIELDS];
|
|
|
68
103
|
const DEFAULT_ASSIGNMENT_SELECT = ["id", "principalId", "roleDefinitionId", "directoryScopeId"];
|
|
69
104
|
const DEFAULT_ELIGIBLE_SELECT = ["id", "principalId", "roleDefinitionId", "memberType"];
|
|
70
105
|
const DEFAULT_ACTIVE_SELECT = ["id", "principalId", "roleDefinitionId", "assignmentType", "memberType"];
|
|
106
|
+
const DEFAULT_TEMPLATE_LIST_SELECT = ["id", "displayName", "description"];
|
|
107
|
+
const DEFAULT_TEMPLATE_SHOW_SELECT = [...KNOWN_TEMPLATE_FIELDS];
|
|
108
|
+
const DEFAULT_MEMBER_SELECT = ["id", "displayName"];
|
|
109
|
+
const DEFAULT_SCOPED_LIST_SELECT = ["id", "principalId", "roleId", "directoryScopeId"];
|
|
110
|
+
const DEFAULT_SCOPED_SHOW_SELECT = [...KNOWN_SCOPED_MEMBER_FIELDS];
|
|
71
111
|
export const DEFAULT_ROLE_SCOPES = ["https://graph.microsoft.com/RoleManagement.Read.Directory"];
|
|
72
112
|
export const DEFAULT_ELIGIBLE_SCOPES = ["https://graph.microsoft.com/RoleEligibilitySchedule.Read.Directory"];
|
|
73
113
|
export const DEFAULT_ACTIVE_SCOPES = ["https://graph.microsoft.com/RoleAssignmentSchedule.Read.Directory"];
|
|
@@ -115,9 +155,14 @@ function truncateValue(value, full) {
|
|
|
115
155
|
}
|
|
116
156
|
// Local projection preserves Graph's null/missing distinction: an explicit
|
|
117
157
|
// null stays null, an absent property stays absent and is never synthesized.
|
|
118
|
-
function project(row, fields, full) {
|
|
158
|
+
function project(row, fields, full, keepODataType = false) {
|
|
119
159
|
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
120
160
|
const projected = {};
|
|
161
|
+
// Heterogeneous member rows carry their kind in @odata.type, which is
|
|
162
|
+
// structural rather than a selectable property, so it rides along first.
|
|
163
|
+
if (keepODataType && typeof source["@odata.type"] === "string") {
|
|
164
|
+
projected["@odata.type"] = source["@odata.type"];
|
|
165
|
+
}
|
|
121
166
|
let truncated = false;
|
|
122
167
|
for (const field of fields) {
|
|
123
168
|
if (!Object.hasOwn(source, field))
|
|
@@ -175,6 +220,10 @@ const ACTIVE_DENIAL_HINTS = [
|
|
|
175
220
|
"Active PIM reads need RoleAssignmentSchedule.Read.Directory plus Global Reader, Security Operator, Security Reader, Security Administrator or Privileged Role Administrator for delegated access, or admin-consented RoleAssignmentSchedule.Read.Directory for application access",
|
|
176
221
|
"PIM reads need P2 or ID Governance, not only P2",
|
|
177
222
|
];
|
|
223
|
+
const SCOPED_DENIAL_HINTS = [
|
|
224
|
+
"Scoped membership reads need RoleManagement.Read.Directory plus Global Reader, Security Reader or Privileged Role Administrator for delegated access, or admin-consented RoleManagement.Read.Directory for application access",
|
|
225
|
+
"Administrative-unit-scoped memberships need P1",
|
|
226
|
+
];
|
|
178
227
|
const ROLE_LIST = {
|
|
179
228
|
command: "entra directory-role list",
|
|
180
229
|
key: "directoryRoles",
|
|
@@ -223,10 +272,68 @@ const PIM_ACTIVE = {
|
|
|
223
272
|
scopeNote: "Active distinguishes directly assigned (assignmentType Assigned) and activated eligible (assignmentType Activated) assignments",
|
|
224
273
|
emptyNote: "0 active assignments matched; the absence of results is the answer, not an error",
|
|
225
274
|
};
|
|
275
|
+
const TEMPLATE_LIST = {
|
|
276
|
+
command: "entra directory-role-template list",
|
|
277
|
+
key: "directoryRoleTemplates",
|
|
278
|
+
noun: "directory-role templates",
|
|
279
|
+
known: TEMPLATE_KNOWN,
|
|
280
|
+
knownList: KNOWN_TEMPLATE_FIELDS,
|
|
281
|
+
defaultSelect: DEFAULT_TEMPLATE_LIST_SELECT,
|
|
282
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
283
|
+
denialHints: ROLE_DENIAL_HINTS,
|
|
284
|
+
scopeNote: "Templates describe every role definition; activation state lives on directoryRoles, which appear only after activation",
|
|
285
|
+
emptyNote: "0 directory-role templates matched; the absence of results is the answer, not an error",
|
|
286
|
+
};
|
|
287
|
+
const MEMBER_LIST = {
|
|
288
|
+
command: "entra directory-role member list",
|
|
289
|
+
key: "roleMembers",
|
|
290
|
+
noun: "role members",
|
|
291
|
+
known: MEMBER_KNOWN,
|
|
292
|
+
knownList: KNOWN_ROLE_MEMBER_FIELDS,
|
|
293
|
+
defaultSelect: DEFAULT_MEMBER_SELECT,
|
|
294
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
295
|
+
denialHints: ROLE_DENIAL_HINTS,
|
|
296
|
+
scopeNote: "Members are the principals assigned to the activated role; @odata.type names each member kind (user, group, device or service principal)",
|
|
297
|
+
emptyNote: "0 role members matched; the absence of results is the answer, not an error",
|
|
298
|
+
parent: { param: "directoryRole-id", flag: "role", label: "activated directory-role object ID whose members are listed" },
|
|
299
|
+
keepODataType: true,
|
|
300
|
+
};
|
|
301
|
+
const SCOPED_LIST = {
|
|
302
|
+
command: "entra directory-role scoped-member list",
|
|
303
|
+
key: "scopedMembers",
|
|
304
|
+
noun: "scoped members",
|
|
305
|
+
known: SCOPED_KNOWN,
|
|
306
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
307
|
+
defaultSelect: DEFAULT_SCOPED_LIST_SELECT,
|
|
308
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
309
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
310
|
+
scopeNote: "Scoped members bind one role to one principal within one directory scope; administrative-unit scopes need P1",
|
|
311
|
+
emptyNote: "0 scoped members matched; the absence of results is the answer, not an error",
|
|
312
|
+
parent: { param: "directoryRole-id", flag: "role", label: "activated directory-role object ID whose scoped members are listed" },
|
|
313
|
+
};
|
|
314
|
+
const MEMBERSHIP_LIST = {
|
|
315
|
+
command: "entra scoped-role-membership list",
|
|
316
|
+
key: "scopedRoleMemberships",
|
|
317
|
+
noun: "scoped role memberships",
|
|
318
|
+
known: SCOPED_KNOWN,
|
|
319
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
320
|
+
defaultSelect: DEFAULT_SCOPED_LIST_SELECT,
|
|
321
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
322
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
323
|
+
scopeNote: "Scoped role memberships are administrative-unit-scoped admin assignments; directory-wide assignments live on roleAssignments",
|
|
324
|
+
emptyNote: "0 scoped role memberships matched; the absence of results is the answer, not an error",
|
|
325
|
+
};
|
|
226
326
|
async function listCollection(shape, session, flags, profile, operation, help, profileName) {
|
|
227
327
|
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
228
328
|
if (cursor !== undefined && !cursor.trim())
|
|
229
329
|
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
330
|
+
let params;
|
|
331
|
+
if (shape.parent !== undefined) {
|
|
332
|
+
const parentId = String(flags[shape.parent.flag] ?? "");
|
|
333
|
+
if (!parentId.trim())
|
|
334
|
+
throw new AxiError(`--${shape.parent.flag} needs the ${shape.parent.label}`, "VALIDATION_ERROR", [help]);
|
|
335
|
+
params = { [shape.parent.param]: parentId };
|
|
336
|
+
}
|
|
230
337
|
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
231
338
|
const savedSelect = saved?.$select;
|
|
232
339
|
const { select, fields } = selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help);
|
|
@@ -240,6 +347,8 @@ async function listCollection(shape, session, flags, profile, operation, help, p
|
|
|
240
347
|
if (filter !== undefined)
|
|
241
348
|
query.$filter = filter;
|
|
242
349
|
const args = { profile, operation, query, scopes };
|
|
350
|
+
if (params !== undefined)
|
|
351
|
+
args.params = params;
|
|
243
352
|
if (cursor !== undefined)
|
|
244
353
|
args.cursor = cursor;
|
|
245
354
|
if (flags.all === true) {
|
|
@@ -256,11 +365,13 @@ async function listCollection(shape, session, flags, profile, operation, help, p
|
|
|
256
365
|
const rows = [];
|
|
257
366
|
let truncated = false;
|
|
258
367
|
for (const row of result.value) {
|
|
259
|
-
const projected = project(row, fields, full);
|
|
368
|
+
const projected = project(row, fields, full, shape.keepODataType === true);
|
|
260
369
|
rows.push(projected.row);
|
|
261
370
|
truncated = truncated || projected.truncated;
|
|
262
371
|
}
|
|
263
|
-
const showHint = shape.key === "directoryRoles" ? `mg-axi entra directory-role show --id <role-id> ${profileHint(profileName)}`
|
|
372
|
+
const showHint = shape.key === "directoryRoles" ? `mg-axi entra directory-role show --id <role-id> ${profileHint(profileName)}`
|
|
373
|
+
: shape.key === "directoryRoleTemplates" ? `mg-axi entra directory-role-template show --id <template-id> ${profileHint(profileName)}`
|
|
374
|
+
: undefined;
|
|
264
375
|
const correlateHint = shape.key === "directoryRoles"
|
|
265
376
|
? undefined
|
|
266
377
|
: `For built-in roles, roleDefinitionId matches the directory-role roleTemplateId: mg-axi entra directory-role list ${profileHint(profileName)}`;
|
|
@@ -314,3 +425,78 @@ export async function listPimEligible(session, flags, profile, operation, help,
|
|
|
314
425
|
export async function listPimActive(session, flags, profile, operation, help, profileName) {
|
|
315
426
|
return listCollection(PIM_ACTIVE, session, flags, profile, operation, help, profileName);
|
|
316
427
|
}
|
|
428
|
+
const TEMPLATE_SINGLE = {
|
|
429
|
+
command: "entra directory-role-template show",
|
|
430
|
+
key: "directoryRoleTemplate",
|
|
431
|
+
noun: "directory-role template",
|
|
432
|
+
known: TEMPLATE_KNOWN,
|
|
433
|
+
knownList: KNOWN_TEMPLATE_FIELDS,
|
|
434
|
+
defaultSelect: DEFAULT_TEMPLATE_SHOW_SELECT,
|
|
435
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
436
|
+
denialHints: ROLE_DENIAL_HINTS,
|
|
437
|
+
};
|
|
438
|
+
const SCOPED_SINGLE = {
|
|
439
|
+
command: "entra directory-role scoped-member show",
|
|
440
|
+
key: "scopedMember",
|
|
441
|
+
noun: "scoped member",
|
|
442
|
+
known: SCOPED_KNOWN,
|
|
443
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
444
|
+
defaultSelect: DEFAULT_SCOPED_SHOW_SELECT,
|
|
445
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
446
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
447
|
+
};
|
|
448
|
+
const MEMBERSHIP_SINGLE = {
|
|
449
|
+
command: "entra scoped-role-membership show",
|
|
450
|
+
key: "scopedRoleMembership",
|
|
451
|
+
noun: "scoped role membership",
|
|
452
|
+
known: SCOPED_KNOWN,
|
|
453
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
454
|
+
defaultSelect: DEFAULT_SCOPED_SHOW_SELECT,
|
|
455
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
456
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
457
|
+
};
|
|
458
|
+
async function showSingle(shape, params, session, flags, profile, operation, help, profileName) {
|
|
459
|
+
const { select, fields } = selectedFields(flags, shape.defaultSelect, shape.known, shape.knownList, help);
|
|
460
|
+
const scopes = scopesFor(flags, profile, shape.defaultScopes, help);
|
|
461
|
+
const full = flags.full === true;
|
|
462
|
+
const raw = await withGuidance(shape.denialHints, () => session.execute({
|
|
463
|
+
profile,
|
|
464
|
+
operation,
|
|
465
|
+
params,
|
|
466
|
+
query: { $select: select.join(",") },
|
|
467
|
+
scopes,
|
|
468
|
+
}));
|
|
469
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
470
|
+
throw new AxiError(`Graph returned a malformed ${shape.noun} body`, "GRAPH_ERROR", [
|
|
471
|
+
`Single-${shape.noun} reads carry one object; treat anything else as unknown, not empty`,
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
const { row, truncated } = project(raw, fields, full);
|
|
475
|
+
if (truncated)
|
|
476
|
+
return { [shape.key]: row, help: [fullHint(shape.command, flags, profileName)] };
|
|
477
|
+
return { [shape.key]: row };
|
|
478
|
+
}
|
|
479
|
+
export async function listDirectoryRoleTemplates(session, flags, profile, operation, help, profileName) {
|
|
480
|
+
return listCollection(TEMPLATE_LIST, session, flags, profile, operation, help, profileName);
|
|
481
|
+
}
|
|
482
|
+
export async function showDirectoryRoleTemplate(session, flags, profile, operation, help, profileName) {
|
|
483
|
+
return showSingle(TEMPLATE_SINGLE, { "directoryRoleTemplate-id": String(flags.id) }, session, flags, profile, operation, help, profileName);
|
|
484
|
+
}
|
|
485
|
+
export async function listRoleMembers(session, flags, profile, operation, help, profileName) {
|
|
486
|
+
return listCollection(MEMBER_LIST, session, flags, profile, operation, help, profileName);
|
|
487
|
+
}
|
|
488
|
+
export async function listScopedMembers(session, flags, profile, operation, help, profileName) {
|
|
489
|
+
return listCollection(SCOPED_LIST, session, flags, profile, operation, help, profileName);
|
|
490
|
+
}
|
|
491
|
+
export async function showScopedMember(session, flags, profile, operation, help, profileName) {
|
|
492
|
+
const roleId = String(flags.role ?? "");
|
|
493
|
+
if (!roleId.trim())
|
|
494
|
+
throw new AxiError("--role needs the activated directory-role object ID owning the scoped member", "VALIDATION_ERROR", [help]);
|
|
495
|
+
return showSingle(SCOPED_SINGLE, { "directoryRole-id": roleId, "scopedRoleMembership-id": String(flags.id) }, session, flags, profile, operation, help, profileName);
|
|
496
|
+
}
|
|
497
|
+
export async function listScopedRoleMemberships(session, flags, profile, operation, help, profileName) {
|
|
498
|
+
return listCollection(MEMBERSHIP_LIST, session, flags, profile, operation, help, profileName);
|
|
499
|
+
}
|
|
500
|
+
export async function showScopedRoleMembership(session, flags, profile, operation, help, profileName) {
|
|
501
|
+
return showSingle(MEMBERSHIP_SINGLE, { "scopedRoleMembership-id": String(flags.id) }, session, flags, profile, operation, help, profileName);
|
|
502
|
+
}
|