diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-member-add.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-member-add.mdx
new file mode 100644
index 00000000000..5c06d471d86
--- /dev/null
+++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-member-add.mdx
@@ -0,0 +1,93 @@
+import Global from '/docs/cmd/_global.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# aad administrativeunit member add
+
+Add a member (user, group, or device) to an administrative unit
+
+## Usage
+
+```sh
+m365 aad administrativeunit member add [options]
+```
+
+## Options
+
+```md definition-list
+`-i, --administrativeUnitId [administrativeUnitId]`
+: The id of the administrative unit. Specify either `administrativeUnitId` or `administrativeUnitName` but not both.
+
+`-n, --administrativeUnitName [administrativeUnitName]`
+: The name of the administrative unit. Specify either `administrativeUnitId` or `administrativeUnitName` but not both.
+
+`--userId [userId]`
+: The id of the user to be added. Specify `userId`, `userName`, `groupId`, `groupName`, `deviceId` or `deviceName`.
+
+`--userName [userName]`
+: The user principal name (UPN) of the user to be added. Specify `userId`, `userName`, `groupId`, `groupName`, `deviceId` or `deviceName`.
+
+`--groupId [groupId]`
+: The id of the group to be added. Specify `userId`, `userName`, `groupId`, `groupName`, `deviceId` or `deviceName`.
+
+`--groupName [groupName]`
+: The name of the group to be added. Specify `userId`, `userName`, `groupId`, `groupName`, `deviceId` or `deviceName`.
+
+`--deviceId [deviceId]`
+: The id of the device to be added. Specify `userId`, `userName`, `groupId`, `groupName`, `deviceId` or `deviceName`.
+
+`--deviceName [deviceName]`
+: The name of the device to be added. Specify `userId`, `userName`, `groupId`, `groupName`, `deviceId` or `deviceName`.
+```
+
+
+
+## Remarks
+
+:::info
+
+To use this command you must be either **Global Administrator** or **Privileged Role Administrator**.
+
+:::
+
+## Examples
+
+Add a single user specified by id to an administrative unit specified by id
+
+```sh
+m365 aad administrativeunit member add --administrativeUnitId 03c4c9dc-6f0c-4c4f-a4e6-0c9ed80f54c7 --userId 1caf7dcd-7e83-4c3a-94f7-932a1299c844
+```
+
+Add a single user specified by user principal name to an administrative unit specified by name
+
+```sh
+m365 aad administrativeunit member add --administrativeUnitName 'Marketing Division' --userName john.doe@contoso.com
+```
+
+Add a single group specified by id to an administrative unit specified by id
+
+```sh
+m365 aad administrativeunit member add --administrativeUnitId 03c4c9dc-6f0c-4c4f-a4e6-0c9ed80f54c7 --groupId b2307a39-e878-458b-bc90-03bc578531d6
+```
+
+Add a single group specified by name to an administrative unit specified by name
+
+```sh
+m365 aad administrativeunit member add --administrativeUnitName 'Marketing Division' --groupName 'Marketing Group'
+```
+
+Add a single device specified by id to an administrative unit specified by id
+
+```sh
+m365 aad administrativeunit member add --administrativeUnitId 03c4c9dc-6f0c-4c4f-a4e6-0c9ed80f54c7 --deviceId 810c84a8-4a9e-49e6-bf7d-12d183f40d01
+```
+
+Add a single device specified by name to an administrative unit specified by name
+
+```sh
+m365 aad administrativeunit member add --administrativeUnitName 'Marketing Division' --deviceName 'JohnDoe-PC'
+```
+
+## More information
+
+- Administrative units: https://learn.microsoft.com/entra/identity/role-based-access-control/administrative-units
\ No newline at end of file
diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts
index a1853499a36..f5a5e2365d6 100644
--- a/docs/src/config/sidebars.ts
+++ b/docs/src/config/sidebars.ts
@@ -47,6 +47,11 @@ const sidebars: SidebarsConfig = {
label: 'administrativeunit remove',
id: 'cmd/aad/administrativeunit/administrativeunit-remove'
},
+ {
+ type: 'doc',
+ label: 'administrativeunit member add',
+ id: 'cmd/aad/administrativeunit/administrativeunit-member-add'
+ },
{
type: 'doc',
label: 'administrativeunit member list',
diff --git a/src/m365/aad/commands.ts b/src/m365/aad/commands.ts
index 6183fa4399d..ed8c1025e30 100644
--- a/src/m365/aad/commands.ts
+++ b/src/m365/aad/commands.ts
@@ -5,6 +5,7 @@ export default {
ADMINISTRATIVEUNIT_GET: `${prefix} administrativeunit get`,
ADMINISTRATIVEUNIT_LIST: `${prefix} administrativeunit list`,
ADMINISTRATIVEUNIT_REMOVE: `${prefix} administrativeunit remove`,
+ ADMINISTRATIVEUNIT_MEMBER_ADD: `${prefix} administrativeunit member add`,
ADMINISTRATIVEUNIT_MEMBER_LIST: `${prefix} administrativeunit member list`,
APP_ADD: `${prefix} app add`,
APP_GET: `${prefix} app get`,
diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-member-add.spec.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-member-add.spec.ts
new file mode 100644
index 00000000000..c13cf749a72
--- /dev/null
+++ b/src/m365/aad/commands/administrativeunit/administrativeunit-member-add.spec.ts
@@ -0,0 +1,466 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import auth from '../../../../Auth.js';
+import commands from '../../commands.js';
+import request from '../../../../request.js';
+import { telemetry } from '../../../../telemetry.js';
+import { Logger } from '../../../../cli/Logger.js';
+import { CommandError } from '../../../../Command.js';
+import { pid } from '../../../../utils/pid.js';
+import { session } from '../../../../utils/session.js';
+import { sinonUtil } from '../../../../utils/sinonUtil.js';
+import { cli } from '../../../../cli/cli.js';
+import { CommandInfo } from '../../../../cli/CommandInfo.js';
+import command from './administrativeunit-member-add.js';
+import { aadAdministrativeUnit } from '../../../../utils/aadAdministrativeUnit.js';
+import { aadGroup } from '../../../../utils/aadGroup.js';
+import { aadUser } from '../../../../utils/aadUser.js';
+import { aadDevice } from '../../../../utils/aadDevice.js';
+import { settingsNames } from '../../../../settingsNames.js';
+
+describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => {
+ const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab';
+ const administrativeUnitName = 'European Division';
+ const userId = '23b415fb-baea-4995-a26e-c74073beadff';
+ const userName = 'adele.vence@contoso.com';
+ const groupId = '593af7e2-d27e-42b8-ad17-abe5e57dab61';
+ const groupName = 'Marketing';
+ const deviceId = '60c99a96-70af-4d68-a8dc-5c51b345c6ce';
+ const deviceName = 'AdeleVence-PC';
+
+ let log: string[];
+ let logger: Logger;
+ 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);
+ }
+ };
+ });
+
+ afterEach(() => {
+ sinonUtil.restore([
+ request.post,
+ aadAdministrativeUnit.getAdministrativeUnitByDisplayName,
+ aadUser.getUserIdByUpn,
+ aadGroup.getGroupIdByDisplayName,
+ aadDevice.getDeviceByDisplayName,
+ cli.getSettingWithDefaultValue,
+ cli.handleMultipleResultsFound,
+ cli.promptForSelection
+ ]);
+ });
+
+ after(() => {
+ sinon.restore();
+ auth.service.connected = false;
+ });
+
+ it('has correct name', () => {
+ assert.strictEqual(command.name, commands.ADMINISTRATIVEUNIT_MEMBER_ADD);
+ });
+
+ it('has a description', () => {
+ assert.notStrictEqual(command.description, null);
+ });
+
+ it('passes validation when administrativeUnitId is a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('fails validation if administrativeUnitId is not a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: 'invalid', userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('passes validation when userId is a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('fails validation if userId is not a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: 'invalid' } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('passes validation when groupId is a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('fails validation if groupId is not a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: 'invalid' } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('passes validation when deviceId is a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('fails validation if deviceId is not a valid GUID', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: 'invalid' } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both administrativeUnitId and administrativeUnitName options 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, userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation if both administrativeUnitId and administrativeUnitName options 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: { userId: userId } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userId and userName options 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: '00000000-0000-0000-0000-000000000000', userId: userId, userName: userName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userId and groupId options 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: '00000000-0000-0000-0000-000000000000', userId: userId, groupId: groupId } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userId and groupName options 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: '00000000-0000-0000-0000-000000000000', userId: userId, groupName: groupName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userId and deviceId options 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: '00000000-0000-0000-0000-000000000000', userId: userId, deviceId: deviceId } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userId and deviceName options 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: '00000000-0000-0000-0000-000000000000', userId: userId, deviceName: deviceName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userName and groupId options 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: '00000000-0000-0000-0000-000000000000', userName: userName, groupId: groupId } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userName and groupName options 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: '00000000-0000-0000-0000-000000000000', userName: userName, groupName: groupName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userName and deviceId options 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: '00000000-0000-0000-0000-000000000000', userName: userName, deviceId: deviceId } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both userName and deviceName options 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: '00000000-0000-0000-0000-000000000000', userName: userName, deviceName: deviceName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both groupId and groupName options 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: '00000000-0000-0000-0000-000000000000', groupId: groupId, groupName: groupName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both groupId and deviceId options 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: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceId: deviceId } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both groupId and deviceName options 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: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceName: deviceName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both groupName and deviceId options 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: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceId: deviceId } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both groupName and deviceName options 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: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceName: deviceName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when both deviceId and deviceName options 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: '00000000-0000-0000-0000-000000000000', deviceId: deviceId, deviceName: deviceName } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('passes validation if all the required options are specified', async () => {
+ const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, userId: userId } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('adds a user specified by its id to an administrative unit specified by its id', async () => {
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`) {
+ return;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: { administrativeUnitId: administrativeUnitId, userId: userId } });
+ assert(postStub.called);
+ assert.deepStrictEqual(postStub.lastCall.args[0].data, { "@odata.id": `https://graph.microsoft.com/v1.0/users/${userId}` });
+ });
+
+ it('adds a user specified by its name to an administrative unit specified by its name (verbose)', async () => {
+ sinon.stub(aadUser, 'getUserIdByUpn').withArgs(userName).resolves(userId);
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).resolves({ id: administrativeUnitId, displayName: administrativeUnitName });
+
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`) {
+ return;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: { administrativeUnitName: administrativeUnitName, userName: userName, verbose: true } });
+ assert(postStub.called);
+ assert.deepStrictEqual(postStub.lastCall.args[0].data, { "@odata.id": `https://graph.microsoft.com/v1.0/users/${userId}` });
+ });
+
+ it('adds a group specified by its id to an administrative unit specified by its id', async () => {
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`) {
+ return;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: { administrativeUnitId: administrativeUnitId, groupId: groupId } });
+ assert(postStub.called);
+ assert.deepStrictEqual(postStub.lastCall.args[0].data, { "@odata.id": `https://graph.microsoft.com/v1.0/groups/${groupId}` });
+ });
+
+ it('adds a group specified by its name to an administrative unit specified by its name (verbose)', async () => {
+ sinon.stub(aadGroup, 'getGroupIdByDisplayName').withArgs(groupName).resolves(groupId);
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).resolves({ id: administrativeUnitId, displayName: administrativeUnitName });
+
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`) {
+ return;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: { administrativeUnitName: administrativeUnitName, groupName: groupName, verbose: true } });
+ assert(postStub.called);
+ assert.deepStrictEqual(postStub.lastCall.args[0].data, { "@odata.id": `https://graph.microsoft.com/v1.0/groups/${groupId}` });
+ });
+
+ it('adds a device specified by its id to an administrative unit specified by its id', async () => {
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`) {
+ return;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: { administrativeUnitId: administrativeUnitId, deviceId: deviceId } });
+ assert(postStub.called);
+ assert.deepStrictEqual(postStub.lastCall.args[0].data, { "@odata.id": `https://graph.microsoft.com/v1.0/devices/${deviceId}` });
+ });
+
+ it('adds a device specified by its name to an administrative unit specified by its name (verbose)', async () => {
+ sinon.stub(aadDevice, 'getDeviceByDisplayName').withArgs(deviceName).resolves({ id: deviceId, displayName: deviceName });
+ sinon.stub(aadAdministrativeUnit, 'getAdministrativeUnitByDisplayName').withArgs(administrativeUnitName).resolves({ id: administrativeUnitId, displayName: administrativeUnitName });
+
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`) {
+ return;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: { administrativeUnitName: administrativeUnitName, deviceName: deviceName, verbose: true } });
+ assert(postStub.called);
+ assert.deepStrictEqual(postStub.lastCall.args[0].data, { "@odata.id": `https://graph.microsoft.com/v1.0/devices/${deviceId}` });
+ });
+
+ it('throws an error when administrative unit by id cannot be found', async () => {
+ const error = {
+ error: {
+ code: 'Request_ResourceNotFound',
+ message: `Resource '${administrativeUnitId}' does not exist or one of its queried reference-property objects are not present.`,
+ innerError: {
+ date: '2023-10-27T12:24:36',
+ 'request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b',
+ 'client-request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b'
+ }
+ }
+ };
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`) {
+ throw error;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await assert.rejects(command.action(logger, { options: { administrativeUnitId: administrativeUnitId, userId: userId } }), new CommandError(error.error.message));
+ });
+});
\ No newline at end of file
diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-member-add.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-member-add.ts
new file mode 100644
index 00000000000..59bb25e8518
--- /dev/null
+++ b/src/m365/aad/commands/administrativeunit/administrativeunit-member-add.ts
@@ -0,0 +1,185 @@
+import GlobalOptions from "../../../../GlobalOptions.js";
+import { Logger } from "../../../../cli/Logger.js";
+import { aadAdministrativeUnit } from "../../../../utils/aadAdministrativeUnit.js";
+import { aadGroup } from "../../../../utils/aadGroup.js";
+import { aadUser } from "../../../../utils/aadUser.js";
+import { validation } from "../../../../utils/validation.js";
+import GraphCommand from "../../../base/GraphCommand.js";
+import commands from "../../commands.js";
+import request, { CliRequestOptions } from "../../../../request.js";
+import { aadDevice } from "../../../../utils/aadDevice.js";
+
+interface CommandArgs {
+ options: Options;
+}
+
+interface Options extends GlobalOptions {
+ administrativeUnitId?: string;
+ administrativeUnitName?: string;
+ userId?: string;
+ userName?: string;
+ groupId?: string;
+ groupName?: string;
+ deviceId?: string;
+ deviceName?: string;
+}
+
+class AadAdministrativeUnitMemberAddCommand extends GraphCommand {
+ public get name(): string {
+ return commands.ADMINISTRATIVEUNIT_MEMBER_ADD;
+ }
+
+ public get description(): string {
+ return 'Adds a member (user, group, device) to an administrative unit';
+ }
+
+ constructor() {
+ super();
+
+ this.#initTelemetry();
+ this.#initOptions();
+ this.#initValidators();
+ this.#initOptionSets();
+ }
+
+ #initTelemetry(): void {
+ this.telemetry.push((args: CommandArgs) => {
+ Object.assign(this.telemetryProperties, {
+ userId: typeof args.options.userId !== 'undefined',
+ userName: typeof args.options.userName !== 'undefined',
+ groupId: typeof args.options.groupId !== 'undefined',
+ groupName: typeof args.options.groupName !== 'undefined',
+ deviceId: typeof args.options.deviceId !== 'undefined',
+ deviceName: typeof args.options.deviceName !== 'undefined'
+ });
+ });
+ }
+
+ #initOptions(): void {
+ this.options.unshift(
+ {
+ option: '-i, --administrativeUnitId [administrativeUnitId]'
+ },
+ {
+ option: '-n, --administrativeUnitName [administrativeUnitName]'
+ },
+ {
+ option: "--userId [userId]"
+ },
+ {
+ option: "--userName [userName]"
+ },
+ {
+ option: "--groupId [groupId]"
+ },
+ {
+ option: "--groupName [groupName]"
+ },
+ {
+ option: "--deviceId [deviceId]"
+ },
+ {
+ option: "--deviceName [deviceName]"
+ }
+ );
+ }
+
+ #initValidators(): void {
+ this.validators.push(
+ async (args: CommandArgs) => {
+ if (args.options.administrativeUnitId && !validation.isValidGuid(args.options.administrativeUnitId as string)) {
+ return `${args.options.administrativeUnitId} is not a valid GUID`;
+ }
+
+ if (args.options.userId && !validation.isValidGuid(args.options.userId as string)) {
+ return `${args.options.userId} is not a valid GUID`;
+ }
+
+ if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) {
+ return `${args.options.groupId} is not a valid GUID`;
+ }
+
+ if (args.options.deviceId && !validation.isValidGuid(args.options.deviceId as string)) {
+ return `${args.options.deviceId} is not a valid GUID`;
+ }
+
+ return true;
+ }
+ );
+ }
+
+ #initOptionSets(): void {
+ this.optionSets.push({ options: ['administrativeUnitId', 'administrativeUnitName'] });
+ this.optionSets.push({ options: ['userId', 'userName', 'groupId', 'groupName', 'deviceId', 'deviceName'] });
+ }
+
+ public async commandAction(logger: Logger, args: CommandArgs): Promise {
+ let administrativeUnitId = args.options.administrativeUnitId;
+ let memberType;
+ let memberId;
+
+ try {
+ if (args.options.administrativeUnitName) {
+ administrativeUnitId = (await aadAdministrativeUnit.getAdministrativeUnitByDisplayName(args.options.administrativeUnitName)).id!;
+
+ if (this.verbose) {
+ await logger.logToStderr(`Administrative unit with name ${args.options.administrativeUnitName} retrieved, returned id: ${administrativeUnitId}`);
+ }
+ }
+
+ if (args.options.userId || args.options.userName) {
+ memberType = 'users';
+ memberId = args.options.userId;
+
+ if (args.options.userName) {
+ memberId = await aadUser.getUserIdByUpn(args.options.userName);
+
+ if (this.verbose) {
+ await logger.logToStderr(`User with name ${args.options.userName} retrieved, returned id: ${memberId}`);
+ }
+ }
+ }
+ else if (args.options.groupId || args.options.groupName) {
+ memberType = 'groups';
+ memberId = args.options.groupId;
+
+ if (args.options.groupName) {
+ memberId = await aadGroup.getGroupIdByDisplayName(args.options.groupName);
+
+ if (this.verbose) {
+ await logger.logToStderr(`Group with name ${args.options.groupName} retrieved, returned id: ${memberId}`);
+ }
+ }
+ }
+ else if (args.options.deviceId || args.options.deviceName) {
+ memberType = 'devices';
+ memberId = args.options.deviceId;
+
+ if (args.options.deviceName) {
+ memberId = (await aadDevice.getDeviceByDisplayName(args.options.deviceName)).id;
+
+ if (this.verbose) {
+ await logger.logToStderr(`Device with name ${args.options.deviceName} retrieved, returned id: ${memberId}`);
+ }
+ }
+ }
+
+ const requestOptions: CliRequestOptions = {
+ url: `${this.resource}/v1.0/directory/administrativeUnits/${administrativeUnitId}/members/$ref`,
+ headers: {
+ 'accept': 'application/json;odata.metadata=none'
+ },
+ data: {
+ "@odata.id": `https://graph.microsoft.com/v1.0/${memberType}/${memberId}`
+ }
+ };
+
+ await request.post(requestOptions);
+ }
+ catch (err: any) {
+ this.handleRejectedODataJsonPromise(err);
+ }
+ }
+}
+
+export default new AadAdministrativeUnitMemberAddCommand();
\ No newline at end of file
diff --git a/src/utils/aadDevice.spec.ts b/src/utils/aadDevice.spec.ts
new file mode 100644
index 00000000000..09d22f8fe4e
--- /dev/null
+++ b/src/utils/aadDevice.spec.ts
@@ -0,0 +1,102 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import { aadDevice } from './aadDevice.js';
+import { cli } from "../cli/cli.js";
+import request from "../request.js";
+import { sinonUtil } from "./sinonUtil.js";
+import { formatting } from './formatting.js';
+import { settingsNames } from '../settingsNames.js';
+
+
+describe('utils/aadDevice', () => {
+ const deviceId = 'fc33aa61-cf0e-46b6-9506-f633347202ab';
+ const secondDeviceId = 'fc33aa61-cf0e-1234-9506-f633347202ab';
+ const displayName = 'AdeleVence-PC';
+ const secondDisplayName = 'JohnDoe-PC';
+ const invalidDisplayName = 'European';
+
+ afterEach(() => {
+ sinonUtil.restore([
+ request.get,
+ cli.getSettingWithDefaultValue,
+ cli.handleMultipleResultsFound
+ ]);
+ });
+
+ it('correctly get single device by name using getDeviceByDisplayName', async () => {
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/devices?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) {
+ return {
+ value: [
+ {
+ id: deviceId,
+ displayName: displayName
+ }
+ ]
+ };
+ }
+
+ return 'Invalid Request';
+ });
+
+ const actual = await aadDevice.getDeviceByDisplayName(displayName);
+ assert.deepStrictEqual(actual, { id: deviceId, displayName: displayName });
+ });
+
+ it('handles selecting single device when multiple devices with the specified name found using getDeviceByDisplayName and cli is set to prompt', async () => {
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/devices?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) {
+ return {
+ value: [
+ { id: deviceId, displayName: displayName },
+ { id: secondDeviceId, displayName: secondDisplayName }
+ ]
+ };
+ }
+
+ return 'Invalid Request';
+ });
+
+ sinon.stub(cli, 'handleMultipleResultsFound').resolves({ id: deviceId, displayName: displayName });
+
+ const actual = await aadDevice.getDeviceByDisplayName(displayName);
+ assert.deepStrictEqual(actual, { id: deviceId, displayName: displayName });
+ });
+
+ it('throws error message when no device was found using getDeviceByDisplayName', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/devices?$filter=displayName eq '${formatting.encodeQueryParameter(invalidDisplayName)}'`) {
+ return { value: [] };
+ }
+
+ throw 'Invalid Request';
+ });
+
+ await assert.rejects(aadDevice.getDeviceByDisplayName(invalidDisplayName)), Error(`The specified device '${invalidDisplayName}' does not exist.`);
+ });
+
+ it('throws error message when multiple devices were found using getDeviceByDisplayName', 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/devices?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) {
+ return {
+ value: [
+ { id: deviceId },
+ { id: deviceId }
+ ]
+ };
+ }
+
+ return 'Invalid Request';
+ });
+
+ await assert.rejects(aadDevice.getDeviceByDisplayName(displayName), Error(`Multiple devices with name '${displayName}' found. Found: ${deviceId}.`));
+ });
+});
\ No newline at end of file
diff --git a/src/utils/aadDevice.ts b/src/utils/aadDevice.ts
new file mode 100644
index 00000000000..808ed956a3f
--- /dev/null
+++ b/src/utils/aadDevice.ts
@@ -0,0 +1,30 @@
+import { Device } from "@microsoft/microsoft-graph-types";
+import { odata } from "./odata.js";
+import { formatting } from "./formatting.js";
+import { cli } from "../cli/cli.js";
+
+const graphResource = 'https://graph.microsoft.com';
+
+export const aadDevice = {
+ /**
+ * Get a device by its display name.
+ * @param displayName Device display name.
+ * @returns The device.
+ * @throws Error when device was not found.
+ */
+ async getDeviceByDisplayName(displayName: string): Promise {
+ const devices = await odata.getAllItems(`${graphResource}/v1.0/devices?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`);
+
+ if (devices.length === 0) {
+ throw `The specified device '${displayName}' does not exist.`;
+ }
+
+ if (devices.length > 1) {
+ const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', devices);
+ const selectedDevice = await cli.handleMultipleResultsFound(`Multiple devices with name '${displayName}' found.`, resultAsKeyValuePair);
+ return selectedDevice;
+ }
+
+ return devices[0];
+ }
+};
\ No newline at end of file