diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 3137e992727..7cda2cdbd03 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -5,6 +5,7 @@ const dictionary = [
'activation',
'activations',
'adaptive',
+ 'administrative',
'ai',
'app',
'application',
@@ -90,6 +91,7 @@ const dictionary = [
'threat',
'token',
'type',
+ 'unit',
'user',
'web',
'webhook'
diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx
new file mode 100644
index 00000000000..a1880dc3736
--- /dev/null
+++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx
@@ -0,0 +1,98 @@
+import Global from '/docs/cmd/_global.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# aad administrativeunit get
+
+Gets information about a specific administrative unit
+
+## Usage
+
+```sh
+m365 aad administrativeunit get [options]
+```
+
+## Options
+
+```md definition-list
+`-i, --id [id]`
+: The id of the administrative unit. Specify either `id` or `displayName` but not both.
+
+`-n, --displayName [displayName]`
+: The display name of the administrative unit. Specify either `id` or `displayName` but not both.
+```
+
+
+
+## Examples
+
+Get information about the administrative unit by its id
+
+```sh
+m365 aad administrativeunit get --id 03c4c9dc-6f0c-4c4f-a4e6-0c9ed80f54c7
+```
+
+Get information about the administrative unit by its display name
+
+```sh
+m365 aad administrativeunit get --displayName 'Marketing Division'
+```
+
+## Response
+
+
+
+
+ ```json
+ {
+ "id": "0a22c83d-c4ac-43e2-bb5e-87af3015d49f",
+ "deletedDateTime": null,
+ "displayName": "Marketing Division",
+ "description": "Marketing Department Administration",
+ "membershipRule": null,
+ "membershipType": null,
+ "membershipRuleProcessingState": null,
+ "visibility": "HiddenMembership"
+ }
+ ```
+
+
+
+
+ ```text
+ deletedDateTime : null
+ description : Marketing Department Administration
+ displayName : Marketing Division
+ id : 0a22c83d-c4ac-43e2-bb5e-87af3015d49f
+ membershipRule : null
+ membershipRuleProcessingState: null
+ membershipType : null
+ visibility : HiddenMembership
+ ```
+
+
+
+
+ ```csv
+ id,displayName,description,visibility
+ 0a22c83d-c4ac-43e2-bb5e-87af3015d49f,Marketing Division,Marketing Department Administration,HiddenMembership
+ ```
+
+
+
+
+ ```md
+ Date: 10/23/2023
+
+ ## Marketing Division (0a22c83d-c4ac-43e2-bb5e-87af3015d49f)
+
+ Property | Value
+ ---------|-------
+ id | 0a22c83d-c4ac-43e2-bb5e-87af3015d49f
+ displayName | Marketing Division
+ description | Marketing Department Administration
+ visibility | HiddenMembership
+ ```
+
+
+
\ No newline at end of file
diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js
index 28f8a409d37..c5aecc6df34 100644
--- a/docs/src/config/sidebars.js
+++ b/docs/src/config/sidebars.js
@@ -26,6 +26,15 @@ const sidebars = {
'cmd/version',
{
'Azure Active Directory (aad)': [
+ {
+ administrativeunit: [
+ {
+ type: 'doc',
+ label: 'administrativeunit get',
+ id: 'cmd/aad/administrativeunit/administrativeunit-get'
+ }
+ ]
+ },
{
app: [
{
diff --git a/src/m365/aad/commands.ts b/src/m365/aad/commands.ts
index 774e85936d6..50e4633e611 100644
--- a/src/m365/aad/commands.ts
+++ b/src/m365/aad/commands.ts
@@ -1,6 +1,7 @@
const prefix: string = 'aad';
export default {
+ ADMINISTRATIVEUNIT_GET: `${prefix} administrativeunit get`,
APP_ADD: `${prefix} app add`,
APP_GET: `${prefix} app get`,
APP_LIST: `${prefix} app list`,
diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts
new file mode 100644
index 00000000000..dee0c3d6ae7
--- /dev/null
+++ b/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts
@@ -0,0 +1,168 @@
+import assert from 'assert';
+import sinon from "sinon";
+import auth from '../../../../Auth.js';
+import { CommandInfo } from "../../../../cli/CommandInfo.js";
+import { Logger } from "../../../../cli/Logger.js";
+import commands from "../../commands.js";
+import { telemetry } from '../../../../telemetry.js';
+import { pid } from '../../../../utils/pid.js';
+import { session } from '../../../../utils/session.js';
+import { Cli } from '../../../../cli/Cli.js';
+import command from './administrativeunit-get.js';
+import request from '../../../../request.js';
+import { sinonUtil } from '../../../../utils/sinonUtil.js';
+import { CommandError } from '../../../../Command.js';
+import { formatting } from '../../../../utils/formatting.js';
+
+describe(commands.ADMINISTRATIVEUNIT_GET, () => {
+ let log: string[];
+ let logger: Logger;
+ let loggerLogSpy: sinon.SinonSpy;
+ let commandInfo: CommandInfo;
+ const administrativeUnitsReponse = {
+ value: [
+ {
+ id: 'fc33aa61-cf0e-46b6-9506-f633347202ab',
+ displayName: 'European Division',
+ visibility: 'HiddenMembership'
+ },
+ {
+ id: 'a25b4c5e-e8b7-4f02-a23d-0965b6415098',
+ displayName: 'Asian Division',
+ visibility: null
+ }
+ ]
+ };
+ const validId = 'fc33aa61-cf0e-46b6-9506-f633347202ab';
+ const validDisplayName = 'European Division';
+ const invalidDisplayName = 'European';
+
+ 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([
+ request.get,
+ Cli.handleMultipleResultsFound
+ ]);
+ });
+
+ after(() => {
+ sinon.restore();
+ auth.service.connected = false;
+ });
+
+ it('has correct name', () => {
+ assert.strictEqual(command.name, commands.ADMINISTRATIVEUNIT_GET);
+ });
+
+ it('has a description', () => {
+ assert.notStrictEqual(command.description, null);
+ });
+
+ it('retrieves information about the specified administrative unit by id', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${validId}`) {
+ return administrativeUnitsReponse.value[0];
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: { id: validId } });
+ assert(loggerLogSpy.calledOnceWithExactly(administrativeUnitsReponse.value[0]));
+ });
+
+ it('retrieves information about the specified administrative unit by displayName', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(validDisplayName)}'`) {
+ return {
+ value: [
+ administrativeUnitsReponse.value[0]
+ ]
+ };
+ }
+
+ throw 'Invalid Request';
+ });
+
+ await command.action(logger, { options: { displayName: validDisplayName } });
+ assert(loggerLogSpy.calledOnceWithExactly(administrativeUnitsReponse.value[0]));
+ });
+
+ it('throws error message when no administrative unit was found by displayName', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(invalidDisplayName)}'`) {
+ return { value: [] };
+ }
+
+ throw 'Invalid Request';
+ });
+
+ await assert.rejects(command.action(logger, { options: { displayName: invalidDisplayName } }), new CommandError(`The specified administrative unit '${invalidDisplayName}' does not exist.`));
+ });
+
+ it('handles selecting single result when multiple administrative units with the specified displayName found and cli is set to prompt', async () => {
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(validDisplayName)}'`) {
+ return {
+ value: [
+ administrativeUnitsReponse.value[0],
+ administrativeUnitsReponse.value[0]
+ ]
+ };
+ }
+
+ return 'Invalid Request';
+ });
+
+ sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: validId, displayName: validDisplayName, visibility: 'HiddenMembership' });
+
+ await command.action(logger, { options: { displayName: validDisplayName } });
+ assert(loggerLogSpy.calledWith(administrativeUnitsReponse.value[0]));
+ });
+
+ it('handles random API error', async () => {
+ const errorMessage = 'Something went wrong';
+ sinon.stub(request, 'get').rejects(new Error(errorMessage));
+
+ await assert.rejects(command.action(logger, { options: { id: validId } }), new CommandError(errorMessage));
+ });
+
+ it('fails validation if the id is not a valid GUID', async () => {
+ const actual = await command.validate({ options: { id: '123' } }, commandInfo);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('passes validation if the id is a valid GUID', async () => {
+ const actual = await command.validate({ options: { id: validId } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('passes validation if required options specified (displayName)', async () => {
+ const actual = await command.validate({ options: { displayName: validDisplayName } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+});
\ No newline at end of file
diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts
new file mode 100644
index 00000000000..56027c4709b
--- /dev/null
+++ b/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts
@@ -0,0 +1,126 @@
+import { AdministrativeUnit } from "@microsoft/microsoft-graph-types";
+import GlobalOptions from "../../../../GlobalOptions.js";
+import { Logger } from "../../../../cli/Logger.js";
+import { validation } from "../../../../utils/validation.js";
+import request, { CliRequestOptions } from "../../../../request.js";
+import GraphCommand from "../../../base/GraphCommand.js";
+import commands from "../../commands.js";
+import { odata } from "../../../../utils/odata.js";
+import { formatting } from "../../../../utils/formatting.js";
+import { Cli } from "../../../../cli/Cli.js";
+
+interface CommandArgs {
+ options: Options;
+}
+
+export interface Options extends GlobalOptions {
+ id?: string;
+ displayName?: string;
+}
+
+class AadAdministrativeUnitGetCommand extends GraphCommand {
+ public get name(): string {
+ return commands.ADMINISTRATIVEUNIT_GET;
+ }
+
+ public get description(): string {
+ return 'Gets information about a specific administrative unit';
+ }
+
+ constructor() {
+ super();
+
+ this.#initTelemetry();
+ this.#initOptions();
+ this.#initValidators();
+ this.#initOptionSets();
+ this.#initTypes();
+ }
+
+ #initTelemetry(): void {
+ this.telemetry.push((args: CommandArgs) => {
+ Object.assign(this.telemetryProperties, {
+ id: typeof args.options.id !== 'undefined',
+ displayName: typeof args.options.displayName !== 'undefined'
+ });
+ });
+ }
+
+ #initOptions(): void {
+ this.options.unshift(
+ {
+ option: '-i, --id [id]'
+ },
+ {
+ option: '-n, --displayName [displayName]'
+ }
+ );
+ }
+
+ #initValidators(): void {
+ this.validators.push(
+ async (args: CommandArgs) => {
+ if (args.options.id && !validation.isValidGuid(args.options.id as string)) {
+ return `${args.options.id} is not a valid GUID`;
+ }
+
+ return true;
+ }
+ );
+ }
+
+ #initOptionSets(): void {
+ this.optionSets.push({ options: ['id', 'displayName'] });
+ }
+
+ #initTypes(): void {
+ this.types.string.push('displayName');
+ }
+
+ public async commandAction(logger: Logger, args: CommandArgs): Promise {
+ let administrativeUnit: AdministrativeUnit;
+
+ try {
+ if (args.options.id) {
+ administrativeUnit = await this.getAdministrativeUnitById(args.options.id);
+ }
+ else {
+ administrativeUnit = await this.getAdministrativeUnitByDisplayName(args.options.displayName!);
+ }
+
+ await logger.log(administrativeUnit);
+ }
+ catch (err: any) {
+ this.handleRejectedODataJsonPromise(err);
+ }
+ }
+
+ async getAdministrativeUnitById(id: string): Promise {
+ const requestOptions: CliRequestOptions = {
+ url: `${this.resource}/v1.0/directory/administrativeUnits/${id}`,
+ headers: {
+ accept: 'application/json;odata.metadata=none'
+ },
+ responseType: 'json'
+ };
+
+ return await request.get(requestOptions);
+ }
+
+ async getAdministrativeUnitByDisplayName(displayName: string): Promise {
+ const administrativeUnits = await odata.getAllItems(`${this.resource}/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`);
+
+ if (administrativeUnits.length === 0) {
+ throw `The specified administrative unit '${displayName}' does not exist.`;
+ }
+
+ if (administrativeUnits.length > 1) {
+ const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', administrativeUnits);
+ return await Cli.handleMultipleResultsFound(`Multiple administrative units with name '${displayName}' found.`, resultAsKeyValuePair);
+ }
+
+ return administrativeUnits[0];
+ }
+}
+
+export default new AadAdministrativeUnitGetCommand();
\ No newline at end of file