From e952949ddff79f7f5178301c603c5ad4d8b8be15 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Sat, 21 Sep 2024 20:47:04 +0200 Subject: [PATCH] Enhances 'entra m365group set' with displayName. Closes #6146 --- .../cmd/entra/m365group/m365group-set.mdx | 15 ++++--- docs/docs/v10-upgrade-guidance.mdx | 8 ++++ .../commands/m365group/m365group-set.spec.ts | 29 ++++++------ .../entra/commands/m365group/m365group-set.ts | 44 ++++++++++++------- 4 files changed, 59 insertions(+), 37 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-set.mdx b/docs/docs/cmd/entra/m365group/m365group-set.mdx index 214a44628fb..ae58af1f631 100644 --- a/docs/docs/cmd/entra/m365group/m365group-set.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-set.mdx @@ -19,11 +19,14 @@ m365 aad m365group set [options] ## Options ```md definition-list -`-i, --id ` +`-i, --id [id]` : The ID of the Microsoft 365 Group to update `-n, --displayName [displayName]` -: Display name for the Microsoft 365 Group +: Display name of the Microsoft 365 Group to update + +`--newDisplayName [newDisplayName]` +: New display name for the Microsoft 365 Group `-d, --description [description]` : Description for the Microsoft 365 Group @@ -72,7 +75,7 @@ Options `allowExternalSenders` and `autoSubscribeNewMembers` can only be set usi Update Microsoft 365 Group display name. ```sh -m365 entra m365group set --id 28beab62-7540-4db1-a23f-29a6018a3848 --displayName Finance +m365 entra m365group set --id 28beab62-7540-4db1-a23f-29a6018a3848 --newDisplayName Finance ``` Change Microsoft 365 Group visibility to public. @@ -81,16 +84,16 @@ Change Microsoft 365 Group visibility to public. m365 entra m365group set --id 28beab62-7540-4db1-a23f-29a6018a3848 --isPrivate `false` ``` -Add new Microsoft 365 Group owners. +Add new Microsoft 365 Group owners of group. ```sh -m365 entra m365group set --id 28beab62-7540-4db1-a23f-29a6018a3848 --owners "DebraB@contoso.onmicrosoft.com,DiegoS@contoso.onmicrosoft.com" +m365 entra m365group set --displayName 'Project Team' --owners "DebraB@contoso.onmicrosoft.com,DiegoS@contoso.onmicrosoft.com" ``` Add new Microsoft 365 Group members. ```sh -m365 entra m365group set --id 28beab62-7540-4db1-a23f-29a6018a3848 --members "DebraB@contoso.onmicrosoft.com,DiegoS@contoso.onmicrosoft.com" +m365 entra m365group set --displayName 'Project Team' --members "DebraB@contoso.onmicrosoft.com,DiegoS@contoso.onmicrosoft.com" ``` Update Microsoft 365 Group logo. diff --git a/docs/docs/v10-upgrade-guidance.mdx b/docs/docs/v10-upgrade-guidance.mdx index d49609a33fa..aa3893aa47b 100644 --- a/docs/docs/v10-upgrade-guidance.mdx +++ b/docs/docs/v10-upgrade-guidance.mdx @@ -122,6 +122,14 @@ The deprecated option `userName` was removed from the command [entra m365group u In your scripts, replace every occurrence of the deprecated option `userName` with `userNames`. +### Enhanced `entra m365group set` command + +We've enhanced the [entra m365group set](./cmd/entra/m365group/m365group-set.mdx) command so that a group that we wish to update can also be retrieved by it's displayName. Before, this was only able by the group ID. We have replaced the original `displayName` option by `newDisplayName`. + +#### What action do I need to take? + +Make sure that if you are currently updating groups using the `displayName` option, you update your scripts to use the `newDisplayName` option instead. + ## SharePoint ### Updated `spo site appcatalog remove` options diff --git a/src/m365/entra/commands/m365group/m365group-set.spec.ts b/src/m365/entra/commands/m365group/m365group-set.spec.ts index e9e2f05fcd8..e2a3a235017 100644 --- a/src/m365/entra/commands/m365group/m365group-set.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-set.spec.ts @@ -98,7 +98,8 @@ describe(commands.M365GROUP_SET, () => { fs.readFileSync, fs.existsSync, fs.lstatSync, - accessToken.isAppOnlyAccessToken + accessToken.isAppOnlyAccessToken, + entraGroup.getGroupIdByDisplayName ]); }); @@ -125,21 +126,19 @@ describe(commands.M365GROUP_SET, () => { assert.deepStrictEqual(alias, [aadCommands.M365GROUP_SET]); }); - it('updates Microsoft 365 Group display name', async () => { - sinon.stub(request, 'patch').callsFake(async (opts) => { - if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848') { - if (JSON.stringify(opts.data) === JSON.stringify({ - displayName: 'My group' - })) { - return; - } + it('updates Microsoft 365 Group display name while group is being retrieved by display name', async () => { + const groupName = 'Project A'; + sinon.stub(entraGroup, 'getGroupIdByDisplayName').withArgs(groupName).resolves(groupId); + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}`) { + return; } throw 'Invalid request'; }); - await command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', displayName: 'My group' } }); - assert(loggerLogSpy.notCalled); + await command.action(logger, { options: { displayName: groupName, newDisplayName: 'My group', verbose: true } }); + assert(patchStub.calledOnce); }); it('updates Microsoft 365 Group description (debug)', async () => { @@ -497,7 +496,7 @@ describe(commands.M365GROUP_SET, () => { } }); - await assert.rejects(command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', displayName: 'My group' } } as any), + await assert.rejects(command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', newDisplayName: 'My group' } } as any), new CommandError('An error has occurred')); }); @@ -505,7 +504,7 @@ describe(commands.M365GROUP_SET, () => { sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { id: groupId, displayName: 'Updated title' } }), + await assert.rejects(command.action(logger, { options: { id: groupId, newDisplayName: 'Updated title' } }), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); @@ -532,7 +531,7 @@ describe(commands.M365GROUP_SET, () => { }); it('passes validation when the id is a valid GUID and displayName specified', async () => { - const actual = await command.validate({ options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', displayName: 'My group' } }, commandInfo); + const actual = await command.validate({ options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', newDisplayName: 'My group' } }, commandInfo); assert.strictEqual(actual, true); }); @@ -622,7 +621,7 @@ describe(commands.M365GROUP_SET, () => { sinon.stub(stats, 'isDirectory').returns(false); sinon.stub(fs, 'existsSync').returns(true); sinon.stub(fs, 'lstatSync').returns(stats); - const actual = await command.validate({ options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', displayName: 'Title', description: 'Description', logoPath: 'logo.png', owners: 'john@contoso.com', members: 'doe@contoso.com', isPrivate: false, allowExternalSenders: false, autoSubscribeNewMembers: false, hideFromAddressLists: false, hideFromOutlookClients: false } }, commandInfo); + const actual = await command.validate({ options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', newDisplayName: 'Title', description: 'Description', logoPath: 'logo.png', owners: 'john@contoso.com', members: 'doe@contoso.com', isPrivate: false, allowExternalSenders: false, autoSubscribeNewMembers: false, hideFromAddressLists: false, hideFromOutlookClients: false } }, commandInfo); assert.strictEqual(actual, true); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-set.ts b/src/m365/entra/commands/m365group/m365group-set.ts index cb4a19e09c1..aec57984f6d 100644 --- a/src/m365/entra/commands/m365group/m365group-set.ts +++ b/src/m365/entra/commands/m365group/m365group-set.ts @@ -18,8 +18,9 @@ interface CommandArgs { } export interface Options extends GlobalOptions { - id: string; + id?: string; displayName?: string; + newDisplayName?: string; description?: string; owners?: string; members?: string; @@ -52,14 +53,17 @@ class EntraM365GroupSetCommand extends GraphCommand { this.#initTelemetry(); this.#initOptions(); - this.#initTypes(); 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', + newDisplayName: typeof args.options.newDisplayName !== 'undefined', description: typeof args.options.description !== 'undefined', owners: typeof args.options.owners !== 'undefined', members: typeof args.options.members !== 'undefined', @@ -76,11 +80,14 @@ class EntraM365GroupSetCommand extends GraphCommand { #initOptions(): void { this.options.unshift( { - option: '-i, --id ' + option: '-i, --id [id]' }, { option: '-n, --displayName [displayName]' }, + { + option: '--newDisplayName [newDisplayName]' + }, { option: '-d, --description [description]' }, @@ -116,15 +123,19 @@ class EntraM365GroupSetCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + #initTypes(): void { this.types.boolean.push('isPrivate', 'allowEternalSenders', 'autoSubscribeNewMembers', 'hideFromAddressLists', 'hideFromOutlookClients'); - this.types.string.push('id', 'displayName', 'description', 'owners', 'members', 'logoPath'); + this.types.string.push('id', 'displayName', 'newDisplayName', 'description', 'owners', 'members', 'logoPath'); } #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (!args.options.displayName && + if (!args.options.newDisplayName && args.options.description === undefined && !args.options.members && !args.options.owners && @@ -137,7 +148,7 @@ class EntraM365GroupSetCommand extends GraphCommand { return 'Specify at least one option to update.'; } - if (!validation.isValidGuid(args.options.id)) { + if (args.options.id && !validation.isValidGuid(args.options.id)) { return `${args.options.id} is not a valid GUID`; } @@ -180,19 +191,20 @@ class EntraM365GroupSetCommand extends GraphCommand { throw `Option 'allowExternalSenders' and 'autoSubscribeNewMembers' can only be used when using delegated permissions.`; } - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.id); + const groupId = args.options.id || await entraGroup.getGroupIdByDisplayName(args.options.displayName!); + const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.id}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } if (this.verbose) { - await logger.logToStderr(`Updating Microsoft 365 Group ${args.options.id}...`); + await logger.logToStderr(`Updating Microsoft 365 Group ${args.options.id || args.options.displayName}...`); } - if (args.options.displayName || args.options.description !== undefined || args.options.isPrivate !== undefined) { + if (args.options.newDisplayName || args.options.description !== undefined || args.options.isPrivate !== undefined) { const update: Group = { - displayName: args.options.displayName, + displayName: args.options.newDisplayName, description: args.options.description !== '' ? args.options.description : null }; @@ -201,7 +213,7 @@ class EntraM365GroupSetCommand extends GraphCommand { } const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/groups/${args.options.id}`, + url: `${this.resource}/v1.0/groups/${groupId}`, headers: { 'accept': 'application/json;odata.metadata=none' }, @@ -222,7 +234,7 @@ class EntraM365GroupSetCommand extends GraphCommand { }; const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/groups/${args.options.id}`, + url: `${this.resource}/v1.0/groups/${groupId}`, headers: { accept: 'application/json;odata.metadata=none' }, @@ -239,7 +251,7 @@ class EntraM365GroupSetCommand extends GraphCommand { } const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/groups/${args.options.id}/photo/$value`, + url: `${this.resource}/v1.0/groups/${groupId}/photo/$value`, headers: { 'content-type': this.getImageContentType(fullPath) }, @@ -270,7 +282,7 @@ class EntraM365GroupSetCommand extends GraphCommand { const res = await request.get<{ value: { id: string; }[] }>(requestOptions); await Promise.all(res.value.map(u => request.post({ - url: `${this.resource}/v1.0/groups/${args.options.id}/owners/$ref`, + url: `${this.resource}/v1.0/groups/${groupId}/owners/$ref`, headers: { 'content-type': 'application/json' }, @@ -302,7 +314,7 @@ class EntraM365GroupSetCommand extends GraphCommand { const res = await request.get<{ value: { id: string; }[] }>(requestOptions); await Promise.all(res.value.map(u => request.post({ - url: `${this.resource}/v1.0/groups/${args.options.id}/members/$ref`, + url: `${this.resource}/v1.0/groups/${groupId}/members/$ref`, headers: { 'content-type': 'application/json' },