diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 4ed406d88a..5416cd98fe 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -13,6 +13,7 @@ const dictionary = [
'approve',
'assessment',
'assets',
+ 'assignment',
'audit',
'bin',
'builder',
diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-roleassignment-add.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-roleassignment-add.mdx
new file mode 100644
index 0000000000..1a3e146d83
--- /dev/null
+++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-roleassignment-add.mdx
@@ -0,0 +1,116 @@
+import Global from '/docs/cmd/_global.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# entra administrativeunit roleassignment add
+
+Assigns a Microsoft Entra role with administrative unit scope to a user
+
+## Usage
+
+```sh
+m365 entra administrativeunit roleassignment add [options]
+```
+
+## Options
+
+```md definition-list
+`-i, --administrativeUnitId [administrativeUnitId]`
+: The id of the administrative unit. Specify either `administrativeUnitId` or `administrativeUnitName`.
+
+`-n, --administrativeUnitName [administrativeUnitName]`
+: The name of the administrative unit. Specify either `administrativeUnitId` or `administrativeUnitName`.
+
+`--roleDefinitionId [roleDefinitionId]`
+: The id of the role definition that the member is in. Specify either `roleDefinitionId` or `roleDefinitionName`.
+
+`--roleDefinitionName [roleDefinitionName]`
+: The name of the role definition that the member is in. Specify either `roleDefinitionId` or `roleDefinitionName`.
+
+`--userId [userId]`
+: The id of the user that is a member of the scoped role. Specify either `userId` or `userName`.
+
+`--userName [userName]`
+: The name of the user that is a member of the scoped role. Specify either `userId` or `userName`.
+```
+
+
+
+## Remarks
+
+:::info
+
+To use this command you must be either **Global Administrator** or **Privileged Role Administrator**.
+
+:::
+
+## Examples
+
+Assign a role definition specified by id to a user specified by id for an administrative unit specified by id
+
+```sh
+m365 entra administrativeunit roleassignment add --administrativeUnitId 81bb36e4-f4c6-4984-8e56-d4f8feae9e09 --roleDefinitionId 4d6ac14f-3453-41d0-bef9-a3e0c569773a --userId 5f91f951-7305-4a27-9b63-7b00906de09f
+```
+
+Assign a role definition specified by name to a user specified by name for an administrative unit specified by name
+
+```sh
+m365 entra administrativeunit roleassignment add --administrativeUnitName 'Marketing Division' --roleDefinitionName 'License Administrator' --userName 'john.doe@contoso.com'
+```
+
+## Response
+
+
+
+
+ ```json
+ {
+ "id": "5wuT_mJe20eRr5jDpJo4sVH5kV8FcydKm2N7AJBt4J_kNruBxvSESY5W1Pj-rp4J-2",
+ "principalId": "5f91f951-7305-4a27-9b63-7b00906de09f",
+ "directoryScopeId": "/administrativeUnits/81bb36e4-f4c6-4984-8e56-d4f8feae9e09",
+ "roleDefinitionId": "4d6ac14f-3453-41d0-bef9-a3e0c569773a"
+ }
+ ```
+
+
+
+
+ ```text
+ directoryScopeId: /administrativeUnits/81bb36e4-f4c6-4984-8e56-d4f8feae9e09
+ id : 4yeYchSc90m7G5YI8Va7uFH5kV8FcydKm2N7AJBt4J_kNruBxvSESY5W1Pj-rp4J-2
+ principalId : 5f91f951-7305-4a27-9b63-7b00906de09f
+ roleDefinitionId: 4d6ac14f-3453-41d0-bef9-a3e0c569773a
+ ```
+
+
+
+
+ ```csv
+ id,principalId,directoryScopeId,roleDefinitionId
+ UB-K8uf2cUWBi2oS8q9rbFH5kV8FcydKm2N7AJBt4J_kNruBxvSESY5W1Pj-rp4J-2,5f91f951-7305-4a27-9b63-7b00906de09f,/administrativeUnits/81bb36e4-f4c6-4984-8e56-d4f8feae9e09,4d6ac14f-3453-41d0-bef9-a3e0c569773a
+ ```
+
+
+
+
+ ```md
+ # entra administrativeunit roleassignment add --administrativeUnitId "81bb36e4-f4c6-4984-8e56-d4f8feae9e09" --roleDefinitionId "4d6ac14f-3453-41d0-bef9-a3e0c569773a" --userId "5f91f951-7305-4a27-9b63-7b00906de09f"
+
+ Date: 11/16/2023
+
+ ## T8FqTVM00EG--aPgxWl3OlH5kV8FcydKm2N7AJBt4J_kNruBxvSESY5W1Pj-rp4J-2
+
+ Property | Value
+ ---------|-------
+ id | T8FqTVM00EG--aPgxWl3OlH5kV8FcydKm2N7AJBt4J\_kNruBxvSESY5W1Pj-rp4J-2
+ principalId | 5f91f951-7305-4a27-9b63-7b00906de09f
+ directoryScopeId | /administrativeUnits/81bb36e4-f4c6-4984-8e56-d4f8feae9e09
+ roleDefinitionId | 4d6ac14f-3453-41d0-bef9-a3e0c569773a
+ ```
+
+
+
+
+## More information
+
+- [Roles with administrative unit scope](https://learn.microsoft.com/entra/identity/role-based-access-control/admin-units-assign-roles#roles-that-can-be-assigned-with-administrative-unit-scope)
diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts
index f31bd11876..b30f19b6bf 100644
--- a/docs/src/config/sidebars.ts
+++ b/docs/src/config/sidebars.ts
@@ -198,6 +198,11 @@ const sidebars: SidebarsConfig = {
type: 'doc',
label: 'administrativeunit member list',
id: 'cmd/entra/administrativeunit/administrativeunit-member-list'
+ },
+ {
+ type: 'doc',
+ label: 'administrativeunit roleassignment add',
+ id: 'cmd/entra/administrativeunit/administrativeunit-roleassignment-add'
}
]
},
diff --git a/src/m365/entra/aadCommands.ts b/src/m365/entra/aadCommands.ts
index 492da3b946..8667fe0e4d 100644
--- a/src/m365/entra/aadCommands.ts
+++ b/src/m365/entra/aadCommands.ts
@@ -8,6 +8,7 @@ export default {
ADMINISTRATIVEUNIT_MEMBER_ADD: `${prefix} administrativeunit member add`,
ADMINISTRATIVEUNIT_MEMBER_GET: `${prefix} administrativeunit member get`,
ADMINISTRATIVEUNIT_MEMBER_LIST: `${prefix} administrativeunit member list`,
+ ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD: `${prefix} administrativeunit roleassignment add`,
APP_ADD: `${prefix} app add`,
APP_GET: `${prefix} app get`,
APP_LIST: `${prefix} app list`,
diff --git a/src/m365/entra/commands.ts b/src/m365/entra/commands.ts
index 486a7d044d..3c79e1916a 100644
--- a/src/m365/entra/commands.ts
+++ b/src/m365/entra/commands.ts
@@ -8,6 +8,7 @@ export default {
ADMINISTRATIVEUNIT_MEMBER_ADD: `${prefix} administrativeunit member add`,
ADMINISTRATIVEUNIT_MEMBER_GET: `${prefix} administrativeunit member get`,
ADMINISTRATIVEUNIT_MEMBER_LIST: `${prefix} administrativeunit member list`,
+ ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD: `${prefix} administrativeunit roleassignment add`,
APP_ADD: `${prefix} app add`,
APP_GET: `${prefix} app get`,
APP_LIST: `${prefix} app list`,
diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.spec.ts
new file mode 100644
index 0000000000..6d44815b9c
--- /dev/null
+++ b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.spec.ts
@@ -0,0 +1,369 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import auth from '../../../../Auth.js';
+import { cli } from '../../../../cli/cli.js';
+import { CommandInfo } from '../../../../cli/CommandInfo.js';
+import commands from '../../commands.js';
+import command from './administrativeunit-roleassignment-add.js';
+import { telemetry } from '../../../../telemetry.js';
+import { pid } from '../../../../utils/pid.js';
+import { session } from '../../../../utils/session.js';
+import { sinonUtil } from '../../../../utils/sinonUtil.js';
+import { Logger } from '../../../../cli/Logger.js';
+import { CommandError } from '../../../../Command.js';
+import { aadAdministrativeUnit } from '../../../../utils/aadAdministrativeUnit.js';
+import { aadUser } from '../../../../utils/aadUser.js';
+import { roleAssignment } from '../../../../utils/roleAssignment.js';
+import { roleDefinition } from '../../../../utils/roleDefinition.js';
+import { settingsNames } from '../../../../settingsNames.js';
+import request from '../../../../request.js';
+
+describe(commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD, () => {
+ const roleDefinitionId = 'fe930be7-5e62-47db-91af-98c3a49a38b1';
+ const roleDefinitionName = 'User Administrator';
+ const userId = '2056d2f6-3257-4253-8cfc-b73393e414e5';
+ const userName = 'AdeleVance@contoso.com';
+ const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab';
+ const administrativeUnitName = 'Marketing Department';
+ const unifiedRoleAssignment = {
+ "id": "BH21sHQtUEyvox7IA_Eu_mm3jqnUe4lEhvatluHIWb7-1",
+ "roleDefinitionId": "fe930be7-5e62-47db-91af-98c3a49a38b1",
+ "principalId": "2056d2f6-3257-4253-8cfc-b73393e414e5",
+ "directoryScopeId": "/administrativeUnits/fc33aa61-cf0e-46b6-9506-f633347202ab"
+ };
+
+ let log: string[];
+ let logger: Logger;
+ let loggerLogSpy: sinon.SinonSpy;
+ let commandInfo: CommandInfo;
+
+ before(() => {
+ sinon.stub(auth, 'restoreAuth').resolves();
+ sinon.stub(telemetry, 'trackEvent').returns();
+ sinon.stub(pid, 'getProcessName').returns('');
+ sinon.stub(session, 'getId').returns('');
+ auth.service.connected = true;
+ commandInfo = cli.getCommandInfo(command);
+ });
+
+ beforeEach(() => {
+ log = [];
+ logger = {
+ log: async (msg: string) => {
+ log.push(msg);
+ },
+ logRaw: async (msg: string) => {
+ log.push(msg);
+ },
+ logToStderr: async (msg: string) => {
+ log.push(msg);
+ }
+ };
+ loggerLogSpy = sinon.spy(logger, 'log');
+ });
+
+ afterEach(() => {
+ sinonUtil.restore([
+ aadAdministrativeUnit.getAdministrativeUnitByDisplayName,
+ aadUser.getUserIdByUpn,
+ roleAssignment.createRoleAssignmentWithAdministrativeUnitScope,
+ roleDefinition.getRoleDefinitionByDisplayName,
+ cli.getSettingWithDefaultValue,
+ request.post
+ ]);
+ });
+
+ after(() => {
+ sinon.restore();
+ auth.service.connected = false;
+ });
+
+ it('has correct name', () => {
+ assert.strictEqual(command.name, commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD);
+ });
+
+ it('has a description', () => {
+ assert.notStrictEqual(command.description, null);
+ });
+
+ it('passes validation if administrative unit id, role definition id and user id are passed', async () => {
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: roleDefinitionId,
+ userId: userId
+ }
+ }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('passes validation if administrative unit name, role definition name and user name are passed', async () => {
+ const actual = await command.validate({
+ options: {
+ administrativeUnitName: administrativeUnitName,
+ roleDefinitionName: roleDefinitionName,
+ userName: userName
+ }
+ }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('fails validation if both user id and user name are not passed', async () => {
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
+ if (settingName === settingsNames.prompt) {
+ return false;
+ }
+
+ return defaultValue;
+ });
+
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: roleDefinitionId
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if both user id and user name are passed', async () => {
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
+ if (settingName === settingsNames.prompt) {
+ return false;
+ }
+
+ return defaultValue;
+ });
+
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: roleDefinitionId,
+ userId: userId,
+ userName: userName
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if both role definition id and role definition name are not passed', async () => {
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
+ if (settingName === settingsNames.prompt) {
+ return false;
+ }
+
+ return defaultValue;
+ });
+
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ userId: userId
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if both role definition id and role definition name are passed', async () => {
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
+ if (settingName === settingsNames.prompt) {
+ return false;
+ }
+
+ return defaultValue;
+ });
+
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: roleDefinitionId,
+ roleDefinitionName: roleDefinitionName,
+ userId: userId
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if both administrative unit id and administrative unit name are not passed', async () => {
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
+ if (settingName === settingsNames.prompt) {
+ return false;
+ }
+
+ return defaultValue;
+ });
+
+ const actual = await command.validate({
+ options: {
+ roleDefinitionId: roleDefinitionId,
+ userId: userId
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if both administrative unit id and administrative unit name are passed', async () => {
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
+ if (settingName === settingsNames.prompt) {
+ return false;
+ }
+
+ return defaultValue;
+ });
+
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ administrativeUnitName: administrativeUnitName,
+ roleDefinitionId: roleDefinitionId,
+ userId: userId
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if administrative unit id is not a valid GUID', async () => {
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: '123',
+ roleDefinitionId: roleDefinitionId,
+ userId: userId
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if role definition id is not a valid GUID', async () => {
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: '123',
+ userId: userId
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if user id is not a valid GUID', async () => {
+ const actual = await command.validate({
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: roleDefinitionId,
+ userId: '123'
+ }
+ }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('correctly assigns a role specified by id to and administrative unit specified by id and to a user specified by id', async () => {
+ sinon.stub(roleAssignment, 'createRoleAssignmentWithAdministrativeUnitScope').withArgs(roleDefinitionId, userId, administrativeUnitId).resolves(unifiedRoleAssignment);
+
+ await command.action(logger, {
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: roleDefinitionId,
+ userId: userId
+ }
+ });
+
+ assert(loggerLogSpy.calledOnceWithExactly(unifiedRoleAssignment));
+ });
+
+ it('correctly assigns a role specified by name to and administrative unit specified by name and to a user specified by name (verbose)', async () => {
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).resolves({ id: administrativeUnitId, displayName: administrativeUnitName });
+ sinon.stub(aadUser, 'getUserIdByUpn').withArgs(userName).resolves(userId);
+ sinon.stub(roleDefinition, 'getRoleDefinitionByDisplayName').withArgs(roleDefinitionName).resolves({ id: roleDefinitionId, displayName: roleDefinitionName });
+ sinon.stub(roleAssignment, 'createRoleAssignmentWithAdministrativeUnitScope').withArgs(roleDefinitionId, userId, administrativeUnitId).resolves(unifiedRoleAssignment);
+
+ await command.action(logger, {
+ options: {
+ administrativeUnitName: administrativeUnitName,
+ roleDefinitionName: roleDefinitionName,
+ userName: userName,
+ verbose: true
+ }
+ });
+ assert(loggerLogSpy.calledOnceWithExactly(unifiedRoleAssignment));
+ });
+
+ it('correctly handles error', async () => {
+ sinon.stub(roleAssignment, 'createRoleAssignmentWithAdministrativeUnitScope').throws(Error('Invalid request'));
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ administrativeUnitId: administrativeUnitId,
+ roleDefinitionId: roleDefinitionId,
+ userId: userId
+ }
+ }), new CommandError('Invalid request'));
+ });
+
+ it('fails if an administrative unit specified by name was not found', async () => {
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).throws(Error("The specified administrative unit 'Marketing Department' does not exist."));
+ sinon.stub(aadUser, 'getUserIdByUpn').withArgs(userName).resolves(userId);
+ sinon.stub(roleDefinition, 'getRoleDefinitionByDisplayName').withArgs(roleDefinitionName).resolves({ id: roleDefinitionId, displayName: roleDefinitionName });
+ sinon.stub(roleAssignment, 'createRoleAssignmentWithAdministrativeUnitScope').withArgs(roleDefinitionId, userId, administrativeUnitId).resolves(unifiedRoleAssignment);
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ administrativeUnitName: administrativeUnitName,
+ roleDefinitionName: roleDefinitionName,
+ userName: userName
+ }
+ }), new CommandError("The specified administrative unit 'Marketing Department' does not exist."));
+ });
+
+ it('fails if a role definition specified by name was not found', async () => {
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).resolves({ id: administrativeUnitId, displayName: administrativeUnitName });
+ sinon.stub(aadUser, 'getUserIdByUpn').withArgs(userName).resolves(userId);
+ sinon.stub(roleDefinition, 'getRoleDefinitionByDisplayName').withArgs(roleDefinitionName).throws(Error("The specified role definition 'User Administrator' does not exist."));
+ sinon.stub(roleAssignment, 'createRoleAssignmentWithAdministrativeUnitScope').withArgs(roleDefinitionId, userId, administrativeUnitId).resolves(unifiedRoleAssignment);
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ administrativeUnitName: administrativeUnitName,
+ roleDefinitionName: roleDefinitionName,
+ userName: userName
+ }
+ }), new CommandError("The specified role definition 'User Administrator' does not exist."));
+ });
+
+ it('fails if a user specified by UPN was not found', async () => {
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).resolves({ id: administrativeUnitId, displayName: administrativeUnitName });
+ sinon.stub(aadUser, 'getUserIdByUpn').withArgs(userName).throws(Error("The specified user with user name AdeleVance@contoso.com does not exist."));
+ sinon.stub(roleDefinition, 'getRoleDefinitionByDisplayName').withArgs(roleDefinitionName).resolves({ id: roleDefinitionId, displayName: roleDefinitionName });
+ sinon.stub(roleAssignment, 'createRoleAssignmentWithAdministrativeUnitScope').withArgs(roleDefinitionId, userId, administrativeUnitId).resolves(unifiedRoleAssignment);
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ administrativeUnitName: administrativeUnitName,
+ roleDefinitionName: roleDefinitionName,
+ userName: userName
+ }
+ }), new CommandError("The specified user with user name AdeleVance@contoso.com does not exist."));
+ });
+
+ it('correctly handles API OData error when creating role assignment with an administrative unit scope failed', async () => {
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).resolves({ id: administrativeUnitId, displayName: administrativeUnitName });
+ sinon.stub(aadUser, 'getUserIdByUpn').withArgs(userName).resolves(userId);
+ sinon.stub(roleDefinition, 'getRoleDefinitionByDisplayName').withArgs(roleDefinitionName).resolves({ id: roleDefinitionId, displayName: roleDefinitionName });
+ sinon.stub(request, 'post').rejects({
+ error: {
+ 'odata.error': {
+ code: '-1, InvalidOperationException',
+ message: {
+ value: 'Invalid request'
+ }
+ }
+ }
+ });
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ administrativeUnitName: administrativeUnitName,
+ roleDefinitionName: roleDefinitionName,
+ userName: userName
+ }
+ }), new CommandError("Invalid request"));
+ });
+});
\ No newline at end of file
diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.ts
new file mode 100644
index 0000000000..d95af45a78
--- /dev/null
+++ b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.ts
@@ -0,0 +1,142 @@
+import GlobalOptions from '../../../../GlobalOptions.js';
+import { Logger } from '../../../../cli/Logger.js';
+import { aadAdministrativeUnit } from '../../../../utils/aadAdministrativeUnit.js';
+import { aadUser } from '../../../../utils/aadUser.js';
+import { roleAssignment } from '../../../../utils/roleAssignment.js';
+import { roleDefinition } from '../../../../utils/roleDefinition.js';
+import { validation } from '../../../../utils/validation.js';
+import GraphCommand from '../../../base/GraphCommand.js';
+import commands from '../../commands.js';
+
+interface CommandArgs {
+ options: Options;
+}
+
+interface Options extends GlobalOptions {
+ administrativeUnitId?: string;
+ administrativeUnitName?: string;
+ roleDefinitionId?: string;
+ roleDefinitionName?: string;
+ userId?: string;
+ userName?: string;
+}
+
+class EntraAdministrativeUnitRoleAssignmentAddCommand extends GraphCommand {
+ public get name(): string {
+ return commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD;
+ }
+
+ public get description(): string {
+ return 'Assigns a Microsoft Entra role with administrative unit scope to a user';
+ }
+
+ constructor() {
+ super();
+
+ this.#initTelemetry();
+ this.#initOptions();
+ this.#initValidators();
+ this.#initOptionSets();
+ }
+
+ #initTelemetry(): void {
+ this.telemetry.push((args: CommandArgs) => {
+ Object.assign(this.telemetryProperties, {
+ administrativeUnitId: typeof args.options.administrativeUnitId !== 'undefined',
+ administrativeUnitName: typeof args.options.administrativeUnitName !== 'undefined',
+ roleDefinitionId: typeof args.options.roleDefinitionId !== 'undefined',
+ roleDefinitionName: typeof args.options.roleDefinitionName !== 'undefined',
+ userId: typeof args.options.userId !== 'undefined',
+ userName: typeof args.options.userName !== 'undefined'
+ });
+ });
+ }
+
+ #initOptions(): void {
+ this.options.unshift(
+ {
+ option: '-i, --administrativeUnitId [administrativeUnitId]'
+ },
+ {
+ option: '-n, --administrativeUnitName [administrativeUnitName]'
+ },
+ {
+ option: '--roleDefinitionId [roleDefinitionId]'
+ },
+ {
+ option: '--roleDefinitionName [roleDefinitionName]'
+ },
+ {
+ option: '--userId [userId]'
+ },
+ {
+ option: '--userName [userName]'
+ }
+ );
+ }
+
+ #initValidators(): void {
+ this.validators.push(
+ async (args: CommandArgs) => {
+ if (args.options.administrativeUnitId && !validation.isValidGuid(args.options.administrativeUnitId)) {
+ return `${args.options.administrativeUnitId} is not a valid GUID`;
+ }
+
+ if (args.options.roleDefinitionId && !validation.isValidGuid(args.options.roleDefinitionId)) {
+ return `${args.options.roleDefinitionId} is not a valid GUID`;
+ }
+
+ if (args.options.userId && !validation.isValidGuid(args.options.userId)) {
+ return `${args.options.userId} is not a valid GUID`;
+ }
+
+ return true;
+ }
+ );
+ }
+
+ #initOptionSets(): void {
+ this.optionSets.push({ options: ['administrativeUnitId', 'administrativeUnitName'] });
+ this.optionSets.push({ options: ['roleDefinitionId', 'roleDefinitionName'] });
+ this.optionSets.push({ options: ['userId', 'userName'] });
+ }
+
+ public async commandAction(logger: Logger, args: CommandArgs): Promise {
+ try {
+ let { administrativeUnitId, roleDefinitionId, userId } = args.options;
+
+ if (args.options.administrativeUnitName) {
+ if (this.verbose) {
+ await logger.logToStderr(`Retrieving administrative unit by its name '${args.options.administrativeUnitName}'`);
+ }
+
+ administrativeUnitId = (await aadAdministrativeUnit.getAdministrativeUnitByDisplayName(args.options.administrativeUnitName)).id;
+ }
+
+ if (args.options.roleDefinitionName) {
+ if (this.verbose) {
+ await logger.logToStderr(`Retrieving role definition by its name '${args.options.roleDefinitionName}'`);
+ }
+
+ roleDefinitionId = (await roleDefinition.getRoleDefinitionByDisplayName(args.options.roleDefinitionName)).id;
+ }
+
+ if (args.options.userName) {
+ if (this.verbose) {
+ await logger.logToStderr(`Retrieving user by UPN '${args.options.userName}'`);
+ }
+
+ userId = await aadUser.getUserIdByUpn(args.options.userName);
+ }
+
+ const unifiedRoleAssignment = await roleAssignment.createRoleAssignmentWithAdministrativeUnitScope(roleDefinitionId!, userId!, administrativeUnitId!);
+
+ await logger.log(unifiedRoleAssignment);
+ }
+ catch (err: any) {
+ this.handleRejectedODataJsonPromise(err);
+ }
+ }
+}
+
+export default new EntraAdministrativeUnitRoleAssignmentAddCommand();
\ No newline at end of file
diff --git a/src/utils/roleAssignment.spec.ts b/src/utils/roleAssignment.spec.ts
new file mode 100644
index 0000000000..96b56ec86e
--- /dev/null
+++ b/src/utils/roleAssignment.spec.ts
@@ -0,0 +1,89 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import request from '../request.js';
+import { sinonUtil } from './sinonUtil.js';
+import { roleAssignment } from './roleAssignment.js';
+
+describe('utils/roleAssignment', () => {
+ const roleDefinitionId = 'fe930be7-5e62-47db-91af-98c3a49a38b1';
+ const userId = '2056d2f6-3257-4253-8cfc-b73393e414e5';
+ const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab';
+ const unifieRoleAssignmentWithAdministrativeUnitScopeResponse = {
+ "id": "BH21sHQtUEyvox7IA_Eu_mm3jqnUe4lEhvatluHIWb7-1",
+ "roleDefinitionId": "fe930be7-5e62-47db-91af-98c3a49a38b1",
+ "principalId": "2056d2f6-3257-4253-8cfc-b73393e414e5",
+ "directoryScopeId": "/administrativeUnits/fc33aa61-cf0e-46b6-9506-f633347202ab"
+ };
+ const unifieRoleAssignmentWithTenantScopeResponse = {
+ "id": "BH21sHQtUEyvox7IA_Eu_mm3jqnUe4lEhvatluHIWb7-1",
+ "roleDefinitionId": "fe930be7-5e62-47db-91af-98c3a49a38b1",
+ "principalId": "2056d2f6-3257-4253-8cfc-b73393e414e5",
+ "directoryScopeId": "/"
+ };
+
+ afterEach(() => {
+ sinonUtil.restore([
+ request.post
+ ]);
+ });
+
+ it('correctly assigns a directory (Entra ID) role specified by id with administrative unit scope to a user specified by id', async () => {
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments' &&
+ JSON.stringify(opts.data) === JSON.stringify({
+ "roleDefinitionId": roleDefinitionId,
+ "principalId": userId,
+ "directoryScopeId": `/administrativeUnits/${administrativeUnitId}`
+ })) {
+ return unifieRoleAssignmentWithAdministrativeUnitScopeResponse;
+ }
+
+ throw 'Invalid request';
+ });
+
+ const unifiedRoleAssignment = await roleAssignment.createRoleAssignmentWithAdministrativeUnitScope(roleDefinitionId, userId, administrativeUnitId);
+ assert.deepStrictEqual(unifiedRoleAssignment, {
+ "id": "BH21sHQtUEyvox7IA_Eu_mm3jqnUe4lEhvatluHIWb7-1",
+ "roleDefinitionId": "fe930be7-5e62-47db-91af-98c3a49a38b1",
+ "principalId": "2056d2f6-3257-4253-8cfc-b73393e414e5",
+ "directoryScopeId": "/administrativeUnits/fc33aa61-cf0e-46b6-9506-f633347202ab"
+ });
+ });
+
+ it('correctly assigns a directory (Entra ID) role specified by id with tenant scope to a user specified by id', async () => {
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments' &&
+ JSON.stringify(opts.data) === JSON.stringify({
+ "roleDefinitionId": roleDefinitionId,
+ "principalId": userId,
+ "directoryScopeId": '/'
+ })) {
+ return unifieRoleAssignmentWithTenantScopeResponse;
+ }
+
+ throw 'Invalid request';
+ });
+
+ const unifiedRoleAssignment = await roleAssignment.createRoleAssignmentWithTenantScope(roleDefinitionId, userId);
+ assert.deepStrictEqual(unifiedRoleAssignment, {
+ "id": "BH21sHQtUEyvox7IA_Eu_mm3jqnUe4lEhvatluHIWb7-1",
+ "roleDefinitionId": "fe930be7-5e62-47db-91af-98c3a49a38b1",
+ "principalId": "2056d2f6-3257-4253-8cfc-b73393e414e5",
+ "directoryScopeId": "/"
+ });
+ });
+
+ it('correctly handles random API error when createRoleAssignmentWithAdministrativeUnitScope is called', async () => {
+ const errorMessage = 'Something went wrong';
+ sinon.stub(request, 'post').rejects(new Error(errorMessage));
+
+ await assert.rejects(roleAssignment.createRoleAssignmentWithAdministrativeUnitScope(roleDefinitionId, userId, administrativeUnitId), new Error(errorMessage));
+ });
+
+ it('correctly handles random API error when createRoleAssignmentWithTenantScope is called', async () => {
+ const errorMessage = 'Something went wrong';
+ sinon.stub(request, 'post').rejects(new Error(errorMessage));
+
+ await assert.rejects(roleAssignment.createRoleAssignmentWithTenantScope(roleDefinitionId, userId), new Error(errorMessage));
+ });
+});
\ No newline at end of file
diff --git a/src/utils/roleAssignment.ts b/src/utils/roleAssignment.ts
new file mode 100644
index 0000000000..9bb9434568
--- /dev/null
+++ b/src/utils/roleAssignment.ts
@@ -0,0 +1,45 @@
+import { UnifiedRoleAssignment } from '@microsoft/microsoft-graph-types';
+import request, { CliRequestOptions } from '../request.js';
+
+const getRequestOptions = (roleDefinitionId: string, principalId: string, directoryScopeId: string): CliRequestOptions => ({
+ url: `https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments`,
+ headers: {
+ accept: 'application/json;odata.metadata=none'
+ },
+ responseType: 'json',
+ data: {
+ roleDefinitionId: roleDefinitionId,
+ principalId: principalId,
+ directoryScopeId: directoryScopeId
+ }
+});
+
+/**
+ * Utils for RBAC.
+ * Supported RBAC providers:
+ * - Directory (Entra ID)
+ */
+export const roleAssignment = {
+ /**
+ * Assigns a specific role to a principal with scope to an administrative unit
+ * @param roleDefinitionId Role which lists the actions that can be performed
+ * @param principalId Object that represents a user, group, service principal, or managed identity that is requesting access to resources
+ * @param administrativeUnitId Administrative unit which represents a current scope for a role assignment
+ * @returns Returns unified role assignment object that represents a role definition assigned to a principal with scope to an administrative unit
+ */
+ async createRoleAssignmentWithAdministrativeUnitScope(roleDefinitionId: string, principalId: string, administrativeUnitId: string): Promise {
+ const requestOptions = getRequestOptions(roleDefinitionId, principalId, `/administrativeUnits/${administrativeUnitId}`);
+ return await request.post(requestOptions);
+ },
+
+ /**
+ * Assigns a specific role to a principal with scope to the whole tenant
+ * @param roleDefinitionId Role which lists the actions that can be performed
+ * @param principalId Object that represents a user, group, service principal, or managed identity that is requesting access to resources
+ * @returns Returns unified role assignment object that represents a role definition assigned to a principal with scope to the whole tenant
+ */
+ async createRoleAssignmentWithTenantScope(roleDefinitionId: string, principalId: string): Promise {
+ const requestOptions = getRequestOptions(roleDefinitionId, principalId, '/');
+ return await request.post(requestOptions);
+ }
+};
\ No newline at end of file
diff --git a/src/utils/roleDefinition.spec.ts b/src/utils/roleDefinition.spec.ts
new file mode 100644
index 0000000000..49bb4ddaaa
--- /dev/null
+++ b/src/utils/roleDefinition.spec.ts
@@ -0,0 +1,205 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import { cli } from '../cli/cli.js';
+import request from '../request.js';
+import { sinonUtil } from './sinonUtil.js';
+import { roleDefinition } from './roleDefinition.js';
+import { formatting } from './formatting.js';
+import { settingsNames } from '../settingsNames.js';
+
+describe('utils/roleDefinition', () => {
+ const displayName = 'Helpdesk Administrator';
+ const invalidDisplayName = 'Helpdeks Administratr';
+ const roleDefinitionResponse = {
+ "id": "729827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "description": "Can reset passwords for non-administrators and Helpdesk Administrators.",
+ "displayName": "Helpdesk Administrator",
+ "isBuiltIn": true,
+ "isEnabled": true,
+ "templateId": "729827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "version": "1",
+ "rolePermissions": [
+ {
+ "allowedResourceActions": [
+ "microsoft.directory/users/invalidateAllRefreshTokens",
+ "microsoft.directory/users/bitLockerRecoveryKeys/read",
+ "microsoft.directory/users/password/update",
+ "microsoft.azure.serviceHealth/allEntities/allTasks",
+ "microsoft.azure.supportTickets/allEntities/allTasks",
+ "microsoft.office365.webPortal/allEntities/standard/read",
+ "microsoft.office365.serviceHealth/allEntities/allTasks",
+ "microsoft.office365.supportTickets/allEntities/allTasks"
+ ],
+ "condition": null
+ }
+ ],
+ "inheritsPermissionsFrom": [
+ {
+ "id": "88d8e3e3-8f55-4a1e-953a-9b9898b8876b"
+ }
+ ]
+ };
+ const customRoleDefinitionResponse = {
+ "id": "129827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "description": "Can update passwords for non-administrators and Helpdesk Administrators.",
+ "displayName": "Helpdesk Administrator",
+ "isBuiltIn": false,
+ "isEnabled": true,
+ "templateId": "729827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "version": "1",
+ "rolePermissions": [
+ {
+ "allowedResourceActions": [
+ "microsoft.directory/users/invalidateAllRefreshTokens",
+ "microsoft.directory/users/bitLockerRecoveryKeys/read",
+ "microsoft.directory/users/password/update"
+ ],
+ "condition": null
+ }
+ ],
+ "inheritsPermissionsFrom": [
+ {
+ "id": "88d8e3e3-8f55-4a1e-953a-9b9898b8876b"
+ }
+ ]
+ };
+
+ afterEach(() => {
+ sinonUtil.restore([
+ request.get,
+ cli.getSettingWithDefaultValue,
+ cli.handleMultipleResultsFound
+ ]);
+ });
+
+ it('correctly get single role definition by name using getDirectoryRoleDefinitionByDisplayName', async () => {
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) {
+ return {
+ value: [
+ roleDefinitionResponse
+ ]
+ };
+ }
+
+ return 'Invalid Request';
+ });
+
+ const actual = await roleDefinition.getRoleDefinitionByDisplayName(displayName);
+ assert.deepStrictEqual(actual, {
+ "id": "729827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "description": "Can reset passwords for non-administrators and Helpdesk Administrators.",
+ "displayName": "Helpdesk Administrator",
+ "isBuiltIn": true,
+ "isEnabled": true,
+ "templateId": "729827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "version": "1",
+ "rolePermissions": [
+ {
+ "allowedResourceActions": [
+ "microsoft.directory/users/invalidateAllRefreshTokens",
+ "microsoft.directory/users/bitLockerRecoveryKeys/read",
+ "microsoft.directory/users/password/update",
+ "microsoft.azure.serviceHealth/allEntities/allTasks",
+ "microsoft.azure.supportTickets/allEntities/allTasks",
+ "microsoft.office365.webPortal/allEntities/standard/read",
+ "microsoft.office365.serviceHealth/allEntities/allTasks",
+ "microsoft.office365.supportTickets/allEntities/allTasks"
+ ],
+ "condition": null
+ }
+ ],
+ "inheritsPermissionsFrom": [
+ {
+ "id": "88d8e3e3-8f55-4a1e-953a-9b9898b8876b"
+ }
+ ]
+ });
+ });
+
+ it('handles selecting single role definition when multiple role definitions with the specified name found using getDirectoryRoleDefinitionByDisplayName and cli is set to prompt', async () => {
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) {
+ return {
+ value: [
+ roleDefinitionResponse,
+ customRoleDefinitionResponse
+ ]
+ };
+ }
+
+ return 'Invalid Request';
+ });
+
+ sinon.stub(cli, 'handleMultipleResultsFound').resolves(roleDefinitionResponse);
+
+ const actual = await roleDefinition.getRoleDefinitionByDisplayName(displayName);
+ assert.deepStrictEqual(actual, {
+ "id": "729827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "description": "Can reset passwords for non-administrators and Helpdesk Administrators.",
+ "displayName": "Helpdesk Administrator",
+ "isBuiltIn": true,
+ "isEnabled": true,
+ "templateId": "729827e3-9c14-49f7-bb1b-9608f156bbb8",
+ "version": "1",
+ "rolePermissions": [
+ {
+ "allowedResourceActions": [
+ "microsoft.directory/users/invalidateAllRefreshTokens",
+ "microsoft.directory/users/bitLockerRecoveryKeys/read",
+ "microsoft.directory/users/password/update",
+ "microsoft.azure.serviceHealth/allEntities/allTasks",
+ "microsoft.azure.supportTickets/allEntities/allTasks",
+ "microsoft.office365.webPortal/allEntities/standard/read",
+ "microsoft.office365.serviceHealth/allEntities/allTasks",
+ "microsoft.office365.supportTickets/allEntities/allTasks"
+ ],
+ "condition": null
+ }
+ ],
+ "inheritsPermissionsFrom": [
+ {
+ "id": "88d8e3e3-8f55-4a1e-953a-9b9898b8876b"
+ }
+ ]
+ });
+ });
+
+ it('throws error message when no role definition was found using getDirectoryRoleDefinitionByDisplayName', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$filter=displayName eq '${formatting.encodeQueryParameter(invalidDisplayName)}'`) {
+ return { value: [] };
+ }
+
+ throw 'Invalid Request';
+ });
+
+ await assert.rejects(roleDefinition.getRoleDefinitionByDisplayName(invalidDisplayName)), Error(`The specified role definition '${invalidDisplayName}' does not exist.`);
+ });
+
+ it('throws error message when multiple role definition were found using getDirectoryRoleDefinitionByDisplayName', async () => {
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
+ if (settingName === settingsNames.prompt) {
+ return false;
+ }
+
+ return defaultValue;
+ });
+
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) {
+ return {
+ value: [
+ roleDefinitionResponse,
+ customRoleDefinitionResponse
+ ]
+ };
+ }
+
+ return 'Invalid Request';
+ });
+
+ await assert.rejects(roleDefinition.getRoleDefinitionByDisplayName(displayName),
+ Error(`Multiple role definitions with name '${displayName}' found. Found: ${roleDefinitionResponse.id}, ${customRoleDefinitionResponse.id}.`));
+ });
+});
\ No newline at end of file
diff --git a/src/utils/roleDefinition.ts b/src/utils/roleDefinition.ts
new file mode 100644
index 0000000000..2ab7508839
--- /dev/null
+++ b/src/utils/roleDefinition.ts
@@ -0,0 +1,28 @@
+import { RoleDefinition } from '@microsoft/microsoft-graph-types';
+import { cli } from '../cli/cli.js';
+import { formatting } from './formatting.js';
+import { odata } from './odata.js';
+
+export const roleDefinition = {
+ /**
+ * Get a directory (Microsoft Entra) role
+ * @param displayName Role definition display name.
+ * @returns The role definition.
+ * @throws Error when role definition was not found.
+ */
+ async getRoleDefinitionByDisplayName(displayName: string): Promise {
+ const roleDefinitions = await odata.getAllItems(`https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`);
+
+ if (roleDefinitions.length === 0) {
+ throw `The specified role definition '${displayName}' does not exist.`;
+ }
+
+ if (roleDefinitions.length > 1) {
+ const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', roleDefinitions);
+ const selectedRoleDefinition = await cli.handleMultipleResultsFound(`Multiple role definitions with name '${displayName}' found.`, resultAsKeyValuePair);
+ return selectedRoleDefinition;
+ }
+
+ return roleDefinitions[0];
+ }
+};
\ No newline at end of file