@pnp/cli-microsoft365 5.7.0-beta.8be35f8 → 5.7.0-beta.9e8cf99

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.
@@ -27,7 +27,9 @@ RUN apt-get update && apt-get install -y \
27
27
  && apt-get install nodejs -y \
28
28
  && rm -rf /var/lib/apt/lists/*
29
29
 
30
- RUN pip3 install mkdocs-material==7.1.7 pymdown-extensions==9.0 pygments==2.11
30
+ COPY ../docs/pip_requirements.txt .
31
+
32
+ RUN pip install -r pip_requirements.txt
31
33
 
32
34
  RUN useradd \
33
35
  --user-group \
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "CLI for Microsoft 365",
3
3
  "dockerFile": "Dockerfile",
4
+ "context": "..",
4
5
  "settings": {
5
6
  "terminal.integrated.profiles.linux": {
6
7
  "zsh": {
@@ -27,6 +27,7 @@ class AadAppAddCommand extends GraphCommand_1.default {
27
27
  super();
28
28
  _AadAppAddCommand_instances.add(this);
29
29
  this.appName = '';
30
+ this.appPermissions = [];
30
31
  __classPrivateFieldGet(this, _AadAppAddCommand_instances, "m", _AadAppAddCommand_initTelemetry).call(this);
31
32
  __classPrivateFieldGet(this, _AadAppAddCommand_instances, "m", _AadAppAddCommand_initOptions).call(this);
32
33
  __classPrivateFieldGet(this, _AadAppAddCommand_instances, "m", _AadAppAddCommand_initValidators).call(this);
@@ -50,6 +51,7 @@ class AadAppAddCommand extends GraphCommand_1.default {
50
51
  return Promise.resolve(appInfo);
51
52
  })
52
53
  .then(appInfo => this.updateAppFromManifest(args, appInfo))
54
+ .then(appInfo => this.grantAdminConsent(appInfo, args.options.grantAdminConsent, logger))
53
55
  .then(appInfo => this.configureUri(args, appInfo, logger))
54
56
  .then(appInfo => this.configureSecret(args, appInfo, logger))
55
57
  .then(appInfo => this.saveAppInfo(args, appInfo, logger))
@@ -120,6 +122,81 @@ class AadAppAddCommand extends GraphCommand_1.default {
120
122
  return request_1.default.post(createApplicationRequestOptions);
121
123
  });
122
124
  }
125
+ grantAdminConsent(appInfo, adminConsent, logger) {
126
+ if (!adminConsent || this.appPermissions.length === 0) {
127
+ return Promise.resolve(appInfo);
128
+ }
129
+ return this.createServicePrincipal(appInfo.appId)
130
+ .then((sp) => {
131
+ if (this.debug) {
132
+ logger.logToStderr("Service principal created, returned object id: " + sp.id);
133
+ }
134
+ const tasks = [];
135
+ this.appPermissions.forEach(permission => {
136
+ if (permission.scope.length > 0) {
137
+ tasks.push(this.grantOAuth2Permission(sp.id, permission.resourceId, permission.scope.join(' ')));
138
+ if (this.debug) {
139
+ logger.logToStderr(`Admin consent granted for following resource ${permission.resourceId}, with delegated permissions: ${permission.scope.join(',')}`);
140
+ }
141
+ }
142
+ permission.resourceAccess.filter(access => access.type === "Role").forEach((access) => {
143
+ tasks.push(this.addRoleToServicePrincipal(sp.id, permission.resourceId, access.id));
144
+ if (this.debug) {
145
+ logger.logToStderr(`Admin consent granted for following resource ${permission.resourceId}, with application permission: ${access.id}`);
146
+ }
147
+ });
148
+ });
149
+ return Promise.all(tasks)
150
+ .then(_ => {
151
+ return appInfo;
152
+ });
153
+ });
154
+ }
155
+ addRoleToServicePrincipal(objectId, resourceId, appRoleId) {
156
+ const requestOptions = {
157
+ url: `${this.resource}/v1.0/myorganization/servicePrincipals/${objectId}/appRoleAssignments`,
158
+ headers: {
159
+ 'Content-Type': 'application/json'
160
+ },
161
+ responseType: 'json',
162
+ data: {
163
+ appRoleId: appRoleId,
164
+ principalId: objectId,
165
+ resourceId: resourceId
166
+ }
167
+ };
168
+ return request_1.default.post(requestOptions);
169
+ }
170
+ grantOAuth2Permission(appId, resourceId, scopeName) {
171
+ const grantAdminConsentApplicationRequestOptions = {
172
+ url: `${this.resource}/v1.0/myorganization/oauth2PermissionGrants`,
173
+ headers: {
174
+ accept: 'application/json;odata.metadata=none'
175
+ },
176
+ responseType: 'json',
177
+ data: {
178
+ clientId: appId,
179
+ consentType: "AllPrincipals",
180
+ principalId: null,
181
+ resourceId: resourceId,
182
+ scope: scopeName
183
+ }
184
+ };
185
+ return request_1.default.post(grantAdminConsentApplicationRequestOptions);
186
+ }
187
+ createServicePrincipal(appId) {
188
+ const requestOptions = {
189
+ url: `${this.resource}/v1.0/myorganization/servicePrincipals`,
190
+ headers: {
191
+ 'content-type': 'application/json'
192
+ },
193
+ data: {
194
+ appId: appId
195
+ },
196
+ responseType: 'json'
197
+ };
198
+ return request_1.default.post(requestOptions);
199
+ }
123
200
  updateAppFromManifest(args, appInfo) {
124
201
  if (!args.options.manifest) {
125
202
  return Promise.resolve(appInfo);
@@ -135,6 +212,11 @@ class AadAppAddCommand extends GraphCommand_1.default {
135
212
  // separately
136
213
  const secrets = this.getSecretsFromManifest(v2Manifest);
137
214
  // Azure Portal returns v2 manifest whereas the Graph API expects a v1.6
215
+ if (args.options.apisApplication || args.options.apisDelegated) {
216
+ // take submitted delegated / application permissions as options
217
+ // otherwise, they will be skipped in the app update
218
+ v2Manifest.requiredResourceAccess = appInfo.requiredResourceAccess;
219
+ }
138
220
  const graphManifest = this.transformManifest(v2Manifest);
139
221
  const updateAppRequestOptions = {
140
222
  url: `${this.resource}/v1.0/myorganization/applications/${appInfo.id}`,
@@ -334,36 +416,69 @@ class AadAppAddCommand extends GraphCommand_1.default {
334
416
  .then(_ => appInfo);
335
417
  }
336
418
  resolveApis(args, logger) {
337
- if (!args.options.apisDelegated && !args.options.apisApplication) {
419
+ var _a;
420
+ if (!args.options.apisDelegated && !args.options.apisApplication
421
+ && (typeof ((_a = this.manifest) === null || _a === void 0 ? void 0 : _a.requiredResourceAccess) === 'undefined' || this.manifest.requiredResourceAccess.length === 0)) {
338
422
  return Promise.resolve([]);
339
423
  }
340
424
  if (this.verbose) {
341
425
  logger.logToStderr('Resolving requested APIs...');
342
426
  }
343
427
  return utils_1.odata
344
- .getAllItems(`${this.resource}/v1.0/myorganization/servicePrincipals?$select=servicePrincipalNames,appId,oauth2PermissionScopes,appRoles`)
428
+ .getAllItems(`${this.resource}/v1.0/myorganization/servicePrincipals?$select=appId,appRoles,id,oauth2PermissionScopes,servicePrincipalNames`)
345
429
  .then(servicePrincipals => {
430
+ var _a;
431
+ let resolvedApis = [];
346
432
  try {
347
- const resolvedApis = this.getRequiredResourceAccessForApis(servicePrincipals, args.options.apisDelegated, 'Scope', logger);
348
- if (this.debug) {
349
- logger.logToStderr(`Resolved delegated permissions: ${JSON.stringify(resolvedApis, null, 2)}`);
350
- }
351
- const resolvedApplicationApis = this.getRequiredResourceAccessForApis(servicePrincipals, args.options.apisApplication, 'Role', logger);
352
- if (this.debug) {
353
- logger.logToStderr(`Resolved application permissions: ${JSON.stringify(resolvedApplicationApis, null, 2)}`);
354
- }
355
- // merge resolved application APIs onto resolved delegated APIs
356
- resolvedApplicationApis.forEach(resolvedRequiredResource => {
357
- const requiredResource = resolvedApis.find(api => api.resourceAppId === resolvedRequiredResource.resourceAppId);
358
- if (requiredResource) {
359
- requiredResource.resourceAccess.push(...resolvedRequiredResource.resourceAccess);
433
+ if (args.options.apisDelegated || args.options.apisApplication) {
434
+ resolvedApis = this.getRequiredResourceAccessForApis(servicePrincipals, args.options.apisDelegated, 'Scope', logger);
435
+ if (this.verbose) {
436
+ logger.logToStderr(`Resolved delegated permissions: ${JSON.stringify(resolvedApis, null, 2)}`);
360
437
  }
361
- else {
362
- resolvedApis.push(resolvedRequiredResource);
438
+ const resolvedApplicationApis = this.getRequiredResourceAccessForApis(servicePrincipals, args.options.apisApplication, 'Role', logger);
439
+ if (this.verbose) {
440
+ logger.logToStderr(`Resolved application permissions: ${JSON.stringify(resolvedApplicationApis, null, 2)}`);
363
441
  }
364
- });
365
- if (this.debug) {
442
+ // merge resolved application APIs onto resolved delegated APIs
443
+ resolvedApplicationApis.forEach(resolvedRequiredResource => {
444
+ const requiredResource = resolvedApis.find(api => api.resourceAppId === resolvedRequiredResource.resourceAppId);
445
+ if (requiredResource) {
446
+ requiredResource.resourceAccess.push(...resolvedRequiredResource.resourceAccess);
447
+ }
448
+ else {
449
+ resolvedApis.push(resolvedRequiredResource);
450
+ }
451
+ });
452
+ }
453
+ if (typeof ((_a = this.manifest) === null || _a === void 0 ? void 0 : _a.requiredResourceAccess) !== 'undefined' && this.manifest.requiredResourceAccess.length > 0) {
454
+ const manifestApis = this.manifest.requiredResourceAccess;
455
+ manifestApis.forEach(manifestApi => {
456
+ const requiredResource = resolvedApis.find(api => api.resourceAppId === manifestApi.resourceAppId);
457
+ if (requiredResource) {
458
+ // exclude if any duplicate required resources in both manifest and submitted options
459
+ requiredResource.resourceAccess.push(...manifestApi.resourceAccess.filter(manRes => !requiredResource.resourceAccess.some(res => res.id === manRes.id)));
460
+ }
461
+ else {
462
+ resolvedApis.push(manifestApi);
463
+ }
464
+ const app = servicePrincipals.find(servicePrincipals => servicePrincipals.appId === manifestApi.resourceAppId);
465
+ if (app) {
466
+ manifestApi.resourceAccess.forEach((res => {
467
+ var _a;
468
+ const resourceAccessPermission = {
469
+ id: res.id,
470
+ type: res.type
471
+ };
472
+ const oAuthValue = (_a = app.oauth2PermissionScopes.find(scp => scp.id === res.id)) === null || _a === void 0 ? void 0 : _a.value;
473
+ this.updateAppPermissions(app.id, resourceAccessPermission, oAuthValue);
474
+ }));
475
+ }
476
+ });
477
+ }
478
+ if (this.verbose) {
366
479
  logger.logToStderr(`Merged delegated and application permissions: ${JSON.stringify(resolvedApis, null, 2)}`);
480
+ logger.logToStderr(`App role assignments: ${JSON.stringify(this.appPermissions.flatMap(permission => permission.resourceAccess.filter(access => access.type === "Role")), null, 2)}`);
481
+ logger.logToStderr(`OAuth2 permissions: ${JSON.stringify(this.appPermissions.flatMap(permission => permission.scope), null, 2)}`);
367
482
  }
368
483
  return Promise.resolve(resolvedApis);
369
484
  }
@@ -405,13 +520,34 @@ class AadAppAddCommand extends GraphCommand_1.default {
405
520
  };
406
521
  resolvedApis.push(resolvedApi);
407
522
  }
408
- resolvedApi.resourceAccess.push({
523
+ const resourceAccessPermission = {
409
524
  id: permission.id,
410
525
  type: scopeType
411
- });
526
+ };
527
+ resolvedApi.resourceAccess.push(resourceAccessPermission);
528
+ this.updateAppPermissions(servicePrincipal.id, resourceAccessPermission, permission.value);
412
529
  });
413
530
  return resolvedApis;
414
531
  }
532
+ updateAppPermissions(spId, resourceAccessPermission, oAuth2PermissionValue) {
533
+ // During API resolution, we store globally both app role assignments and oauth2permissions
534
+ // So that we'll be able to parse them during the admin consent process
535
+ let existingPermission = this.appPermissions.find(oauth => oauth.resourceId === spId);
536
+ if (!existingPermission) {
537
+ existingPermission = {
538
+ resourceId: spId,
539
+ resourceAccess: [],
540
+ scope: []
541
+ };
542
+ this.appPermissions.push(existingPermission);
543
+ }
544
+ if (resourceAccessPermission.type === 'Scope' && oAuth2PermissionValue && !existingPermission.scope.find(scp => scp === oAuth2PermissionValue)) {
545
+ existingPermission.scope.push(oAuth2PermissionValue);
546
+ }
547
+ if (!existingPermission.resourceAccess.find(res => res.id === resourceAccessPermission.id)) {
548
+ existingPermission.resourceAccess.push(resourceAccessPermission);
549
+ }
550
+ }
415
551
  configureSecret(args, appInfo, logger) {
416
552
  if (!args.options.withSecret) {
417
553
  return Promise.resolve(appInfo);
@@ -523,7 +659,8 @@ _AadAppAddCommand_instances = new WeakSet(), _AadAppAddCommand_initTelemetry = f
523
659
  withSecret: args.options.withSecret,
524
660
  certificateFile: typeof args.options.certificateFile !== 'undefined',
525
661
  certificateBase64Encoded: typeof args.options.certificateBase64Encoded !== 'undefined',
526
- certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined'
662
+ certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined',
663
+ grantAdminConsent: typeof args.options.grantAdminConsent !== 'undefined'
527
664
  });
528
665
  });
529
666
  }, _AadAppAddCommand_initOptions = function _AadAppAddCommand_initOptions() {
@@ -565,6 +702,8 @@ _AadAppAddCommand_instances = new WeakSet(), _AadAppAddCommand_initTelemetry = f
565
702
  option: '--manifest [manifest]'
566
703
  }, {
567
704
  option: '--save'
705
+ }, {
706
+ option: '--grantAdminConsent'
568
707
  });
569
708
  }, _AadAppAddCommand_initValidators = function _AadAppAddCommand_initValidators() {
570
709
  this.validators.push((args) => __awaiter(this, void 0, void 0, function* () {
@@ -58,6 +58,9 @@ m365 aad app add [options]
58
58
  `--certificateDisplayName [certificateDisplayName]`
59
59
  : Display name for the certificate. If not given, the displayName will be set to the certificate subject. When specified, also specify either `certificateFile` or `certificateBase64Encoded`
60
60
 
61
+ `--grantAdminConsent`
62
+ : When specified, grants application & delegated permissions through admin consent
63
+
61
64
  `--manifest [manifest]`
62
65
  : Azure AD app manifest as retrieved from the Azure Portal to create the app registration from
63
66
 
@@ -94,6 +97,8 @@ After creating the Azure AD app registration, this command returns the app ID an
94
97
 
95
98
  If you want to store the information about the created Azure AD app registration, use the `--save` option. This is useful when you build solutions connected to Microsoft 365 and want to easily manage app registrations used with your solution. When you use the `--save` option, after you create the app registration, the command will write its ID and name to the `.m365rc.json` file in the current directory. If the file already exists, it will add the information about the to it, allowing you to track multiple apps. If the file doesn't exist, the command will create it.
96
99
 
100
+ When specifying `--grantAdminConsent` option, a service principal will be created for the app registration.
101
+
97
102
  ## Examples
98
103
 
99
104
  Create new Azure AD app registration with the specified name
@@ -156,6 +161,12 @@ Create new Azure AD app registration with Application ID URI set to a value that
156
161
  m365 aad app add --name 'My AAD app' --uri api://caf406b91cd4.ngrok.io/_appId_ --scopeName access_as_user --scopeAdminConsentDescription 'Access as a user' --scopeAdminConsentDisplayName 'Access as a user' --scopeConsentBy adminsAndUsers
157
162
  ```
158
163
 
164
+ Create new Azure AD app registration for a deamon app with specified Microsoft Graph application permissions, including admin consent
165
+
166
+ ```sh
167
+ m365 aad app add --name 'My AAD app' --apisApplication 'https://graph.microsoft.com/Group.ReadWrite.All' --grantAdminConsent
168
+ ```
169
+
159
170
  Create new Azure AD app registration with the specified name. Store information about the created app registration in the _.m365rc.json_ file in the current directory.
160
171
 
161
172
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnp/cli-microsoft365",
3
- "version": "5.7.0-beta.8be35f8",
3
+ "version": "5.7.0-beta.9e8cf99",
4
4
  "description": "Manage Microsoft 365 and SharePoint Framework projects on any platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/api.js",