Skip to content

Commit 15c70d3

Browse files
authored
Adds Permissions section for entra user license commands. Closes #6953
1 parent 71e4ac5 commit 15c70d3

9 files changed

Lines changed: 84 additions & 29 deletions

docs/docs/cmd/entra/user/user-license-add.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ The user must have a `usageLocation` value in order to assign a license to it.
3535

3636
:::
3737

38+
## Permissions
39+
40+
<Tabs>
41+
<TabItem value="Delegated">
42+
43+
| Resource | Permissions |
44+
|-----------------|---------------------------------|
45+
| Microsoft Graph | LicenseAssignment.ReadWrite.All |
46+
47+
</TabItem>
48+
<TabItem value="Application">
49+
50+
| Resource | Permissions |
51+
|-----------------|---------------------------------|
52+
| Microsoft Graph | LicenseAssignment.ReadWrite.All |
53+
54+
</TabItem>
55+
</Tabs>
56+
3857
## Examples
3958

4059
Assign specific licenses to a specific user by UPN.

docs/docs/cmd/entra/user/user-license-list.mdx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,27 @@ m365 entra user license list [options]
2828

2929
:::tip
3030

31-
If you don't specify any option, the command will list the license details of the current logged in user. This does not work when using application permissions.
31+
If you don't specify any option, the command will list the license details of the current logged in user.
3232

3333
:::
3434

35+
## Permissions
36+
37+
<Tabs>
38+
<TabItem value="Delegated">
39+
40+
| Resource | Permissions |
41+
|-----------------|----------------------------|
42+
| Microsoft Graph | LicenseAssignment.Read.All |
43+
44+
</TabItem>
45+
<TabItem value="Application">
46+
47+
This command does not support application permissions.
48+
49+
</TabItem>
50+
</Tabs>
51+
3552
## Examples
3653

3754
List license details of the current logged in user.

docs/docs/cmd/entra/user/user-license-remove.mdx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import Global from '../../_global.mdx';
2+
import Tabs from '@theme/Tabs';
3+
import TabItem from '@theme/TabItem';
24

35
# entra user license remove
46

@@ -28,6 +30,25 @@ m365 entra user license remove [options]
2830

2931
<Global />
3032

33+
## Permissions
34+
35+
<Tabs>
36+
<TabItem value="Delegated">
37+
38+
| Resource | Permissions |
39+
|-----------------|---------------------------------|
40+
| Microsoft Graph | LicenseAssignment.ReadWrite.All |
41+
42+
</TabItem>
43+
<TabItem value="Application">
44+
45+
| Resource | Permissions |
46+
|-----------------|---------------------------------|
47+
| Microsoft Graph | LicenseAssignment.ReadWrite.All |
48+
49+
</TabItem>
50+
</Tabs>
51+
3152
## Examples
3253

3354
Remove specific licenses from a specific user by UPN.

