Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/docs/cmd/entra/user/user-license-add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ The user must have a `usageLocation` value in order to assign a license to it.

:::

## Permissions

<Tabs>
<TabItem value="Delegated">

| Resource | Permissions |
|-----------------|---------------------------------|
| Microsoft Graph | LicenseAssignment.ReadWrite.All |

</TabItem>
<TabItem value="Application">

| Resource | Permissions |
|-----------------|---------------------------------|
| Microsoft Graph | LicenseAssignment.ReadWrite.All |

</TabItem>
</Tabs>

## Examples

Assign specific licenses to a specific user by UPN.
Expand Down
19 changes: 18 additions & 1 deletion docs/docs/cmd/entra/user/user-license-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,27 @@ m365 entra user license list [options]

:::tip

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.
If you don't specify any option, the command will list the license details of the current logged in user.

:::

## Permissions

<Tabs>
<TabItem value="Delegated">

| Resource | Permissions |
|-----------------|----------------------------|
| Microsoft Graph | LicenseAssignment.Read.All |

</TabItem>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also display that application permissions are not supported here.

<TabItem value="Application">

This command does not support application permissions.

</TabItem>
</Tabs>

## Examples

List license details of the current logged in user.
Expand Down
21 changes: 21 additions & 0 deletions docs/docs/cmd/entra/user/user-license-remove.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Global from '../../_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# entra user license remove

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

<Global />

## Permissions

<Tabs>
<TabItem value="Delegated">

| Resource | Permissions |
|-----------------|---------------------------------|
| Microsoft Graph | LicenseAssignment.ReadWrite.All |

</TabItem>
<TabItem value="Application">

| Resource | Permissions |
|-----------------|---------------------------------|
| Microsoft Graph | LicenseAssignment.ReadWrite.All |

</TabItem>
</Tabs>

## Examples

Remove specific licenses from a specific user by UPN.
Expand Down
5 changes: 3 additions & 2 deletions src/m365/entra/commands/user/user-license-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { formatting } from '../../../../utils/formatting.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './user-license-add.js';
Expand Down Expand Up @@ -107,7 +108,7 @@ describe(commands.USER_LICENSE_ADD, () => {

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

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

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

Expand Down
3 changes: 2 additions & 1 deletion src/m365/entra/commands/user/user-license-add.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { validation } from '../../../../utils/validation.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';
Expand Down Expand Up @@ -83,7 +84,7 @@ class EntraUserLicenseAddCommand extends GraphCommand {
const requestBody = { "addLicenses": addLicenses, "removeLicenses": [] };

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/users/${args.options.userId || args.options.userName}/assignLicense`,
url: `${this.resource}/v1.0/users/${formatting.encodeQueryParameter(args.options.userId || args.options.userName)}/assignLicense`,
headers: {
accept: 'application/json;odata.metadata=none'
},
Expand Down
20 changes: 10 additions & 10 deletions src/m365/entra/commands/user/user-license-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { telemetry } from '../../../../telemetry.js';
import { accessToken } from '../../../../utils/accessToken.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { formatting } from '../../../../utils/formatting.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './user-license-list.js';
Expand Down Expand Up @@ -51,6 +52,7 @@ describe(commands.USER_LICENSE_LIST, () => {
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let assertAccessTokenTypeStub: sinon.SinonStub;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
Expand Down Expand Up @@ -79,13 +81,13 @@ describe(commands.USER_LICENSE_LIST, () => {
}
};
loggerLogSpy = sinon.spy(logger, 'log');
sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false);
assertAccessTokenTypeStub = sinon.stub(accessToken, 'assertAccessTokenType').withArgs('delegated').resolves();
});

afterEach(() => {
sinonUtil.restore([
request.get,
accessToken.isAppOnlyAccessToken
accessToken.assertAccessTokenType
]);
});

Expand Down Expand Up @@ -126,13 +128,11 @@ describe(commands.USER_LICENSE_LIST, () => {
assert.strictEqual(actual, true);
});

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

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

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

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

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

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

Expand Down
14 changes: 4 additions & 10 deletions src/m365/entra/commands/user/user-license-list.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import { accessToken } from '../../../../utils/accessToken.js';
import { odata } from '../../../../utils/odata.js';
import { formatting } from '../../../../utils/formatting.js';
import { validation } from '../../../../utils/validation.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';
import auth from '../../../../Auth.js';
import GraphDelegatedCommand from '../../../base/GraphDelegatedCommand.js';

interface CommandArgs {
options: Options;
Expand All @@ -16,7 +15,7 @@ interface Options extends GlobalOptions {
userName?: string;
}

class EntraUserLicenseListCommand extends GraphCommand {
class EntraUserLicenseListCommand extends GraphDelegatedCommand {
public get name(): string {
return commands.USER_LICENSE_LIST;
}
Expand Down Expand Up @@ -82,18 +81,13 @@ class EntraUserLicenseListCommand extends GraphCommand {
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[this.resource].accessToken);
if (isAppOnlyAccessToken && !args.options.userId && !args.options.userName) {
this.handleError(`Specify at least 'userId' or 'userName' when using application permissions.`);
}

if (this.verbose) {
await logger.logToStderr(`Retrieving licenses from user: ${args.options.userId || args.options.userName || 'current user'}.`);
}

let requestUrl: string = `${this.resource}/v1.0/`;
if (args.options.userId || args.options.userName) {
requestUrl += `users/${args.options.userId || args.options.userName}`;
requestUrl += `users/${formatting.encodeQueryParameter(args.options.userId || args.options.userName as string)}`;
}
else {
requestUrl += 'me';
Expand Down
9 changes: 5 additions & 4 deletions src/m365/entra/commands/user/user-license-remove.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { formatting } from '../../../../utils/formatting.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './user-license-remove.js';
Expand Down Expand Up @@ -140,7 +141,7 @@ describe(commands.USER_LICENSE_REMOVE, () => {

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

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

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

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

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

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

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

Expand Down
3 changes: 2 additions & 1 deletion src/m365/entra/commands/user/user-license-remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import GlobalOptions from '../../../../GlobalOptions.js';
import commands from '../../commands.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { validation } from '../../../../utils/validation.js';
import { formatting } from '../../../../utils/formatting.js';
import { cli } from '../../../../cli/cli.js';
import GraphCommand from '../../../base/GraphCommand.js';

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

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/users/${args.options.userId || args.options.userName}/assignLicense`,
url: `${this.resource}/v1.0/users/${formatting.encodeQueryParameter(args.options.userId || args.options.userName as string)}/assignLicense`,
headers: {
accept: 'application/json;odata.metadata=none'
},
Expand Down