From b01f1f5f99b4e7d596a734f0bab5ab8d2e09f656 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 23 Oct 2023 12:20:33 +0200 Subject: [PATCH 1/2] Adds "aad administrativeunit get" command --- .eslintrc.cjs | 2 + .../administrativeunit-get.mdx | 98 ++++++++++ docs/src/config/sidebars.js | 9 + src/m365/aad/commands.ts | 1 + .../administrativeunit-get.spec.ts | 168 ++++++++++++++++++ .../administrativeunit-get.ts | 121 +++++++++++++ 6 files changed, 399 insertions(+) create mode 100644 docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-get.ts 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..1500daa2fcb --- /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..0f5279dec5a --- /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.calledWith(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.calledWith(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..06efd7855ee --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts @@ -0,0 +1,121 @@ +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(); + } + + #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'] }); + } + + 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 From 750199c7ad3d3c310c9b9259c1b9a95f4c7ac311 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sat, 28 Oct 2023 18:03:45 +0200 Subject: [PATCH 2/2] Adds "aad administrativeunit get" command --- .../administrativeunit-get.mdx | 16 ++++++++-------- .../administrativeunit-get.spec.ts | 4 ++-- .../administrativeunit/administrativeunit-get.ts | 5 +++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx index 1500daa2fcb..a1880dc3736 100644 --- a/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx @@ -45,14 +45,14 @@ m365 aad administrativeunit get --displayName 'Marketing Division' ```json { - "id": "0a22c83d-c4ac-43e2-bb5e-87af3015d49f", - "deletedDateTime": null, - "displayName": "Marketing Division", - "description": "Marketing Department Administration", - "membershipRule": null, - "membershipType": null, - "membershipRuleProcessingState": null, - "visibility": "HiddenMembership" + "id": "0a22c83d-c4ac-43e2-bb5e-87af3015d49f", + "deletedDateTime": null, + "displayName": "Marketing Division", + "description": "Marketing Department Administration", + "membershipRule": null, + "membershipType": null, + "membershipRuleProcessingState": null, + "visibility": "HiddenMembership" } ``` diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts index 0f5279dec5a..dee0c3d6ae7 100644 --- a/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts @@ -92,7 +92,7 @@ describe(commands.ADMINISTRATIVEUNIT_GET, () => { }); await command.action(logger, { options: { id: validId } }); - assert(loggerLogSpy.calledWith(administrativeUnitsReponse.value[0])); + assert(loggerLogSpy.calledOnceWithExactly(administrativeUnitsReponse.value[0])); }); it('retrieves information about the specified administrative unit by displayName', async () => { @@ -109,7 +109,7 @@ describe(commands.ADMINISTRATIVEUNIT_GET, () => { }); await command.action(logger, { options: { displayName: validDisplayName } }); - assert(loggerLogSpy.calledWith(administrativeUnitsReponse.value[0])); + assert(loggerLogSpy.calledOnceWithExactly(administrativeUnitsReponse.value[0])); }); it('throws error message when no administrative unit was found by displayName', async () => { diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts index 06efd7855ee..56027c4709b 100644 --- a/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts @@ -34,6 +34,7 @@ class AadAdministrativeUnitGetCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); this.#initOptionSets(); + this.#initTypes(); } #initTelemetry(): void { @@ -72,6 +73,10 @@ class AadAdministrativeUnitGetCommand extends GraphCommand { this.optionSets.push({ options: ['id', 'displayName'] }); } + #initTypes(): void { + this.types.string.push('displayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { let administrativeUnit: AdministrativeUnit;