@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,464 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
// EXT-01 custom-security-attributes subfamily: the read mapping behind
|
|
3
|
+
// `mg-axi entra attribute-set list/show/count`,
|
|
4
|
+
// `mg-axi entra custom-security-attribute-definition list/show/count` and
|
|
5
|
+
// `mg-axi entra allowed-value list/show/count`. Operation construction stays
|
|
6
|
+
// beside its command; the shared session owns URLs, credentials, paging,
|
|
7
|
+
// retries and error translation, and the SDK owns TOON rendering. This
|
|
8
|
+
// module only maps flags to session calls and projects rows for compact
|
|
9
|
+
// output.
|
|
10
|
+
//
|
|
11
|
+
// Reviewed against the v1.0 directory-list-attributesets, attributeset-get,
|
|
12
|
+
// directory-list-customsecurityattributedefinitions,
|
|
13
|
+
// customsecurityattributedefinition-get,
|
|
14
|
+
// customsecurityattributedefinition-list-allowedvalues and allowedvalue-get
|
|
15
|
+
// operation documentation and the attributeSet,
|
|
16
|
+
// customSecurityAttributeDefinition and allowedValue resource references on
|
|
17
|
+
// 2026-10-05. Every read takes D/A CustomSecAttributeDefinition.Read.All;
|
|
18
|
+
// delegated callers additionally need a custom-security-attribute role, and
|
|
19
|
+
// even Global Administrators have no custom-security-attribute access by
|
|
20
|
+
// default (Attribute Definition Reader or Attribute Definition
|
|
21
|
+
// Administrator work for every read in this slice). Personal Microsoft
|
|
22
|
+
// accounts are not supported. No P1/P2 prerequisite is stated for these
|
|
23
|
+
// reads. The attribute-set list documents $select, $top and $orderby only
|
|
24
|
+
// ($filter is not supported), so it offers no --filter; the definition
|
|
25
|
+
// list documents $select, $top and $filter (eq), so --filter passes through
|
|
26
|
+
// as plain $filter with no $count or ConsistencyLevel contract. The
|
|
27
|
+
// allowed-values list documents $select only, so it offers no --filter.
|
|
28
|
+
// Definition $expand (inline allowedValues) is not reviewed here: allowed
|
|
29
|
+
// values have their own list/show commands. The $count routes carry no
|
|
30
|
+
// operation-level documentation page; only the definition count accepts
|
|
31
|
+
// --filter (its collection documents $filter), the other two take no
|
|
32
|
+
// --filter/--select/--limit/--cursor. Beta attribute sets, definitions and
|
|
33
|
+
// allowed values and every mutation stay out. No mutation lives here.
|
|
34
|
+
// Every attribute-set property this slice may request or display, matching
|
|
35
|
+
// the reviewed resource. Anything else fails before credentials.
|
|
36
|
+
export const KNOWN_ATTRIBUTE_SET_FIELDS = [
|
|
37
|
+
"id",
|
|
38
|
+
"description",
|
|
39
|
+
"maxAttributesPerSet",
|
|
40
|
+
];
|
|
41
|
+
const KNOWN_ATTRIBUTE_SETS = new Set(KNOWN_ATTRIBUTE_SET_FIELDS);
|
|
42
|
+
// Every definition property this slice may request or display, matching the
|
|
43
|
+
// reviewed resource. The allowedValues navigation property is never
|
|
44
|
+
// projected here; it has its own list/show commands. Anything else fails
|
|
45
|
+
// before credentials.
|
|
46
|
+
export const KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS = [
|
|
47
|
+
"attributeSet",
|
|
48
|
+
"description",
|
|
49
|
+
"id",
|
|
50
|
+
"isCollection",
|
|
51
|
+
"isSearchable",
|
|
52
|
+
"name",
|
|
53
|
+
"status",
|
|
54
|
+
"type",
|
|
55
|
+
"usePreDefinedValuesOnly",
|
|
56
|
+
];
|
|
57
|
+
const KNOWN_DEFINITIONS = new Set(KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS);
|
|
58
|
+
// Every allowed-value property this slice may request or display, matching
|
|
59
|
+
// the reviewed resource. Anything else fails before credentials.
|
|
60
|
+
export const KNOWN_ALLOWED_VALUE_FIELDS = [
|
|
61
|
+
"id",
|
|
62
|
+
"isActive",
|
|
63
|
+
];
|
|
64
|
+
const KNOWN_ALLOWED_VALUES = new Set(KNOWN_ALLOWED_VALUE_FIELDS);
|
|
65
|
+
// Attribute sets carry only three short properties, so lists and shows
|
|
66
|
+
// share the full reviewed set.
|
|
67
|
+
const DEFAULT_ATTRIBUTE_SET_LIST_SELECT = [...KNOWN_ATTRIBUTE_SET_FIELDS];
|
|
68
|
+
const DEFAULT_ATTRIBUTE_SET_SHOW_SELECT = [...KNOWN_ATTRIBUTE_SET_FIELDS];
|
|
69
|
+
// Compact definition rows: the identifier, the owning set and name, the
|
|
70
|
+
// availability status and the data type that decide whether the definition
|
|
71
|
+
// is usable.
|
|
72
|
+
const DEFAULT_DEFINITION_LIST_SELECT = ["id", "attributeSet", "name", "status", "type"];
|
|
73
|
+
// Show rows: the full reviewed definition set.
|
|
74
|
+
const DEFAULT_DEFINITION_SHOW_SELECT = [...KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS];
|
|
75
|
+
// Allowed values carry only two short properties, so lists and shows share
|
|
76
|
+
// the full reviewed set.
|
|
77
|
+
const DEFAULT_ALLOWED_VALUE_LIST_SELECT = [...KNOWN_ALLOWED_VALUE_FIELDS];
|
|
78
|
+
const DEFAULT_ALLOWED_VALUE_SHOW_SELECT = [...KNOWN_ALLOWED_VALUE_FIELDS];
|
|
79
|
+
// Delegated defaults are operation-specific; application profiles use their
|
|
80
|
+
// configured .default audience and reject --scopes.
|
|
81
|
+
export const DEFAULT_CUSTOM_SECURITY_SCOPES = ["https://graph.microsoft.com/CustomSecAttributeDefinition.Read.All"];
|
|
82
|
+
const TRUNCATE_AT = 500;
|
|
83
|
+
function fieldList(raw, known, knownList, flag, help, noun) {
|
|
84
|
+
const parts = String(raw)
|
|
85
|
+
.split(",")
|
|
86
|
+
.map(part => part.trim())
|
|
87
|
+
.filter(part => part.length > 0);
|
|
88
|
+
if (!parts.length)
|
|
89
|
+
throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
|
|
90
|
+
const fields = [];
|
|
91
|
+
for (const part of parts) {
|
|
92
|
+
if (!known.has(part)) {
|
|
93
|
+
throw new AxiError(`Unknown custom-security-attribute property ${part} in --${flag}`, "VALIDATION_ERROR", [
|
|
94
|
+
help,
|
|
95
|
+
`Known ${noun} properties: ${knownList.join(", ")}`,
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
if (!fields.includes(part))
|
|
99
|
+
fields.push(part);
|
|
100
|
+
}
|
|
101
|
+
return fields;
|
|
102
|
+
}
|
|
103
|
+
function scopesFor(flags, defaults, profile, help) {
|
|
104
|
+
if (profile.mode === "application") {
|
|
105
|
+
if (flags.scopes !== undefined) {
|
|
106
|
+
throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
if (flags.scopes === undefined)
|
|
111
|
+
return [...defaults];
|
|
112
|
+
const scopes = String(flags.scopes)
|
|
113
|
+
.split(",")
|
|
114
|
+
.map(scope => scope.trim())
|
|
115
|
+
.filter(scope => scope.length > 0);
|
|
116
|
+
return scopes;
|
|
117
|
+
}
|
|
118
|
+
function truncateValue(value, full) {
|
|
119
|
+
const truncate = (text) => full || text.length <= TRUNCATE_AT
|
|
120
|
+
? { value: text, truncated: false }
|
|
121
|
+
: { value: `${text.slice(0, TRUNCATE_AT)}... (truncated, ${text.length} chars total)`, truncated: true };
|
|
122
|
+
if (typeof value === "string")
|
|
123
|
+
return truncate(value);
|
|
124
|
+
if (Array.isArray(value) && value.every(entry => typeof entry === "string")) {
|
|
125
|
+
const projected = value.map(truncate);
|
|
126
|
+
return { value: projected.map(entry => entry.value), truncated: projected.some(entry => entry.truncated) };
|
|
127
|
+
}
|
|
128
|
+
return { value, truncated: false };
|
|
129
|
+
}
|
|
130
|
+
// Local projection preserves Graph's null/missing distinction: an explicit
|
|
131
|
+
// null stays null, an absent property stays absent and is never synthesized.
|
|
132
|
+
function project(row, fields, full) {
|
|
133
|
+
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
134
|
+
const projected = {};
|
|
135
|
+
let truncated = false;
|
|
136
|
+
for (const field of fields) {
|
|
137
|
+
if (!Object.hasOwn(source, field))
|
|
138
|
+
continue;
|
|
139
|
+
const result = truncateValue(source[field], full);
|
|
140
|
+
projected[field] = result.value;
|
|
141
|
+
truncated = truncated || result.truncated;
|
|
142
|
+
}
|
|
143
|
+
return { row: projected, truncated };
|
|
144
|
+
}
|
|
145
|
+
function selectedFields(flags, known, knownList, defaults, help, noun) {
|
|
146
|
+
const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, known, knownList, "select", help, noun);
|
|
147
|
+
const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, known, knownList, "fields", help, noun);
|
|
148
|
+
const missing = fields.find(field => !select.includes(field));
|
|
149
|
+
if (missing) {
|
|
150
|
+
throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
|
|
151
|
+
}
|
|
152
|
+
return { select, fields };
|
|
153
|
+
}
|
|
154
|
+
function profileHint(profileName) {
|
|
155
|
+
return `--profile ${shellValue(profileName)}`;
|
|
156
|
+
}
|
|
157
|
+
function shellValue(value) {
|
|
158
|
+
return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
159
|
+
}
|
|
160
|
+
function fullHint(command, flags, profileName) {
|
|
161
|
+
const args = Object.entries({ ...flags, profile: profileName, full: true })
|
|
162
|
+
.map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
|
|
163
|
+
return `mg-axi ${command} ${args.join(" ")}`;
|
|
164
|
+
}
|
|
165
|
+
// Denials carry the session's generic grant/role/licence cause; each
|
|
166
|
+
// operation adds the scope, roles and licensing that actually unlock it,
|
|
167
|
+
// because a 403 alone never says which prerequisite is missing. Custom
|
|
168
|
+
// security attributes are unusual: even Global Administrators have no
|
|
169
|
+
// access by default, so the hints name the attribute roles plainly.
|
|
170
|
+
function withGuidance(hints, run) {
|
|
171
|
+
return run().catch(error => {
|
|
172
|
+
if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
|
|
173
|
+
throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const CUSTOM_SECURITY_DENIAL_HINTS = [
|
|
179
|
+
"Custom-security-attribute reads need CustomSecAttributeDefinition.Read.All for delegated or application access; delegated callers pass it as --scopes",
|
|
180
|
+
"Delegated reads additionally need a custom-security-attribute role even for Global Administrators: Attribute Definition Reader or Attribute Definition Administrator work for every read in this slice (some reads also accept Attribute Assignment Reader or Attribute Assignment Administrator); by default Global Administrator and other administrator roles have no custom-security-attribute access",
|
|
181
|
+
"Personal Microsoft accounts are not supported for custom-security-attribute reads",
|
|
182
|
+
"No P1/P2 prerequisite is stated for custom-security-attribute reads; never diagnose licence solely from HTTP 403",
|
|
183
|
+
];
|
|
184
|
+
// Restoring the saved select/filter keeps cursor resumes lossless when
|
|
185
|
+
// --select or --filter is omitted. Only the definition list documents
|
|
186
|
+
// $filter, so only it passes --filter through as plain $filter with no
|
|
187
|
+
// $count or ConsistencyLevel contract; strict input validation refuses the
|
|
188
|
+
// flag on the other lists before credentials instead of forwarding a
|
|
189
|
+
// misleading request.
|
|
190
|
+
function collectionCommon(session, flags, known, knownList, defaults, noun, operation, help, profile, withFilter) {
|
|
191
|
+
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
192
|
+
if (cursor !== undefined && !cursor.trim())
|
|
193
|
+
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
194
|
+
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
195
|
+
const { select, fields } = selectedFields(flags, known, knownList, saved?.["$select"] === undefined ? defaults : fieldList(saved["$select"], known, knownList, "select", help, noun), help, noun);
|
|
196
|
+
const filter = withFilter
|
|
197
|
+
? (flags.filter === undefined ? saved?.["$filter"] : String(flags.filter))
|
|
198
|
+
: undefined;
|
|
199
|
+
return { cursor, select, fields, scopes: scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help), full: flags.full === true, filter };
|
|
200
|
+
}
|
|
201
|
+
function collectArgs(profile, operation, common, flags, help, params) {
|
|
202
|
+
const query = { $select: common.select.join(",") };
|
|
203
|
+
if (common.filter !== undefined)
|
|
204
|
+
query.$filter = common.filter;
|
|
205
|
+
const args = { profile, operation, query, scopes: common.scopes };
|
|
206
|
+
if (params !== undefined)
|
|
207
|
+
args.params = params;
|
|
208
|
+
if (common.cursor !== undefined)
|
|
209
|
+
args.cursor = common.cursor;
|
|
210
|
+
if (flags.all === true) {
|
|
211
|
+
if (flags.limit !== undefined)
|
|
212
|
+
throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
|
|
216
|
+
}
|
|
217
|
+
return args;
|
|
218
|
+
}
|
|
219
|
+
function resumeHint(profileName) {
|
|
220
|
+
return `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`;
|
|
221
|
+
}
|
|
222
|
+
function attributeSetId(flags, help) {
|
|
223
|
+
const id = String(flags.id);
|
|
224
|
+
if (!id.trim())
|
|
225
|
+
throw new AxiError("--id needs the attribute-set identifier", "VALIDATION_ERROR", [help]);
|
|
226
|
+
return id;
|
|
227
|
+
}
|
|
228
|
+
function definitionId(flags, help, flag = "id") {
|
|
229
|
+
const id = String(flags[flag]);
|
|
230
|
+
if (!id.trim())
|
|
231
|
+
throw new AxiError(`--${flag} needs the custom-security-attribute-definition identifier`, "VALIDATION_ERROR", [help]);
|
|
232
|
+
return id;
|
|
233
|
+
}
|
|
234
|
+
function allowedValueId(flags, help) {
|
|
235
|
+
const id = String(flags.id);
|
|
236
|
+
if (!id.trim())
|
|
237
|
+
throw new AxiError("--id needs the allowed-value identifier", "VALIDATION_ERROR", [help]);
|
|
238
|
+
return id;
|
|
239
|
+
}
|
|
240
|
+
function definitionParams(flags, help) {
|
|
241
|
+
return { "customSecurityAttributeDefinition-id": definitionId(flags, help, "definition") };
|
|
242
|
+
}
|
|
243
|
+
function singleResult(raw, fields, full, kind) {
|
|
244
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
245
|
+
throw new AxiError(`Graph returned a malformed ${kind} body`, "GRAPH_ERROR", [
|
|
246
|
+
`Single-${kind} reads carry one ${kind} object; treat anything else as unknown, not empty`,
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
return project(raw, fields, full);
|
|
250
|
+
}
|
|
251
|
+
export async function listAttributeSets(session, flags, profile, operation, help, profileName) {
|
|
252
|
+
const common = collectionCommon(session, flags, KNOWN_ATTRIBUTE_SETS, KNOWN_ATTRIBUTE_SET_FIELDS, DEFAULT_ATTRIBUTE_SET_LIST_SELECT, "attribute-set", operation, help, profile, false);
|
|
253
|
+
const result = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
|
|
254
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_ATTRIBUTE_SET_LIST_SELECT.join(",") };
|
|
255
|
+
const sets = [];
|
|
256
|
+
let truncated = false;
|
|
257
|
+
for (const row of result.value) {
|
|
258
|
+
const projected = project(row, common.fields, common.full);
|
|
259
|
+
sets.push(projected.row);
|
|
260
|
+
truncated = truncated || projected.truncated;
|
|
261
|
+
}
|
|
262
|
+
const showHint = `mg-axi entra attribute-set show --id <set-id> ${profileHint(profileName)}`;
|
|
263
|
+
const filterNote = "Attribute-set lists offer no --filter: Graph documents $select, $top and $orderby only for /directory/attributeSets";
|
|
264
|
+
const truncationHints = truncated ? [fullHint("entra attribute-set list", effectiveFlags, profileName)] : [];
|
|
265
|
+
if (!result.complete) {
|
|
266
|
+
return {
|
|
267
|
+
attributeSets: sets,
|
|
268
|
+
count: { returned: sets.length, complete: false, reason: result.reason },
|
|
269
|
+
cursor: result.cursor,
|
|
270
|
+
help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const count = { returned: sets.length, complete: true };
|
|
274
|
+
if (!sets.length) {
|
|
275
|
+
return {
|
|
276
|
+
attributeSets: sets,
|
|
277
|
+
count,
|
|
278
|
+
help: [filterNote, "0 attribute sets matched; the absence of results is the answer, not an error"],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { attributeSets: sets, count, help: [...truncationHints, showHint, filterNote] };
|
|
282
|
+
}
|
|
283
|
+
export async function showAttributeSet(session, flags, profile, operation, help, profileName) {
|
|
284
|
+
const { select, fields } = selectedFields(flags, KNOWN_ATTRIBUTE_SETS, KNOWN_ATTRIBUTE_SET_FIELDS, DEFAULT_ATTRIBUTE_SET_SHOW_SELECT, help, "attribute-set");
|
|
285
|
+
const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
|
|
286
|
+
const full = flags.full === true;
|
|
287
|
+
const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
|
|
288
|
+
profile,
|
|
289
|
+
operation,
|
|
290
|
+
params: { "attributeSet-id": attributeSetId(flags, help) },
|
|
291
|
+
query: { $select: select.join(",") },
|
|
292
|
+
scopes,
|
|
293
|
+
}));
|
|
294
|
+
const { row, truncated } = singleResult(raw, fields, full, "attribute set");
|
|
295
|
+
if (truncated)
|
|
296
|
+
return { attributeSet: row, help: [fullHint("entra attribute-set show", flags, profileName)] };
|
|
297
|
+
return { attributeSet: row };
|
|
298
|
+
}
|
|
299
|
+
// The $count route returns a text/plain integer scalar rather than a JSON
|
|
300
|
+
// collection, so the leaf reads it through session.execute with scalar mode
|
|
301
|
+
// and accepts only a non-negative integer. There is no
|
|
302
|
+
// --filter/--select/--limit contract on the count: the catalogue declares
|
|
303
|
+
// no such flags and strict input validation refuses them before
|
|
304
|
+
// credentials.
|
|
305
|
+
export async function countAttributeSets(session, flags, profile, operation, help, profileName) {
|
|
306
|
+
void profileName;
|
|
307
|
+
const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
|
|
308
|
+
const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({ profile, operation, scopes, scalar: true }));
|
|
309
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
310
|
+
throw new AxiError("Graph returned a malformed attribute-set count body", "GRAPH_ERROR", [
|
|
311
|
+
"Attribute-set counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
return { count: { returned: raw, complete: true } };
|
|
315
|
+
}
|
|
316
|
+
export async function listCustomSecurityAttributeDefinitions(session, flags, profile, operation, help, profileName) {
|
|
317
|
+
const common = collectionCommon(session, flags, KNOWN_DEFINITIONS, KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS, DEFAULT_DEFINITION_LIST_SELECT, "definition", operation, help, profile, true);
|
|
318
|
+
const result = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
|
|
319
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_DEFINITION_LIST_SELECT.join(",") };
|
|
320
|
+
if (result.query.$filter !== undefined)
|
|
321
|
+
effectiveFlags.filter = result.query.$filter;
|
|
322
|
+
const definitions = [];
|
|
323
|
+
let truncated = false;
|
|
324
|
+
for (const row of result.value) {
|
|
325
|
+
const projected = project(row, common.fields, common.full);
|
|
326
|
+
definitions.push(projected.row);
|
|
327
|
+
truncated = truncated || projected.truncated;
|
|
328
|
+
}
|
|
329
|
+
const showHint = `mg-axi entra custom-security-attribute-definition show --id <definition-id> ${profileHint(profileName)}`;
|
|
330
|
+
const truncationHints = truncated ? [fullHint("entra custom-security-attribute-definition list", effectiveFlags, profileName)] : [];
|
|
331
|
+
if (!result.complete) {
|
|
332
|
+
return {
|
|
333
|
+
customSecurityAttributeDefinitions: definitions,
|
|
334
|
+
count: { returned: definitions.length, complete: false, reason: result.reason },
|
|
335
|
+
cursor: result.cursor,
|
|
336
|
+
help: [...truncationHints, resumeHint(profileName), showHint],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const count = { returned: definitions.length, complete: true };
|
|
340
|
+
if (!definitions.length) {
|
|
341
|
+
return {
|
|
342
|
+
customSecurityAttributeDefinitions: definitions,
|
|
343
|
+
count,
|
|
344
|
+
help: ["0 custom security attribute definitions matched; the absence of results is the answer, not an error"],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return { customSecurityAttributeDefinitions: definitions, count, help: [...truncationHints, showHint] };
|
|
348
|
+
}
|
|
349
|
+
export async function showCustomSecurityAttributeDefinition(session, flags, profile, operation, help, profileName) {
|
|
350
|
+
const { select, fields } = selectedFields(flags, KNOWN_DEFINITIONS, KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS, DEFAULT_DEFINITION_SHOW_SELECT, help, "definition");
|
|
351
|
+
const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
|
|
352
|
+
const full = flags.full === true;
|
|
353
|
+
const id = definitionId(flags, help);
|
|
354
|
+
const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
|
|
355
|
+
profile,
|
|
356
|
+
operation,
|
|
357
|
+
params: { "customSecurityAttributeDefinition-id": id },
|
|
358
|
+
query: { $select: select.join(",") },
|
|
359
|
+
scopes,
|
|
360
|
+
}));
|
|
361
|
+
const { row, truncated } = singleResult(raw, fields, full, "custom security attribute definition");
|
|
362
|
+
const helpHints = [
|
|
363
|
+
...(truncated ? [fullHint("entra custom-security-attribute-definition show", flags, profileName)] : []),
|
|
364
|
+
`mg-axi entra allowed-value list --definition ${shellValue(id)} ${profileHint(profileName)}`,
|
|
365
|
+
];
|
|
366
|
+
return { customSecurityAttributeDefinition: row, help: helpHints };
|
|
367
|
+
}
|
|
368
|
+
// The $count route returns a text/plain integer scalar rather than a JSON
|
|
369
|
+
// collection, so the leaf reads it through session.execute with scalar mode
|
|
370
|
+
// and accepts only a non-negative integer. --filter narrows the count
|
|
371
|
+
// server-side; there is no --select/--limit/--cursor contract on the count.
|
|
372
|
+
export async function countCustomSecurityAttributeDefinitions(session, flags, profile, operation, help, profileName) {
|
|
373
|
+
void profileName;
|
|
374
|
+
const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
|
|
375
|
+
const query = {};
|
|
376
|
+
if (flags.filter !== undefined)
|
|
377
|
+
query.$filter = String(flags.filter);
|
|
378
|
+
const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({ profile, operation, query, scopes, scalar: true }));
|
|
379
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
380
|
+
throw new AxiError("Graph returned a malformed definition count body", "GRAPH_ERROR", [
|
|
381
|
+
"Definition counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
|
|
382
|
+
]);
|
|
383
|
+
}
|
|
384
|
+
if (flags.filter !== undefined) {
|
|
385
|
+
return {
|
|
386
|
+
count: { returned: raw, complete: true },
|
|
387
|
+
help: [`Count reflects --filter ${shellValue(String(flags.filter))}; drop --filter for the tenant total`],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return { count: { returned: raw, complete: true } };
|
|
391
|
+
}
|
|
392
|
+
export async function listAllowedValues(session, flags, profile, operation, help, profileName) {
|
|
393
|
+
const common = collectionCommon(session, flags, KNOWN_ALLOWED_VALUES, KNOWN_ALLOWED_VALUE_FIELDS, DEFAULT_ALLOWED_VALUE_LIST_SELECT, "allowed-value", operation, help, profile, false);
|
|
394
|
+
const definition = definitionId(flags, help, "definition");
|
|
395
|
+
const result = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help, definitionParams(flags, help))));
|
|
396
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_ALLOWED_VALUE_LIST_SELECT.join(",") };
|
|
397
|
+
const values = [];
|
|
398
|
+
let truncated = false;
|
|
399
|
+
for (const row of result.value) {
|
|
400
|
+
const projected = project(row, common.fields, common.full);
|
|
401
|
+
values.push(projected.row);
|
|
402
|
+
truncated = truncated || projected.truncated;
|
|
403
|
+
}
|
|
404
|
+
const showHint = `mg-axi entra allowed-value show --definition ${shellValue(definition)} --id <value-id> ${profileHint(profileName)}`;
|
|
405
|
+
const filterNote = "Allowed-value lists offer no --filter: Graph documents $select only for allowedValues";
|
|
406
|
+
const truncationHints = truncated ? [fullHint("entra allowed-value list", effectiveFlags, profileName)] : [];
|
|
407
|
+
if (!result.complete) {
|
|
408
|
+
return {
|
|
409
|
+
allowedValues: values,
|
|
410
|
+
count: { returned: values.length, complete: false, reason: result.reason },
|
|
411
|
+
cursor: result.cursor,
|
|
412
|
+
help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const count = { returned: values.length, complete: true };
|
|
416
|
+
if (!values.length) {
|
|
417
|
+
return {
|
|
418
|
+
allowedValues: values,
|
|
419
|
+
count,
|
|
420
|
+
help: [filterNote, "0 allowed values matched; the definition may allow free-form values instead of predefined ones"],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return { allowedValues: values, count, help: [...truncationHints, showHint, filterNote] };
|
|
424
|
+
}
|
|
425
|
+
export async function showAllowedValue(session, flags, profile, operation, help, profileName) {
|
|
426
|
+
const { select, fields } = selectedFields(flags, KNOWN_ALLOWED_VALUES, KNOWN_ALLOWED_VALUE_FIELDS, DEFAULT_ALLOWED_VALUE_SHOW_SELECT, help, "allowed-value");
|
|
427
|
+
const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
|
|
428
|
+
const full = flags.full === true;
|
|
429
|
+
const params = { ...definitionParams(flags, help), "allowedValue-id": allowedValueId(flags, help) };
|
|
430
|
+
const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
|
|
431
|
+
profile,
|
|
432
|
+
operation,
|
|
433
|
+
params,
|
|
434
|
+
query: { $select: select.join(",") },
|
|
435
|
+
scopes,
|
|
436
|
+
}));
|
|
437
|
+
const { row, truncated } = singleResult(raw, fields, full, "allowed value");
|
|
438
|
+
if (truncated)
|
|
439
|
+
return { allowedValue: row, help: [fullHint("entra allowed-value show", flags, profileName)] };
|
|
440
|
+
return { allowedValue: row };
|
|
441
|
+
}
|
|
442
|
+
// The $count route returns a text/plain integer scalar rather than a JSON
|
|
443
|
+
// collection, so the leaf reads it through session.execute with scalar mode
|
|
444
|
+
// and accepts only a non-negative integer. There is no
|
|
445
|
+
// --filter/--select/--limit contract on the count: the catalogue declares
|
|
446
|
+
// no such flags and strict input validation refuses them before
|
|
447
|
+
// credentials.
|
|
448
|
+
export async function countAllowedValues(session, flags, profile, operation, help, profileName) {
|
|
449
|
+
void profileName;
|
|
450
|
+
const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
|
|
451
|
+
const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
|
|
452
|
+
profile,
|
|
453
|
+
operation,
|
|
454
|
+
params: definitionParams(flags, help),
|
|
455
|
+
scopes,
|
|
456
|
+
scalar: true,
|
|
457
|
+
}));
|
|
458
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
459
|
+
throw new AxiError("Graph returned a malformed allowed-value count body", "GRAPH_ERROR", [
|
|
460
|
+
"Allowed-value counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
|
|
461
|
+
]);
|
|
462
|
+
}
|
|
463
|
+
return { count: { returned: raw, complete: true } };
|
|
464
|
+
}
|