diff --git a/docs/docs/cmd/entra/user/user-license-add.mdx b/docs/docs/cmd/entra/user/user-license-add.mdx
index c46d0902802..67f52096129 100644
--- a/docs/docs/cmd/entra/user/user-license-add.mdx
+++ b/docs/docs/cmd/entra/user/user-license-add.mdx
@@ -35,6 +35,25 @@ The user must have a `usageLocation` value in order to assign a license to it.
:::
+## Permissions
+
+
+
+
+ | Resource | Permissions |
+ |-----------------|---------------------------------|
+ | Microsoft Graph | LicenseAssignment.ReadWrite.All |
+
+
+
+
+ | Resource | Permissions |
+ |-----------------|---------------------------------|
+ | Microsoft Graph | LicenseAssignment.ReadWrite.All |
+
+
+
+
## Examples
Assign specific licenses to a specific user by UPN.
diff --git a/docs/docs/cmd/entra/user/user-license-list.mdx b/docs/docs/cmd/entra/user/user-license-list.mdx
index a663f9fe694..4cb15838e8a 100644
--- a/docs/docs/cmd/entra/user/user-license-list.mdx
+++ b/docs/docs/cmd/entra/user/user-license-list.mdx
@@ -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
+
+
+
+
+ | Resource | Permissions |
+ |-----------------|----------------------------|
+ | Microsoft Graph | LicenseAssignment.Read.All |
+
+
+
+
+ This command does not support application permissions.
+
+
+
+
## Examples
List license details of the current logged in user.
diff --git a/docs/docs/cmd/entra/user/user-license-remove.mdx b/docs/docs/cmd/entra/user/user-license-remove.mdx
index 86f15dcd705..faef4e0ff08 100644
--- a/docs/docs/cmd/entra/user/user-license-remove.mdx
+++ b/docs/docs/cmd/entra/user/user-license-remove.mdx
@@ -1,4 +1,6 @@
import Global from '../../_global.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
# entra user license remove
@@ -28,6 +30,25 @@ m365 entra user license remove [options]
+## Permissions
+
+
+
+
+ | Resource | Permissions |
+ |-----------------|---------------------------------|
+ | Microsoft Graph | LicenseAssignment.ReadWrite.All |
+
+
+
+
+ | Resource | Permissions |
+ |-----------------|---------------------------------|
+ | Microsoft Graph | LicenseAssignment.ReadWrite.All |
+
+
+
+
## Examples
Remove specific licenses from a specific user by UPN.
diff --git a/src/m365/entra/commands/user/user-license-add.spec.ts b/src/m365/entra/commands/user/user-license-add.spec.ts
index 54e73f7f877..511cdfe61b3 100644
--- a/src/m365/entra/commands/user/user-license-add.spec.ts
+++ b/src/m365/entra/commands/user/user-license-add.spec.ts
@@ -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';
@@ -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;
}
@@ -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;
}
diff --git a/src/m365/entra/commands/user/user-license-add.ts b/src/m365/entra/commands/user/user-license-add.ts
index 9a8ed9aecc7..19767800b39 100644
--- a/src/m365/entra/commands/user/user-license-add.ts
+++ b/src/m365/entra/commands/user/user-license-add.ts
@@ -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';
@@ -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'
},
diff --git a/src/m365/entra/commands/user/user-license-list.spec.ts b/src/m365/entra/commands/user/user-license-list.spec.ts
index c449779eb2c..5cf4f07e801 100644
--- a/src/m365/entra/commands/user/user-license-list.spec.ts
+++ b/src/m365/entra/commands/user/user-license-list.spec.ts
@@ -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';
@@ -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();
@@ -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
]);
});
@@ -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 () => {
@@ -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;
}
@@ -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;
}
diff --git a/src/m365/entra/commands/user/user-license-list.ts b/src/m365/entra/commands/user/user-license-list.ts
index bcb2a8d09de..546e20d403f 100644
--- a/src/m365/entra/commands/user/user-license-list.ts
+++ b/src/m365/entra/commands/user/user-license-list.ts
@@ -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;
@@ -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;
}
@@ -82,18 +81,13 @@ class EntraUserLicenseListCommand extends GraphCommand {
}
public async commandAction(logger: Logger, args: CommandArgs): Promise {
- 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';
diff --git a/src/m365/entra/commands/user/user-license-remove.spec.ts b/src/m365/entra/commands/user/user-license-remove.spec.ts
index f2cd05ce5c8..e5b457e3b88 100644
--- a/src/m365/entra/commands/user/user-license-remove.spec.ts
+++ b/src/m365/entra/commands/user/user-license-remove.spec.ts
@@ -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';
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
diff --git a/src/m365/entra/commands/user/user-license-remove.ts b/src/m365/entra/commands/user/user-license-remove.ts
index 9afcbfcb811..4511ed7c1dc 100644
--- a/src/m365/entra/commands/user/user-license-remove.ts
+++ b/src/m365/entra/commands/user/user-license-remove.ts
@@ -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';
@@ -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'
},