src/m365/entra/commands/user/user-license-add.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import request from '../../../../request.js';
99
import { telemetry } from '../../../../telemetry.js';
1010
import { pid } from '../../../../utils/pid.js';
1111
import { session } from '../../../../utils/session.js';
12+
import { formatting } from '../../../../utils/formatting.js';
1213
import { sinonUtil } from '../../../../utils/sinonUtil.js';
1314
import commands from '../../commands.js';
1415
import command from './user-license-add.js';
@@ -107,7 +108,7 @@ describe(commands.USER_LICENSE_ADD, () => {
107108

108109
it('adds licenses to a user by userId', async () => {
109110
sinon.stub(request, 'post').callsFake(async opts => {
110-
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${validUserId}/assignLicense`)) {
111+
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(validUserId)}/assignLicense`)) {
111112
return userLicenseResponse;
112113
}
113114

@@ -120,7 +121,7 @@ describe(commands.USER_LICENSE_ADD, () => {
120121

121122
it('adds licenses to a user by userName', async () => {
122123
sinon.stub(request, 'post').callsFake(async opts => {
123-
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${validUserName}/assignLicense`)) {
124+
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(validUserName)}/assignLicense`)) {
124125
return userLicenseResponse;
125126
}
126127

src/m365/entra/commands/user/user-license-add.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Logger } from '../../../../cli/Logger.js';
22
import GlobalOptions from '../../../../GlobalOptions.js';
33
import request, { CliRequestOptions } from '../../../../request.js';
4+
import { formatting } from '../../../../utils/formatting.js';
45
import { validation } from '../../../../utils/validation.js';
56
import GraphCommand from '../../../base/GraphCommand.js';
67
import commands from '../../commands.js';
@@ -83,7 +84,7 @@ class EntraUserLicenseAddCommand extends GraphCommand {
8384
const requestBody = { "addLicenses": addLicenses, "removeLicenses": [] };
8485

8586
const requestOptions: CliRequestOptions = {
86-
url: `${this.resource}/v1.0/users/${args.options.userId || args.options.userName}/assignLicense`,
87+
url: `${this.resource}/v1.0/users/${formatting.encodeQueryParameter(args.options.userId || args.options.userName)}/assignLicense`,
8788
headers: {
8889
accept: 'application/json;odata.metadata=none'
8990
},

src/m365/entra/commands/user/user-license-list.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { telemetry } from '../../../../telemetry.js';
1010
import { accessToken } from '../../../../utils/accessToken.js';
1111
import { pid } from '../../../../utils/pid.js';
1212
import { session } from '../../../../utils/session.js';
13+
import { formatting } from '../../../../utils/formatting.js';
1314
import { sinonUtil } from '../../../../utils/sinonUtil.js';
1415
import commands from '../../commands.js';
1516
import command from './user-license-list.js';
@@ -51,6 +52,7 @@ describe(commands.USER_LICENSE_LIST, () => {
5152
let logger: Logger;
5253
let loggerLogSpy: sinon.SinonSpy;
5354
let commandInfo: CommandInfo;
55+
let assertAccessTokenTypeStub: sinon.SinonStub;
5456

5557
before(() => {
5658
sinon.stub(auth, 'restoreAuth').resolves();
@@ -79,13 +81,13 @@ describe(commands.USER_LICENSE_LIST, () => {
7981
}
8082
};
8183
loggerLogSpy = sinon.spy(logger, 'log');
82-
sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false);
84+
assertAccessTokenTypeStub = sinon.stub(accessToken, 'assertAccessTokenType').withArgs('delegated').resolves();
8385
});
8486

8587
afterEach(() => {
8688
sinonUtil.restore([
8789
request.get,
88-
accessToken.isAppOnlyAccessToken
90+
accessToken.assertAccessTokenType
8991
]);
9092
});
9193

@@ -126,13 +128,11 @@ describe(commands.USER_LICENSE_LIST, () => {
126128
assert.strictEqual(actual, true);
127129
});
128130

129-
it('throws an error when using application permissions and no option is specified', async () => {
130-
sinonUtil.restore(accessToken.isAppOnlyAccessToken);
131-
sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true);
131+
it('ensures delegated permissions are enforced', async () => {
132+
sinon.stub(request, 'get').resolves(licenseResponse);
132133

133-
await assert.rejects(command.action(logger, {
134-
options: {}
135-
}), new CommandError(`Specify at least 'userId' or 'userName' when using application permissions.`));
134+
await command.action(logger, { options: { userId: userId } });
135+
assert(assertAccessTokenTypeStub.calledOnceWithExactly('delegated'));
136136
});
137137

138138
it('retrieves license details of the current logged in user', async () => {
@@ -150,7 +150,7 @@ describe(commands.USER_LICENSE_LIST, () => {
150150

151151
it('retrieves license details of a specific user by its ID', async () => {
152152
sinon.stub(request, 'get').callsFake(async opts => {
153-
if (opts.url === `https://graph.microsoft.com/v1.0/users/${userId}/licenseDetails`) {
153+
if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userId)}/licenseDetails`) {
154154
return licenseResponse;
155155
}
156156

@@ -163,7 +163,7 @@ describe(commands.USER_LICENSE_LIST, () => {
163163

164164
it('retrieves license details of a specific user by its UPN', async () => {
165165
sinon.stub(request, 'get').callsFake(async opts => {
166-
if (opts.url === `https://graph.microsoft.com/v1.0/users/${userName}/licenseDetails`) {
166+
if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName)}/licenseDetails`) {
167167
return licenseResponse;
168168
}
169169

src/m365/entra/commands/user/user-license-list.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Logger } from '../../../../cli/Logger.js';
22
import GlobalOptions from '../../../../GlobalOptions.js';
3-
import { accessToken } from '../../../../utils/accessToken.js';
43
import { odata } from '../../../../utils/odata.js';
4+
import { formatting } from '../../../../utils/formatting.js';
55
import { validation } from '../../../../utils/validation.js';
6-
import GraphCommand from '../../../base/GraphCommand.js';
76
import commands from '../../commands.js';
8-
import auth from '../../../../Auth.js';
7+
import GraphDelegatedCommand from '../../../base/GraphDelegatedCommand.js';
98

109
interface CommandArgs {
1110
options: Options;
@@ -16,7 +15,7 @@ interface Options extends GlobalOptions {
1615
userName?: string;
1716
}
1817

19-
class EntraUserLicenseListCommand extends GraphCommand {
18+
class EntraUserLicenseListCommand extends GraphDelegatedCommand {
2019
public get name(): string {
2120
return commands.USER_LICENSE_LIST;
2221
}
@@ -82,18 +81,13 @@ class EntraUserLicenseListCommand extends GraphCommand {
8281
}
8382

8483
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
85-
const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[this.resource].accessToken);
86-
if (isAppOnlyAccessToken && !args.options.userId && !args.options.userName) {
87-
this.handleError(`Specify at least 'userId' or 'userName' when using application permissions.`);
88-
}
89-
9084
if (this.verbose) {
9185
await logger.logToStderr(`Retrieving licenses from user: ${args.options.userId || args.options.userName || 'current user'}.`);
9286
}
9387

9488
let requestUrl: string = `${this.resource}/v1.0/`;
9589
if (args.options.userId || args.options.userName) {
96-
requestUrl += `users/${args.options.userId || args.options.userName}`;
90+
requestUrl += `users/${formatting.encodeQueryParameter(args.options.userId || args.options.userName as string)}`;
9791
}
9892
else {
9993
requestUrl += 'me';

src/m365/entra/commands/user/user-license-remove.spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import request from '../../../../request.js';
99
import { telemetry } from '../../../../telemetry.js';
1010
import { pid } from '../../../../utils/pid.js';
1111
import { session } from '../../../../utils/session.js';
12+
import { formatting } from '../../../../utils/formatting.js';
1213
import { sinonUtil } from '../../../../utils/sinonUtil.js';
1314
import commands from '../../commands.js';
1415
import command from './user-license-remove.js';
@@ -140,7 +141,7 @@ describe(commands.USER_LICENSE_REMOVE, () => {
140141

141142
it('removes a single user license by userId without confirmation prompt', async () => {
142143
const postSpy = sinon.stub(request, 'post').callsFake(async opts => {
143-
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${validUserId}/assignLicense`)) {
144+
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(validUserId)}/assignLicense`)) {
144145
return;
145146
}
146147

@@ -153,7 +154,7 @@ describe(commands.USER_LICENSE_REMOVE, () => {
153154

154155
it('removes the specified user licenses by userName when prompt confirmed', async () => {
155156
const postSpy = sinon.stub(request, 'post').callsFake(async (opts) => {
156-
if (opts.url === `https://graph.microsoft.com/v1.0/users/${validUserName}/assignLicense`) {
157+
if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(validUserName)}/assignLicense`) {
157158
return;
158159
}
159160

@@ -173,7 +174,7 @@ describe(commands.USER_LICENSE_REMOVE, () => {
173174

174175
it('removes the specified user licenses by userId without confirmation prompt', async () => {
175176
const postSpy = sinon.stub(request, 'post').callsFake(async (opts) => {
176-
if (opts.url === `https://graph.microsoft.com/v1.0/users/${validUserId}/assignLicense`) {
177+
if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(validUserId)}/assignLicense`) {
177178
return;
178179
}
179180

@@ -196,7 +197,7 @@ describe(commands.USER_LICENSE_REMOVE, () => {
196197
};
197198

198199
sinon.stub(request, 'post').callsFake(async opts => {
199-
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${validUserId}/assignLicense`)) {
200+
if ((opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(validUserId)}/assignLicense`)) {
200201
throw error;
201202
}
202203

src/m365/entra/commands/user/user-license-remove.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import GlobalOptions from '../../../../GlobalOptions.js';
33
import commands from '../../commands.js';
44
import request, { CliRequestOptions } from '../../../../request.js';
55
import { validation } from '../../../../utils/validation.js';
6+
import { formatting } from '../../../../utils/formatting.js';
67
import { cli } from '../../../../cli/cli.js';
78
import GraphCommand from '../../../base/GraphCommand.js';
89

@@ -111,7 +112,7 @@ class EntraUserLicenseRemoveCommand extends GraphCommand {
111112
const requestBody = { "addLicenses": [], "removeLicenses": removeLicenses };
112113

113114
const requestOptions: CliRequestOptions = {
114-
url: `${this.resource}/v1.0/users/${args.options.userId || args.options.userName}/assignLicense`,
115+
url: `${this.resource}/v1.0/users/${formatting.encodeQueryParameter(args.options.userId || args.options.userName as string)}/assignLicense`,
115116
headers: {
116117
accept: 'application/json;odata.metadata=none'
117118
},

0 commit comments

Comments
 (0)