From 4e0ea2e1d80d62d4715f63bac71cb7bf7feac20d Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 9 Aug 2024 09:10:08 +0200 Subject: [PATCH 1/6] All entra m365group commands should accept displayName option --- .../m365group/m365group-conversation-list.mdx | 15 ++- .../cmd/entra/m365group/m365group-get.mdx | 13 +- .../cmd/entra/m365group/m365group-remove.mdx | 11 +- .../cmd/entra/m365group/m365group-renew.mdx | 13 +- .../cmd/entra/m365group/m365group-teamify.mdx | 15 ++- .../entra/m365group/m365group-user-add.mdx | 4 +- .../entra/m365group/m365group-user-remove.mdx | 12 +- .../entra/m365group/m365group-user-set.mdx | 6 +- .../m365group-conversation-list.spec.ts | 22 +++- .../m365group/m365group-conversation-list.ts | 36 +++++- .../commands/m365group/m365group-get.spec.ts | 92 +++++++++++++- .../entra/commands/m365group/m365group-get.ts | 35 ++++-- .../m365group/m365group-remove.spec.ts | 17 ++- .../commands/m365group/m365group-remove.ts | 37 ++++-- .../m365group/m365group-renew.spec.ts | 26 +++- .../commands/m365group/m365group-renew.ts | 33 +++-- .../m365group/m365group-teamify.spec.ts | 118 +++++++++++++++++- .../commands/m365group/m365group-teamify.ts | 38 +++--- .../m365group/m365group-user-add.spec.ts | 1 + .../commands/m365group/m365group-user-add.ts | 13 +- .../m365group/m365group-user-remove.spec.ts | 52 +++++++- .../m365group/m365group-user-remove.ts | 9 ++ .../m365group/m365group-user-set.spec.ts | 1 + .../commands/m365group/m365group-user-set.ts | 10 ++ src/utils/entraGroup.spec.ts | 75 +++++++++++ src/utils/entraGroup.ts | 22 ++++ 26 files changed, 631 insertions(+), 95 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx index 1cf4dbcdde7..a33adddd102 100644 --- a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx @@ -15,20 +15,29 @@ m365 entra m365group conversation list [options] ## Options ```md definition-list -`-i, --groupId ` -: The ID of the Microsoft 365 group +`-i, --groupId [groupId]` +: The ID of the Microsoft 365 Group. Specify either `groupId` or `groupDisplayName`, but not both. + +`-n, --groupDisplayName [groupDisplayName]` +: Display name of the Microsoft 365 Group. Specify either `groupId` or `groupDisplayName`, but not both. ``` ## Examples -Lists conversations for the specified Microsoft 365 group +Lists conversations for the Microsoft 365 group specified by id. ```sh m365 entra m365group conversation list --groupId '00000000-0000-0000-0000-000000000000' ``` +Lists conversations for the Microsoft 365 group specified by displayName. + +```sh +m365 entra m365group conversation list --groupDisplayName Finance +``` + ## Response diff --git a/docs/docs/cmd/entra/m365group/m365group-get.mdx b/docs/docs/cmd/entra/m365group/m365group-get.mdx index f7663c58ca3..c1fd7f619a8 100644 --- a/docs/docs/cmd/entra/m365group/m365group-get.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-get.mdx @@ -15,8 +15,11 @@ m365 entra m365group get [options] ## Options ```md definition-list -`-i, --id ` -: The ID of the Microsoft 365 Group or Microsoft Teams team to retrieve information for +`-i, --id [id]` +: The ID of the Microsoft 365 Group or Microsoft Teams team to retrieve information for. Specify either `id` or `displayName`, but not both. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group or Microsoft Teams team to retrieve information for. Specify either `id` or `displayName`, but not both. `--includeSiteUrl` : Set to retrieve the site URL for the group @@ -32,6 +35,12 @@ Get information about the Microsoft 365 Group with id _1caf7dcd-7e83-4c3a-94f7-9 m365 entra m365group get --id 1caf7dcd-7e83-4c3a-94f7-932a1299c844 ``` +Get information about the Microsoft 365 Group with displayName _Finance_ + +```sh +m365 entra m365group get --displayName Finance +``` + Get information about the Microsoft 365 Group with id _1caf7dcd-7e83-4c3a-94f7-932a1299c844_ and also retrieve the URL of the corresponding SharePoint site ```sh diff --git a/docs/docs/cmd/entra/m365group/m365group-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-remove.mdx index 3b65202a7a2..e05eb90eee9 100644 --- a/docs/docs/cmd/entra/m365group/m365group-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-remove.mdx @@ -13,8 +13,11 @@ m365 entra m365group remove [options] ## Options ```md definition-list -`-i, --id ` -: The ID of the Microsoft 365 Group to remove +`-i, --id [id]` +: The ID of the Microsoft 365 Group to remove. Specify either `id` or `displayName`, but not both. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group to remove. Specify either `id` or `displayName`, but not both. `-f, --force` : Don't prompt for confirming removing the group @@ -47,10 +50,10 @@ Remove group with id _28beab62-7540-4db1-a23f-29a6018a3848_. Will prompt for con m365 entra m365group remove --id 28beab62-7540-4db1-a23f-29a6018a3848 ``` -Remove group with id _28beab62-7540-4db1-a23f-29a6018a3848_ without prompting for confirmation +Remove group with displayName _Finance_ without prompting for confirmation ```sh -m365 entra m365group remove --id 28beab62-7540-4db1-a23f-29a6018a3848 --force +m365 entra m365group remove --displayName 28beab62-7540-4db1-a23f-29a6018a3848 --force ``` Remove group with id _28beab62-7540-4db1-a23f-29a6018a3848_ without prompting for confirmation and without moving it to the Recycle Bin diff --git a/docs/docs/cmd/entra/m365group/m365group-renew.mdx b/docs/docs/cmd/entra/m365group/m365group-renew.mdx index bafec1f8263..c67fe162964 100644 --- a/docs/docs/cmd/entra/m365group/m365group-renew.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-renew.mdx @@ -13,8 +13,11 @@ m365 entra m365group renew [options] ## Options ```md definition-list -`-i, --id ` -: The ID of the Microsoft 365 group to renew +`-i, --id [id]` +: The ID of the Microsoft 365 group to renew. Specify either `id` or `displayName`, but not both. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group to renew. Specify either `id` or `displayName`, but not both. ``` @@ -31,6 +34,12 @@ Renew the Microsoft 365 group with id _28beab62-7540-4db1-a23f-29a6018a3848_ m365 entra m365group renew --id 28beab62-7540-4db1-a23f-29a6018a3848 ``` +Renew the Microsoft 365 group with displayName _Finance_ + +```sh +m365 entra m365group renew --displayName Finance +``` + ## Response The command won't return a response on success. diff --git a/docs/docs/cmd/entra/m365group/m365group-teamify.mdx b/docs/docs/cmd/entra/m365group/m365group-teamify.mdx index 4f31bb9f458..093eec6476e 100644 --- a/docs/docs/cmd/entra/m365group/m365group-teamify.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-teamify.mdx @@ -14,10 +14,13 @@ m365 entra m365group teamify [options] ```md definition-list `-i, --id [id]` -: The ID of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id` or `mailNickname` but not both. +: The ID of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id`, `displayName` or `mailNickname`, but not multiple. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id`, `displayName` or `mailNickname`, but not multiple. `--mailNickname [mailNickname]` -: The mail alias of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id` or `mailNickname` but not both. +: The mail alias of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id`, `displayName` or `mailNickname`, but not multiple. ``` @@ -30,7 +33,13 @@ Creates a new Microsoft Teams team under existing Microsoft 365 group with the s m365 entra m365group teamify --id e3f60f99-0bad-481f-9e9f-ff0f572fbd03 ``` -Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified mailNickname. +Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified displayName. + +```sh +m365 entra m365group teamify --displayName Finance +``` + +Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified mailNickname._ ```sh m365 entra m365group teamify --mailNickname GroupName diff --git a/docs/docs/cmd/entra/m365group/m365group-user-add.mdx b/docs/docs/cmd/entra/m365group/m365group-user-add.mdx index 8f708e970a7..b90ea8e9f77 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-add.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-add.mdx @@ -13,7 +13,7 @@ m365 entra m365group user add [options] ## Alias ```sh -m365 teams user add +m365 teams user add [options] ``` ## Options @@ -38,7 +38,7 @@ m365 teams user add : The user principal names of users. You can also pass a comma-separated list of UPNs. Specify either `ids` or `userNames` but not both. `-r, --role [role]` -: The role to be assigned to the new user: `Owner,Member`. Default `Member`. +: The role to be assigned to the new user. Allowed values: `Owner`, `Member`. Default `Member`. ``` diff --git a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx index 930fc5df09f..2b85c22bc5b 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx @@ -14,16 +14,16 @@ m365 entra m365group user remove [options] ```md definition-list `-i, --groupId [groupId]` -: The ID of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. +: The ID of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. -`--groupName [groupName]` -: The display name of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`.. +`--groupDisplayName [groupDisplayName]` +: The display name of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. `--teamId [teamId]` -: The ID of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. +: The ID of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. `--teamName [teamName]` -: The display name of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. +: The display name of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. `-n, --userName [userName]` : (deprecated) User's UPN (user principal name), eg. `johndoe@example.com`.. Specify only one of the following: `userName`, `ids` or `userNames`. @@ -67,7 +67,7 @@ m365 entra m365group user remove --teamName 'Project Team' --userNames 'anne.mat Removes users specified by a comma separated list of user ids from the specified Microsoft 365 group specified by name. ```sh -m365 entra m365group user remove --groupName 'Project Team' --ids '5b8e4cb1-ea40-484b-a94e-02a4313fefb4,be7a56d8-b045-4938-af35-917ab6e5309f' +m365 entra m365group user remove --groupDisplayName 'Project Team' --ids '5b8e4cb1-ea40-484b-a94e-02a4313fefb4,be7a56d8-b045-4938-af35-917ab6e5309f' ``` ## Response diff --git a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx index b5b60faf6b6..32deed6bba5 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx @@ -32,7 +32,7 @@ m365 entra m365group user set [options] : The user principal names of users. You can also pass a comma-separated list of UPNs. Specify only one of the following `ids` or `userNames`. `-r, --role ` -: Role to set for the given user in the specified Microsoft 365 Group or Microsoft Teams team. Allowed values: `Owner`, `Member` +: Role to set for the given user in the specified Microsoft 365 Group or Microsoft Teams team. Allowed values: `Owner`, `Member`. ``` @@ -70,13 +70,13 @@ m365 entra m365group user set --groupId '00000000-0000-0000-0000-000000000000' - Demote multiple users specified by the userNames parameter from Owner to Member of the given Microsoft Teams team ```sh -m365 entra teams user set --teamId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com,john.doe@contoso.onmicrosoft.com' --role Member +m365 teams user set --teamId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com,john.doe@contoso.onmicrosoft.com' --role Member ``` Demote multiple users specified by the ids parameter from Owner to Member in the given Microsoft Teams team ```sh -m365 entra teams user set --teamName 'Engineering' --ids '74a3b772-3122-447b-b9da-10895e238219,dd3d21e4-a142-46b9-8482-bca8fe9596b3' --role Member +m365 teams user set --teamName 'Engineering' --ids '74a3b772-3122-447b-b9da-10895e238219,dd3d21e4-a142-46b9-8482-bca8fe9596b3' --role Member ``` ## Response diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts index c9e5ae6745f..102f0f14565 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts @@ -50,6 +50,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); @@ -102,7 +103,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { assert.strictEqual(actual, true); }); - it('Retrieve conversations for the specified group by groupId in the tenant (verbose)', async () => { + it('Retrieve conversations for the group specified by groupId in the tenant (verbose)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/conversations`) { return jsonOutput; @@ -119,6 +120,25 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { jsonOutput.value )); }); + + it('Retrieve conversations for the group specified by groupDisplayName in the tenant (verbose)', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/conversations`) { + return jsonOutput; + } + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + verbose: true, groupDisplayName: "Finance" + } + }); + assert(loggerLogSpy.calledWith( + jsonOutput.value + )); + }); + it('correctly handles error when listing conversations', async () => { sinon.stub(request, 'get').rejects(new Error('An error has occurred')); diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.ts index 813dd35fe37..6699c439ac0 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.ts @@ -12,7 +12,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - groupId: string; + groupId?: string; + groupDisplayName?: string; } class EntraM365GroupConversationListCommand extends GraphCommand { @@ -33,12 +34,17 @@ class EntraM365GroupConversationListCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initOptions(): void { this.options.unshift( { - option: '-i, --groupId ' + option: '-i, --groupId [groupId]' + }, + { + option: '-n, --groupDisplayName [groupDisplayName]' } ); } @@ -46,7 +52,7 @@ class EntraM365GroupConversationListCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.groupId as string)) { + if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) { return `${args.options.groupId} is not a valid GUID`; } @@ -55,15 +61,33 @@ class EntraM365GroupConversationListCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['groupId', 'groupDisplayName'] }); + } + + #initTypes(): void { + this.types.string.push('groupId', 'groupDisplayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + await logger.logToStderr(`Retrieving conversations for Microsoft 365 Group: ${args.options.groupId || args.options.groupDisplayName}...`); + } + try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.groupId); + let groupId = args.options.groupId; + + if (args.options.groupDisplayName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); + } + + const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.groupId}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } - const conversations = await odata.getAllItems(`${this.resource}/v1.0/groups/${args.options.groupId}/conversations`); + const conversations = await odata.getAllItems(`${this.resource}/v1.0/groups/${groupId}/conversations`); await logger.log(conversations); } catch (err: any) { diff --git a/src/m365/entra/commands/m365group/m365group-get.spec.ts b/src/m365/entra/commands/m365group/m365group-get.spec.ts index 962e5824ade..5a322a5c5a5 100644 --- a/src/m365/entra/commands/m365group/m365group-get.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-get.spec.ts @@ -65,7 +65,7 @@ describe(commands.M365GROUP_GET, () => { assert.notStrictEqual(command.description, null); }); - it('retrieves information about the specified Microsoft 365 Group', async () => { + it('retrieves information about the Microsoft 365 Group specified by id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { return { @@ -126,6 +126,71 @@ describe(commands.M365GROUP_GET, () => { })); }); + it('retrieves information about the Microsoft 365 Group specified by displayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq 'Finance'`) { + return { + "value": [ + { + "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-11-29T03:27:05Z", + "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", + "displayName": "Finance", + "groupTypes": [ + "Unified" + ], + "mail": "finance@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "finance", + "onPremisesLastSyncDateTime": null, + "onPremisesProvisioningErrors": [], + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [ + "SMTP:finance@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2017-11-29T03:27:05Z", + "securityEnabled": false, + "visibility": "Public" + } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: 'Finance' } }); + assert(loggerLogSpy.calledWith({ + "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-11-29T03:27:05Z", + "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", + "displayName": "Finance", + "groupTypes": [ + "Unified" + ], + "mail": "finance@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "finance", + "onPremisesLastSyncDateTime": null, + "onPremisesProvisioningErrors": [], + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [ + "SMTP:finance@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2017-11-29T03:27:05Z", + "securityEnabled": false, + "visibility": "Public" + })); + }); + it('retrieves information about the specified Microsoft 365 Group (debug)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { @@ -414,12 +479,35 @@ describe(commands.M365GROUP_GET, () => { }); it('shows error when the group is not a unified group', async () => { + sinon.stub(entraGroup, 'getGroupById').resolves({ + "id": "3f04e370-cbc6-4091-80fe-1d038be2ad06", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-11-29T03:27:05Z", + "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", + "displayName": "Finance", + "groupTypes": [], + "mail": "finance@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "finance", + "onPremisesLastSyncDateTime": null, + "onPremisesProvisioningErrors": [], + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [ + "SMTP:finance@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2017-11-29T03:27:05Z", + "securityEnabled": false, + "visibility": "Public" + }); const groupId = '3f04e370-cbc6-4091-80fe-1d038be2ad06'; sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { id: groupId } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId } }), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-get.ts b/src/m365/entra/commands/m365group/m365group-get.ts index 6b4dc99bfe6..36d848673b0 100644 --- a/src/m365/entra/commands/m365group/m365group-get.ts +++ b/src/m365/entra/commands/m365group/m365group-get.ts @@ -12,7 +12,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + displayName?: string; includeSiteUrl: boolean; } @@ -30,12 +31,17 @@ class EntraM365GroupGetCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initOptions(): void { this.options.unshift( { - option: '-i, --id ' + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' }, { option: '--includeSiteUrl' @@ -46,7 +52,7 @@ class EntraM365GroupGetCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { + if (args.options.id && !validation.isValidGuid(args.options.id)) { return `${args.options.id} is not a valid GUID`; } @@ -55,18 +61,31 @@ class EntraM365GroupGetCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { let group: GroupExtended; try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.id); + if (args.options.id) { + group = await entraGroup.getGroupById(args.options.id); + } + else { + group = await entraGroup.getGroupByDisplayName(args.options.displayName!); + } + + const isUnifiedGroup = await entraGroup.isUnifiedGroup(group.id!); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.id}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${group.id}' is not a Microsoft 365 group.`); } - group = await entraGroup.getGroupById(args.options.id); - if (args.options.includeSiteUrl) { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/groups/${group.id}/drive?$select=webUrl`, @@ -77,7 +96,7 @@ class EntraM365GroupGetCommand extends GraphCommand { }; const res = await request.get<{ webUrl: string }>(requestOptions); - group.siteUrl = res.webUrl ? res.webUrl.substr(0, res.webUrl.lastIndexOf('/')) : ''; + group.siteUrl = res.webUrl ? res.webUrl.substring(0, res.webUrl.lastIndexOf('/')) : ''; } await logger.log(group); diff --git a/src/m365/entra/commands/m365group/m365group-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-remove.spec.ts index b2f5cdba427..2b4c08e26de 100644 --- a/src/m365/entra/commands/m365group/m365group-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-remove.spec.ts @@ -102,6 +102,7 @@ describe(commands.M365GROUP_REMOVE, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId); auth.connection.active = true; auth.connection.spoUrl = 'https://contoso.sharepoint.com'; commandInfo = cli.getCommandInfo(command); @@ -182,7 +183,7 @@ describe(commands.M365GROUP_REMOVE, () => { assert(getGroupSpy.notCalled); }); - it('deletes the group site for the sepcified group id when prompt confirmed', async () => { + it('deletes the group site for the group specified by id when prompt confirmed', async () => { defaultGetStub(); const deletedGroupSpy: sinon.SinonStub = defaultPostStub(); @@ -194,6 +195,18 @@ describe(commands.M365GROUP_REMOVE, () => { assert(loggerLogToStderrSpy.calledWith(`Deleting the group site: 'https://contoso.sharepoint.com/teams/sales'...`)); }); + it('deletes the group site for the group specified by displayName when prompt confirmed', async () => { + defaultGetStub(); + const deletedGroupSpy: sinon.SinonStub = defaultPostStub(); + + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { options: { displayName: 'Finance', verbose: true } }); + assert(deletedGroupSpy.calledOnce); + assert(loggerLogToStderrSpy.calledWith(`Deleting the group site: 'https://contoso.sharepoint.com/teams/sales'...`)); + }); + it('deletes the group without moving it to the Recycle Bin', async () => { defaultGetStub(); defaultPostStub(); @@ -303,7 +316,7 @@ describe(commands.M365GROUP_REMOVE, () => { sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { id: groupId, force: true } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId, force: true } }), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-remove.ts b/src/m365/entra/commands/m365group/m365group-remove.ts index c6384714180..6d0987617d8 100644 --- a/src/m365/entra/commands/m365group/m365group-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-remove.ts @@ -16,7 +16,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + displayName?: string; force?: boolean; skipRecycleBin: boolean; } @@ -39,6 +40,8 @@ class EntraM365GroupRemoveCommand extends GraphCommand { this.#initTelemetry(); this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initTelemetry(): void { @@ -53,7 +56,10 @@ class EntraM365GroupRemoveCommand extends GraphCommand { #initOptions(): void { this.options.unshift( { - option: '-i, --id ' + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' }, { option: '-f, --force' @@ -67,7 +73,7 @@ class EntraM365GroupRemoveCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { + if (args.options.id && !validation.isValidGuid(args.options.id)) { return `${args.options.id} is not a valid GUID`; } @@ -76,27 +82,40 @@ class EntraM365GroupRemoveCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { const removeGroup = async (): Promise => { if (this.verbose) { - await logger.logToStderr(`Removing Microsoft 365 Group: ${args.options.id}...`); + await logger.logToStderr(`Removing Microsoft 365 Group: ${args.options.id || args.options.displayName}...`); } try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.id); + let groupId = args.options.id; + + if (args.options.displayName) { + groupId = 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.`); } - const siteUrl = await this.getM365GroupSiteUrl(logger, args.options.id); + const siteUrl = await this.getM365GroupSiteUrl(logger, groupId!); const spoAdminUrl = await spo.getSpoAdminUrl(logger, this.debug); // Delete the Microsoft 365 group site. This operation will also delete the group. await this.deleteM365GroupSite(logger, siteUrl, spoAdminUrl); if (args.options.skipRecycleBin) { - await this.deleteM365GroupFromRecycleBin(logger, args.options.id); + await this.deleteM365GroupFromRecycleBin(logger, groupId!); await this.deleteSiteFromRecycleBin(logger, siteUrl, spoAdminUrl); } } @@ -109,7 +128,7 @@ class EntraM365GroupRemoveCommand extends GraphCommand { await removeGroup(); } else { - const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove the group ${args.options.id}?` }); + const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove the group ${args.options.id || args.options.displayName}?` }); if (result) { await removeGroup(); diff --git a/src/m365/entra/commands/m365group/m365group-renew.spec.ts b/src/m365/entra/commands/m365group/m365group-renew.spec.ts index 1eb96a99173..2d3bf71784c 100644 --- a/src/m365/entra/commands/m365group/m365group-renew.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-renew.spec.ts @@ -21,12 +21,15 @@ describe(commands.M365GROUP_RENEW, () => { let commandInfo: CommandInfo; let loggerLogToStderrSpy: sinon.SinonSpy; + const groupId = '28beab62-7540-4db1-a23f-29a6018a3848'; + before(() => { sinon.stub(auth, 'restoreAuth').resolves(); sinon.stub(telemetry, 'trackEvent').returns(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); @@ -68,7 +71,20 @@ describe(commands.M365GROUP_RENEW, () => { assert.notStrictEqual(command.description, null); }); - it('renews expiration the specified group', async () => { + it('renews expiration of a group specified by id', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848/renew/') { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { id: groupId, verbose: true } }); + assert(loggerLogSpy.notCalled); + }); + + it('renews expiration of a group specified by displayName', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848/renew/') { return; @@ -77,7 +93,7 @@ describe(commands.M365GROUP_RENEW, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848' } }); + await command.action(logger, { options: { displayName: 'Finance', verbose: true } }); assert(loggerLogSpy.notCalled); }); @@ -90,14 +106,14 @@ describe(commands.M365GROUP_RENEW, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848' } }); + await command.action(logger, { options: { debug: true, id: groupId } }); assert(loggerLogToStderrSpy.called); }); it('correctly handles error when group is not found', async () => { sinon.stub(request, 'post').rejects({ error: { 'odata.error': { message: { value: 'The remote server returned an error: (404) Not Found.' } } } }); - await assert.rejects(command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848' } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId } }), new CommandError('The remote server returned an error: (404) Not Found.')); }); @@ -128,7 +144,7 @@ describe(commands.M365GROUP_RENEW, () => { sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { id: groupId } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId } }), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-renew.ts b/src/m365/entra/commands/m365group/m365group-renew.ts index aa251b235ca..36f47f6df0a 100644 --- a/src/m365/entra/commands/m365group/m365group-renew.ts +++ b/src/m365/entra/commands/m365group/m365group-renew.ts @@ -11,7 +11,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + displayName?: string; } class EntraM365GroupRenewCommand extends GraphCommand { @@ -28,12 +29,17 @@ class EntraM365GroupRenewCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initOptions(): void { this.options.unshift( { - option: '-i, --id ' + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' } ); } @@ -41,7 +47,7 @@ class EntraM365GroupRenewCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { + if (args.options.id && !validation.isValidGuid(args.options.id)) { return `${args.options.id} is not a valid GUID`; } @@ -50,20 +56,33 @@ class EntraM365GroupRenewCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { if (this.verbose) { - await logger.logToStderr(`Renewing Microsoft 365 group's expiration: ${args.options.id}...`); + await logger.logToStderr(`Renewing Microsoft 365 group's expiration: ${args.options.id || args.options.displayName}...`); } try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.id); + let groupId = args.options.id; + + if (args.options.displayName) { + groupId = 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.`); } const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/groups/${args.options.id}/renew/`, + url: `${this.resource}/v1.0/groups/${groupId}/renew/`, headers: { 'accept': 'application/json;odata.metadata=none' } diff --git a/src/m365/entra/commands/m365group/m365group-teamify.spec.ts b/src/m365/entra/commands/m365group/m365group-teamify.spec.ts index 727be4534ed..473c66a68c2 100644 --- a/src/m365/entra/commands/m365group/m365group-teamify.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-teamify.spec.ts @@ -68,7 +68,7 @@ describe(commands.M365GROUP_TEAMIFY, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if both id and mailNickname options are not passed', async () => { + it('fails validation if id, displayName and mailNickname options are not passed', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return false; @@ -84,7 +84,7 @@ describe(commands.M365GROUP_TEAMIFY, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if both id and mailNickname options are passed', async () => { + it('fails validation if id, displayName and mailNickname options are passed', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return false; @@ -124,7 +124,7 @@ describe(commands.M365GROUP_TEAMIFY, () => { debug: true, mailNickname: 'GroupName' } - }), new CommandError(`The specified Microsoft 365 Group does not exist`)); + }), new CommandError(`The specified group 'GroupName' does not exist.`)); }); it('fails when multiple groups with same name exists', async () => { @@ -236,12 +236,12 @@ describe(commands.M365GROUP_TEAMIFY, () => { debug: true, mailNickname: 'GroupName' } - }), new CommandError("Multiple Microsoft 365 Groups with name 'GroupName' found. Found: 00000000-0000-0000-0000-000000000000.")); + }), new CommandError("Multiple groups with mail nickname 'GroupName' found. Found: 00000000-0000-0000-0000-000000000000.")); }); it('handles selecting single result when multiple groups with the specified name found and cli is set to prompt', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === "https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq 'groupname'") { + if (opts.url === "https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq 'groupname'&$select=id") { return { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups", "value": [ @@ -593,6 +593,114 @@ describe(commands.M365GROUP_TEAMIFY, () => { assert.strictEqual(requestStub.lastCall.args[0].url, 'https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/team'); }); + it('Teamify M365 group by displayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/v1.0/groups?$filter=displayName eq `) > -1) { + return { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups", + "value": [ + { + "@odata.id": "https://graph.microsoft.com/v2/00000000-0000-0000-0000-000000000000/directoryObjects/00000000-0000-0000-0000-000000000000/Microsoft.DirectoryServices.Group", + "id": "00000000-0000-0000-0000-000000000000", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2021-09-05T09:01:19Z", + "creationOptions": [], + "description": "GroupName", + "displayName": "GroupName", + "expirationDateTime": null, + "groupTypes": [ + "Unified" + ], + "isAssignableToRole": null, + "mail": "groupname@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "groupname", + "membershipRule": null, + "membershipRuleProcessingState": null, + "onPremisesDomainName": null, + "onPremisesLastSyncDateTime": null, + "onPremisesNetBiosName": null, + "onPremisesSamAccountName": null, + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "preferredLanguage": null, + "proxyAddresses": [ + "SPO:SPO_00000000-0000-0000-0000-000000000000@SPO_00000000-0000-0000-0000-000000000000", + "SMTP:groupname@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2021-09-05T09:01:19Z", + "resourceBehaviorOptions": [], + "resourceProvisioningOptions": [ + "Team" + ], + "securityEnabled": false, + "securityIdentifier": "S-1-12-1-71288816-1279290235-2033184675-371261341", + "theme": null, + "visibility": "Public", + "onPremisesProvisioningErrors": [] + } + ] + }; + } + + throw 'Invalid request'; + }); + + const requestStub: sinon.SinonStub = sinon.stub(request, 'put').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/team`) { + return { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams/$entity", + "id": "00000000-0000-0000-0000-000000000000", + "createdDateTime": null, + "displayName": "Group Team", + "description": "Group Team description", + "internalId": "19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01@thread.tacv2", + "classification": null, + "specialization": null, + "mailNickname": "groupname", + "visibility": "public", + "webUrl": "https://teams.microsoft.com/l/team/19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01%40thread.tacv2/conversations?groupId=00000000-0000-0000-0000-000000000000&tenantId=3a7a651b-2620-433b-a1a3-42de27ae94e8", + "isArchived": null, + "isMembershipLimitedToOwners": false, + "discoverySettings": null, + "memberSettings": { + "allowCreateUpdateChannels": true, + "allowCreatePrivateChannels": true, + "allowDeleteChannels": true, + "allowAddRemoveApps": true, + "allowCreateUpdateRemoveTabs": true, + "allowCreateUpdateRemoveConnectors": true + }, + "guestSettings": { + "allowCreateUpdateChannels": false, + "allowDeleteChannels": false + }, + "messagingSettings": { + "allowUserEditMessages": true, + "allowUserDeleteMessages": true, + "allowOwnerDeleteMessages": true, + "allowTeamMentions": true, + "allowChannelMentions": true + }, + "funSettings": { + "allowGiphy": true, + "giphyContentRating": "moderate", + "allowStickersAndMemes": true, + "allowCustomMemes": true + } + }; + } + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { displayName: 'GroupName' } + }); + assert.strictEqual(requestStub.lastCall.args[0].url, 'https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/team'); + }); + it('should handle Microsoft graph error response', async () => { sinon.stub(request, 'put').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/8231f9f2-701f-4c6e-93ce-ecb563e3c1ee/team`) { diff --git a/src/m365/entra/commands/m365group/m365group-teamify.ts b/src/m365/entra/commands/m365group/m365group-teamify.ts index de91436791a..0f71895ae41 100644 --- a/src/m365/entra/commands/m365group/m365group-teamify.ts +++ b/src/m365/entra/commands/m365group/m365group-teamify.ts @@ -1,4 +1,3 @@ -import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; @@ -14,6 +13,7 @@ interface CommandArgs { interface Options extends GlobalOptions { id?: string; + displayName?: string; mailNickname?: string; } @@ -33,13 +33,15 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { 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', - mailNickname: typeof args.options.mailNickname !== 'undefined' + mailNickname: typeof args.options.mailNickname !== 'undefined', + displayName: typeof args.options.displayName !== 'undefined' }); }); } @@ -49,6 +51,9 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { { option: '-i, --id [id]' }, + { + option: '-n, --displayName [displayName]' + }, { option: '--mailNickname [mailNickname]' } @@ -68,7 +73,11 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'mailNickname'] }); + this.optionSets.push({ options: ['id', 'dispalyName', 'mailNickname'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName', 'mailNickname'); } private async getGroupId(options: Options): Promise { @@ -76,28 +85,11 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { return options.id; } - const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(options.mailNickname as string)}'`, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }; - - const response = await request.get<{ value: [{ id: string }] }>(requestOptions); - const groupItem: { id: string } | undefined = response.value[0]; - - if (!groupItem) { - throw `The specified Microsoft 365 Group does not exist`; - } - - if (response.value.length > 1) { - const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', response.value); - const result = await cli.handleMultipleResultsFound<{ id: string }>(`Multiple Microsoft 365 Groups with name '${options.mailNickname}' found.`, resultAsKeyValuePair); - return result.id; + if (options.displayName) { + return await entraGroup.getGroupIdByDisplayName(options.displayName); } - return groupItem.id; + return await entraGroup.getGroupIdByMailNickname(options.mailNickname!); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/m365group/m365group-user-add.spec.ts b/src/m365/entra/commands/m365group/m365group-user-add.spec.ts index e27c9624cbf..5c418fa664f 100644 --- a/src/m365/entra/commands/m365group/m365group-user-add.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-add.spec.ts @@ -31,6 +31,7 @@ describe(commands.M365GROUP_USER_ADD, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-add.ts b/src/m365/entra/commands/m365group/m365group-user-add.ts index 0d43d3e3d7f..f14cb5c10f7 100644 --- a/src/m365/entra/commands/m365group/m365group-user-add.ts +++ b/src/m365/entra/commands/m365group/m365group-user-add.ts @@ -20,6 +20,7 @@ interface Options extends GlobalOptions { teamId?: string; teamName?: string; role?: string; + groupDisplayName?: string; } class EntraM365GroupUserAddCommand extends GraphCommand { @@ -132,8 +133,18 @@ class EntraM365GroupUserAddCommand extends GraphCommand { const providedGroupId: string = await this.getGroupId(logger, args); const isUnifiedGroup = await entraGroup.isUnifiedGroup(providedGroupId); + let groupId = args.options.groupId; + + if (args.options.groupDisplayName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); + } + else if (args.options.teamId) { + groupId = args.options.teamId; + } + const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); + if (!isUnifiedGroup) { - throw Error(`Specified group with id '${providedGroupId}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } const userIds: string[] = await this.getUserIds(logger, args.options.ids, args.options.userNames); diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts index 5c9345823fd..f4904ed91d5 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts @@ -31,6 +31,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinon.stub(telemetry, 'trackEvent').returns(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); @@ -193,7 +194,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { assert(postSpy.notCalled); }); - it('removes the specified owner from owners and members endpoint of the specified Microsoft 365 Group with accepted prompt', async () => { + it('removes the specified owner from owners and members endpoint of the Microsoft 365 Group specified by id with accepted prompt', async () => { let memberDeleteCallIssued = false; sinon.stub(request, 'delete').callsFake(async (opts) => { @@ -217,6 +218,55 @@ describe(commands.M365GROUP_USER_REMOVE, () => { assert(memberDeleteCallIssued); }); + it('removes the specified owner from owners and members endpoint of the Microsoft 365 Group specified by groupDisplayName with accepted prompt', async () => { + let memberDeleteCallIssued = false; + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { + return { + "value": "00000000-0000-0000-0000-000000000001" + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { + return { + response: { + status: 200, + data: { + value: "00000000-0000-0000-0000-000000000000" + } + } + }; + } + + throw 'Invalid request'; + }); + + sinon.stub(request, 'delete').callsFake(async (opts) => { + memberDeleteCallIssued = true; + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] + }; + } + + throw 'Invalid request'; + }); + + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { options: { groupDisplayName: "Finance", userName: "anne.matthews@contoso.onmicrosoft.com" } }); + assert(memberDeleteCallIssued); + }); + it('removes the specified owner from owners and members endpoint of the specified Microsoft 365 Group when prompt confirmed', async () => { let memberDeleteCallIssued = false; diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.ts b/src/m365/entra/commands/m365group/m365group-user-remove.ts index 380cc76c50e..29725616e77 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.ts @@ -142,6 +142,15 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { await this.warn(logger, `Option 'userName' is deprecated. Please use 'ids' or 'userNames' instead.`); } + let groupId = args.options.groupId; + + if (args.options.groupDisplayName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); + } + else if (args.options.teamId) { + groupId = args.options.teamId; + } + const removeUser = async (): Promise => { try { const groupId: string = await this.getGroupId(logger, args.options.groupId, args.options.teamId, args.options.groupName, args.options.teamName); diff --git a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts index 75377fe0d57..913ede5f202 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts @@ -30,6 +30,7 @@ describe(commands.M365GROUP_USER_SET, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-set.ts b/src/m365/entra/commands/m365group/m365group-user-set.ts index 063876dec42..b523771f43e 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.ts @@ -131,6 +131,16 @@ class EntraM365GroupUserSetCommand extends GraphCommand { const groupId: string = await this.getGroupId(logger, args); const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); + let groupId = args.options.groupId; + + if (args.options.groupDisplayName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); + } + else if (args.options.teamId) { + groupId = args.options.teamId; + } + const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); + if (!isUnifiedGroup) { throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } diff --git a/src/utils/entraGroup.spec.ts b/src/utils/entraGroup.spec.ts index 97d60fc8f2f..2ed095c8dbe 100644 --- a/src/utils/entraGroup.spec.ts +++ b/src/utils/entraGroup.spec.ts @@ -10,6 +10,7 @@ import { settingsNames } from '../settingsNames.js'; const validGroupName = 'Group name'; const validGroupId = '00000000-0000-0000-0000-000000000000'; +const validGroupMailNickname = 'GroupName'; const singleGroupResponse = { id: validGroupId, @@ -185,6 +186,80 @@ describe('utils/entraGroup', () => { assert.deepStrictEqual(actual, validGroupId); }); + it('correctly get single group id by name using getGroupIdByMailNickname', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { + value: [ + { id: singleGroupResponse.id } + ] + }; + } + + return 'Invalid Request'; + }); + + const actual = await entraGroup.getGroupIdByMailNickname(validGroupMailNickname); + assert.deepStrictEqual(actual, singleGroupResponse.id); + }); + + it('throws error message when no group was found using getGroupIdByMailNickname', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { value: [] }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(entraGroup.getGroupIdByMailNickname(validGroupMailNickname), Error(`The specified group '${validGroupMailNickname}' does not exist.`)); + }); + + it('throws error message when multiple groups were found using getGroupIdByMailNickname', 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/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { + value: [ + { id: validGroupId }, + { id: validGroupId } + ] + }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(entraGroup.getGroupIdByMailNickname(validGroupMailNickname), Error(`Multiple groups with mail nickname 'GroupName' found. Found: 00000000-0000-0000-0000-000000000000.`)); + }); + + it('handles selecting single result when multiple groups with the specified name found using getGroupIdByMailNickname and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { + value: [ + { id: validGroupId }, + { id: validGroupId } + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves({ id: validGroupId }); + + const actual = await entraGroup.getGroupIdByMailNickname(validGroupMailNickname); + assert.deepStrictEqual(actual, validGroupId); + }); + it('updates a group to public successfully', async () => { const patchStub = sinon.stub(request, 'patch').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/${validGroupId}`) { diff --git a/src/utils/entraGroup.ts b/src/utils/entraGroup.ts index fb516e48f39..92e0a372a79 100644 --- a/src/utils/entraGroup.ts +++ b/src/utils/entraGroup.ts @@ -75,6 +75,28 @@ export const entraGroup = { return groups[0].id!; }, + /** + * Get id of a group by its mail nickname. + * @param mailNickname Group mail nickname. + * @throws Error when group was not found. + * @throws Error when multiple groups with the same name were found. + */ + async getGroupIdByMailNickname(mailNickname: string): Promise { + const groups = await odata.getAllItems(`${graphResource}/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(mailNickname)}'&$select=id`); + + if (!groups.length) { + throw Error(`The specified group '${mailNickname}' does not exist.`); + } + + if (groups.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', groups); + const result = await cli.handleMultipleResultsFound(`Multiple groups with mail nickname '${mailNickname}' found.`, resultAsKeyValuePair); + return result.id!; + } + + return groups[0].id!; + }, + async setGroup(id: string, isPrivate: boolean, logger?: Logger, verbose?: boolean): Promise { if (verbose && logger) { await logger.logToStderr(`Updating Microsoft 365 Group ${id}...`); From 7c231841a23c0864a539a0a965f7db6a299932ad Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sat, 24 Aug 2024 22:15:33 +0200 Subject: [PATCH 2/6] All entra m365group commands should accept displayName option --- .../m365group/m365group-conversation-list.mdx | 8 ++++---- .../cmd/entra/m365group/m365group-user-list.mdx | 14 ++++++++++---- .../m365group/m365group-conversation-list.spec.ts | 4 ++-- .../m365group/m365group-conversation-list.ts | 14 +++++++------- .../entra/commands/m365group/m365group-user-add.ts | 13 +------------ .../commands/m365group/m365group-user-list.spec.ts | 8 ++++---- .../commands/m365group/m365group-user-list.ts | 12 ++++++------ .../m365group/m365group-user-remove.spec.ts | 4 ++-- .../commands/m365group/m365group-user-remove.ts | 4 ++-- .../entra/commands/m365group/m365group-user-set.ts | 10 ---------- 10 files changed, 38 insertions(+), 53 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx index a33adddd102..a9b0cfe1627 100644 --- a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx @@ -16,10 +16,10 @@ m365 entra m365group conversation list [options] ```md definition-list `-i, --groupId [groupId]` -: The ID of the Microsoft 365 Group. Specify either `groupId` or `groupDisplayName`, but not both. +: The ID of the Microsoft 365 Group. Specify either `groupId` or `groupName`, but not both. -`-n, --groupDisplayName [groupDisplayName]` -: Display name of the Microsoft 365 Group. Specify either `groupId` or `groupDisplayName`, but not both. +`-n, --groupName [groupName]` +: Display name of the Microsoft 365 Group. Specify either `groupId` or `groupName`, but not both. ``` @@ -35,7 +35,7 @@ m365 entra m365group conversation list --groupId '00000000-0000-0000-0000-000000 Lists conversations for the Microsoft 365 group specified by displayName. ```sh -m365 entra m365group conversation list --groupDisplayName Finance +m365 entra m365group conversation list --groupName Finance ``` ## Response diff --git a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx index 74524fe8469..2b65baf6760 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx @@ -16,10 +16,10 @@ m365 entra m365group user list [options] ```md definition-list `-i, --groupId [groupId]` -: The ID of the Microsoft 365 group. Specify `groupId` or `groupDisplayName` but not both. +: The ID of the Microsoft 365 group. Specify `groupId` or `groupName` but not both. -`-n, --groupDisplayName [groupDisplayName]` -: The display name of the Microsoft 365 group. Specify `groupId` or `groupDisplayName` but not both. +`-n, --groupName [groupName]` +: The display name of the Microsoft 365 group. Specify `groupId` or `groupName` but not both. `-r, --role [role]` : Filter the results to only users with the given role. Allowed values: `Owner`, `Member`. @@ -48,7 +48,7 @@ m365 entra m365group user list --groupId '00000000-0000-0000-0000-000000000000' List all owners from Microsoft 365 group specified by display name. ```sh -m365 entra m365group user list --groupDisplayName Developers --role Owner +m365 entra m365group user list --groupName Developers --role Owner ``` List specific properties for all group users from a group specified by ID. @@ -57,6 +57,12 @@ List specific properties for all group users from a group specified by ID. m365 entra m365group user list --groupId 03cba9da-3974-46c1-afaf-79caa2e45bbe --properties "id,jobTitle,companyName,accountEnabled" ``` +List all group members that are guest users. + +```sh +m365 entra m365group user list --groupName Developers --filter "userType eq 'Guest'" +``` + ## Response diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts index 102f0f14565..f8ac0902fd7 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts @@ -121,7 +121,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { )); }); - it('Retrieve conversations for the group specified by groupDisplayName in the tenant (verbose)', async () => { + it('Retrieve conversations for the group specified by groupName in the tenant (verbose)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/conversations`) { return jsonOutput; @@ -131,7 +131,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { await command.action(logger, { options: { - verbose: true, groupDisplayName: "Finance" + verbose: true, groupName: "Finance" } }); assert(loggerLogSpy.calledWith( diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.ts index 6699c439ac0..2db25b06e01 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.ts @@ -13,7 +13,7 @@ interface CommandArgs { interface Options extends GlobalOptions { groupId?: string; - groupDisplayName?: string; + groupName?: string; } class EntraM365GroupConversationListCommand extends GraphCommand { @@ -44,7 +44,7 @@ class EntraM365GroupConversationListCommand extends GraphCommand { option: '-i, --groupId [groupId]' }, { - option: '-n, --groupDisplayName [groupDisplayName]' + option: '-n, --groupName [groupName]' } ); } @@ -62,23 +62,23 @@ class EntraM365GroupConversationListCommand extends GraphCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['groupId', 'groupDisplayName'] }); + this.optionSets.push({ options: ['groupId', 'groupName'] }); } #initTypes(): void { - this.types.string.push('groupId', 'groupDisplayName'); + this.types.string.push('groupId', 'groupName'); } public async commandAction(logger: Logger, args: CommandArgs): Promise { if (this.verbose) { - await logger.logToStderr(`Retrieving conversations for Microsoft 365 Group: ${args.options.groupId || args.options.groupDisplayName}...`); + await logger.logToStderr(`Retrieving conversations for Microsoft 365 Group: ${args.options.groupId || args.options.groupName}...`); } try { let groupId = args.options.groupId; - if (args.options.groupDisplayName) { - groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); + if (args.options.groupName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupName); } const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); diff --git a/src/m365/entra/commands/m365group/m365group-user-add.ts b/src/m365/entra/commands/m365group/m365group-user-add.ts index f14cb5c10f7..0d43d3e3d7f 100644 --- a/src/m365/entra/commands/m365group/m365group-user-add.ts +++ b/src/m365/entra/commands/m365group/m365group-user-add.ts @@ -20,7 +20,6 @@ interface Options extends GlobalOptions { teamId?: string; teamName?: string; role?: string; - groupDisplayName?: string; } class EntraM365GroupUserAddCommand extends GraphCommand { @@ -133,18 +132,8 @@ class EntraM365GroupUserAddCommand extends GraphCommand { const providedGroupId: string = await this.getGroupId(logger, args); const isUnifiedGroup = await entraGroup.isUnifiedGroup(providedGroupId); - let groupId = args.options.groupId; - - if (args.options.groupDisplayName) { - groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); - } - else if (args.options.teamId) { - groupId = args.options.teamId; - } - const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); - if (!isUnifiedGroup) { - throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${providedGroupId}' is not a Microsoft 365 group.`); } const userIds: string[] = await this.getUserIds(logger, args.options.ids, args.options.userNames); diff --git a/src/m365/entra/commands/m365group/m365group-user-list.spec.ts b/src/m365/entra/commands/m365group/m365group-user-list.spec.ts index 73c329c0bf0..18d4a66562d 100644 --- a/src/m365/entra/commands/m365group/m365group-user-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-list.spec.ts @@ -179,7 +179,7 @@ describe(commands.M365GROUP_USER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true, groupDisplayName: "CLI Test Group" } }); + await command.action(logger, { options: { verbose: true, groupName: "CLI Test Group" } }); assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", @@ -394,12 +394,12 @@ describe(commands.M365GROUP_USER_LIST, () => { }); it('throws error when the group by name is not a unified group', async () => { - const groupDisplayName = 'CLI Test Group'; + const groupName = 'CLI Test Group'; sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { verbose: true, groupDisplayName: groupDisplayName } } as any), - new CommandError(`Specified group '${groupDisplayName}' is not a Microsoft 365 group.`)); + await assert.rejects(command.action(logger, { options: { verbose: true, groupName: groupName } } as any), + new CommandError(`Specified group '${groupName}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-list.ts b/src/m365/entra/commands/m365group/m365group-user-list.ts index 237da67859e..5fca3c0ac78 100644 --- a/src/m365/entra/commands/m365group/m365group-user-list.ts +++ b/src/m365/entra/commands/m365group/m365group-user-list.ts @@ -15,7 +15,7 @@ interface CommandArgs { interface Options extends GlobalOptions { filter?: string; groupId?: string; - groupDisplayName?: string; + groupName?: string; properties?: string; role?: string; } @@ -46,7 +46,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { groupId: typeof args.options.groupId !== 'undefined', - groupDisplayName: typeof args.options.groupDisplayName !== 'undefined', + groupName: typeof args.options.groupName !== 'undefined', role: typeof args.options.role !== 'undefined', properties: typeof args.options.properties !== 'undefined', filter: typeof args.options.filter !== 'undefined' @@ -60,7 +60,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { option: "-i, --groupId [groupId]" }, { - option: "-n, --groupDisplayName [groupDisplayName]" + option: "-n, --groupName [groupName]" }, { option: "-r, --role [type]", @@ -78,7 +78,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { #initOptionSets(): void { this.optionSets.push( { - options: ['groupId', 'groupDisplayName'] + options: ['groupId', 'groupName'] } ); } @@ -107,7 +107,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); if (!isUnifiedGroup) { - throw Error(`Specified group '${args.options.groupId || args.options.groupDisplayName}' is not a Microsoft 365 group.`); + throw Error(`Specified group '${args.options.groupId || args.options.groupName}' is not a Microsoft 365 group.`); } let users: ExtendedUser[] = []; @@ -151,7 +151,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { await logger.logToStderr('Retrieving Group Id...'); } - return await entraGroup.getGroupIdByDisplayName(options.groupDisplayName!); + return await entraGroup.getGroupIdByDisplayName(options.groupName!); } private async getUsers(options: Options, role: string, groupId: string, logger: Logger): Promise { diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts index f4904ed91d5..1550ba5f9dc 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts @@ -218,7 +218,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { assert(memberDeleteCallIssued); }); - it('removes the specified owner from owners and members endpoint of the Microsoft 365 Group specified by groupDisplayName with accepted prompt', async () => { + it('removes the specified owner from owners and members endpoint of the Microsoft 365 Group specified by groupName with accepted prompt', async () => { let memberDeleteCallIssued = false; sinon.stub(request, 'get').callsFake(async (opts) => { @@ -263,7 +263,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { groupDisplayName: "Finance", userName: "anne.matthews@contoso.onmicrosoft.com" } }); + await command.action(logger, { options: { groupName: "Finance", userName: "anne.matthews@contoso.onmicrosoft.com" } }); assert(memberDeleteCallIssued); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.ts b/src/m365/entra/commands/m365group/m365group-user-remove.ts index 29725616e77..13149eb0f65 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.ts @@ -144,8 +144,8 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { let groupId = args.options.groupId; - if (args.options.groupDisplayName) { - groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); + if (args.options.groupName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupName); } else if (args.options.teamId) { groupId = args.options.teamId; diff --git a/src/m365/entra/commands/m365group/m365group-user-set.ts b/src/m365/entra/commands/m365group/m365group-user-set.ts index b523771f43e..063876dec42 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.ts @@ -131,16 +131,6 @@ class EntraM365GroupUserSetCommand extends GraphCommand { const groupId: string = await this.getGroupId(logger, args); const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); - let groupId = args.options.groupId; - - if (args.options.groupDisplayName) { - groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupDisplayName); - } - else if (args.options.teamId) { - groupId = args.options.teamId; - } - const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); - if (!isUnifiedGroup) { throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } From 87ec74c8b2f89dd3abaf99c6af6f3306b92e3092 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 25 Aug 2024 10:48:37 +0200 Subject: [PATCH 3/6] All entra m365group commands should accept displayName option --- src/m365/entra/commands/m365group/m365group-user-add.spec.ts | 1 - src/m365/entra/commands/m365group/m365group-user-set.spec.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/m365/entra/commands/m365group/m365group-user-add.spec.ts b/src/m365/entra/commands/m365group/m365group-user-add.spec.ts index 5c418fa664f..e27c9624cbf 100644 --- a/src/m365/entra/commands/m365group/m365group-user-add.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-add.spec.ts @@ -31,7 +31,6 @@ describe(commands.M365GROUP_USER_ADD, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); - sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts index 913ede5f202..75377fe0d57 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts @@ -30,7 +30,6 @@ describe(commands.M365GROUP_USER_SET, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); - sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); From de1d242f7a6ff9bde6d51466f6326602b77deb26 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Thu, 17 Oct 2024 07:54:35 +0200 Subject: [PATCH 4/6] All entra m365group commands should accept displayName option --- .../entra/m365group/m365group-user-remove.mdx | 12 ++--- .../m365group/m365group-user-remove.spec.ts | 49 ------------------- .../m365group/m365group-user-remove.ts | 9 ---- 3 files changed, 6 insertions(+), 64 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx index 2b85c22bc5b..f4c34f757c2 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx @@ -14,16 +14,16 @@ m365 entra m365group user remove [options] ```md definition-list `-i, --groupId [groupId]` -: The ID of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. +: The ID of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. -`--groupDisplayName [groupDisplayName]` -: The display name of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. +`--groupName [groupName]` +: The display name of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. `--teamId [teamId]` -: The ID of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. +: The ID of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. `--teamName [teamName]` -: The display name of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupDisplayName`, `teamId`, or `teamName`. +: The display name of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. `-n, --userName [userName]` : (deprecated) User's UPN (user principal name), eg. `johndoe@example.com`.. Specify only one of the following: `userName`, `ids` or `userNames`. @@ -67,7 +67,7 @@ m365 entra m365group user remove --teamName 'Project Team' --userNames 'anne.mat Removes users specified by a comma separated list of user ids from the specified Microsoft 365 group specified by name. ```sh -m365 entra m365group user remove --groupDisplayName 'Project Team' --ids '5b8e4cb1-ea40-484b-a94e-02a4313fefb4,be7a56d8-b045-4938-af35-917ab6e5309f' +m365 entra m365group user remove --groupName 'Project Team' --ids '5b8e4cb1-ea40-484b-a94e-02a4313fefb4,be7a56d8-b045-4938-af35-917ab6e5309f' ``` ## Response diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts index 1550ba5f9dc..2d7787c8695 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts @@ -218,55 +218,6 @@ describe(commands.M365GROUP_USER_REMOVE, () => { assert(memberDeleteCallIssued); }); - it('removes the specified owner from owners and members endpoint of the Microsoft 365 Group specified by groupName with accepted prompt', async () => { - let memberDeleteCallIssued = false; - - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } - } - }; - } - - throw 'Invalid request'; - }); - - sinon.stub(request, 'delete').callsFake(async (opts) => { - memberDeleteCallIssued = true; - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; - } - - throw 'Invalid request'; - }); - - sinonUtil.restore(cli.promptForConfirmation); - sinon.stub(cli, 'promptForConfirmation').resolves(true); - - await command.action(logger, { options: { groupName: "Finance", userName: "anne.matthews@contoso.onmicrosoft.com" } }); - assert(memberDeleteCallIssued); - }); - it('removes the specified owner from owners and members endpoint of the specified Microsoft 365 Group when prompt confirmed', async () => { let memberDeleteCallIssued = false; diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.ts b/src/m365/entra/commands/m365group/m365group-user-remove.ts index 13149eb0f65..380cc76c50e 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.ts @@ -142,15 +142,6 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { await this.warn(logger, `Option 'userName' is deprecated. Please use 'ids' or 'userNames' instead.`); } - let groupId = args.options.groupId; - - if (args.options.groupName) { - groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupName); - } - else if (args.options.teamId) { - groupId = args.options.teamId; - } - const removeUser = async (): Promise => { try { const groupId: string = await this.getGroupId(logger, args.options.groupId, args.options.teamId, args.options.groupName, args.options.teamName); From dfbc10f0f4952930059ce36edfbef6b532942955 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 18 Oct 2024 07:03:09 +0200 Subject: [PATCH 5/6] All entra m365group commands should accept displayName option --- .../docs/cmd/entra/m365group/m365group-user-list.mdx | 10 +++++----- .../commands/m365group/m365group-user-list.spec.ts | 8 ++++---- .../entra/commands/m365group/m365group-user-list.ts | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx index 2b65baf6760..173ae648e97 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx @@ -16,10 +16,10 @@ m365 entra m365group user list [options] ```md definition-list `-i, --groupId [groupId]` -: The ID of the Microsoft 365 group. Specify `groupId` or `groupName` but not both. +: The ID of the Microsoft 365 group. Specify `groupId` or `groupDisplayName` but not both. -`-n, --groupName [groupName]` -: The display name of the Microsoft 365 group. Specify `groupId` or `groupName` but not both. +`-n, --groupDisplayName [groupDisplayName]` +: The display name of the Microsoft 365 group. Specify `groupId` or `groupDisplayName` but not both. `-r, --role [role]` : Filter the results to only users with the given role. Allowed values: `Owner`, `Member`. @@ -48,7 +48,7 @@ m365 entra m365group user list --groupId '00000000-0000-0000-0000-000000000000' List all owners from Microsoft 365 group specified by display name. ```sh -m365 entra m365group user list --groupName Developers --role Owner +m365 entra m365group user list --groupDisplayName Developers --role Owner ``` List specific properties for all group users from a group specified by ID. @@ -60,7 +60,7 @@ m365 entra m365group user list --groupId 03cba9da-3974-46c1-afaf-79caa2e45bbe -- List all group members that are guest users. ```sh -m365 entra m365group user list --groupName Developers --filter "userType eq 'Guest'" +m365 entra m365group user list --groupDisplayName Developers --filter "userType eq 'Guest'" ``` ## Response diff --git a/src/m365/entra/commands/m365group/m365group-user-list.spec.ts b/src/m365/entra/commands/m365group/m365group-user-list.spec.ts index 18d4a66562d..73c329c0bf0 100644 --- a/src/m365/entra/commands/m365group/m365group-user-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-list.spec.ts @@ -179,7 +179,7 @@ describe(commands.M365GROUP_USER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true, groupName: "CLI Test Group" } }); + await command.action(logger, { options: { verbose: true, groupDisplayName: "CLI Test Group" } }); assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", @@ -394,12 +394,12 @@ describe(commands.M365GROUP_USER_LIST, () => { }); it('throws error when the group by name is not a unified group', async () => { - const groupName = 'CLI Test Group'; + const groupDisplayName = 'CLI Test Group'; sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { verbose: true, groupName: groupName } } as any), - new CommandError(`Specified group '${groupName}' is not a Microsoft 365 group.`)); + await assert.rejects(command.action(logger, { options: { verbose: true, groupDisplayName: groupDisplayName } } as any), + new CommandError(`Specified group '${groupDisplayName}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-list.ts b/src/m365/entra/commands/m365group/m365group-user-list.ts index 5fca3c0ac78..237da67859e 100644 --- a/src/m365/entra/commands/m365group/m365group-user-list.ts +++ b/src/m365/entra/commands/m365group/m365group-user-list.ts @@ -15,7 +15,7 @@ interface CommandArgs { interface Options extends GlobalOptions { filter?: string; groupId?: string; - groupName?: string; + groupDisplayName?: string; properties?: string; role?: string; } @@ -46,7 +46,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { groupId: typeof args.options.groupId !== 'undefined', - groupName: typeof args.options.groupName !== 'undefined', + groupDisplayName: typeof args.options.groupDisplayName !== 'undefined', role: typeof args.options.role !== 'undefined', properties: typeof args.options.properties !== 'undefined', filter: typeof args.options.filter !== 'undefined' @@ -60,7 +60,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { option: "-i, --groupId [groupId]" }, { - option: "-n, --groupName [groupName]" + option: "-n, --groupDisplayName [groupDisplayName]" }, { option: "-r, --role [type]", @@ -78,7 +78,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { #initOptionSets(): void { this.optionSets.push( { - options: ['groupId', 'groupName'] + options: ['groupId', 'groupDisplayName'] } ); } @@ -107,7 +107,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); if (!isUnifiedGroup) { - throw Error(`Specified group '${args.options.groupId || args.options.groupName}' is not a Microsoft 365 group.`); + throw Error(`Specified group '${args.options.groupId || args.options.groupDisplayName}' is not a Microsoft 365 group.`); } let users: ExtendedUser[] = []; @@ -151,7 +151,7 @@ class EntraM365GroupUserListCommand extends GraphCommand { await logger.logToStderr('Retrieving Group Id...'); } - return await entraGroup.getGroupIdByDisplayName(options.groupName!); + return await entraGroup.getGroupIdByDisplayName(options.groupDisplayName!); } private async getUsers(options: Options, role: string, groupId: string, logger: Logger): Promise { From 98d50b7d4a3e0819a10f0eff7ca528c16b9fa318 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Wed, 6 Nov 2024 08:21:06 +0100 Subject: [PATCH 6/6] All entra m365group commands should accept displayName option --- docs/docs/cmd/entra/m365group/m365group-remove.mdx | 2 +- src/m365/entra/commands/m365group/m365group-teamify.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-remove.mdx index e05eb90eee9..0ebd7c05596 100644 --- a/docs/docs/cmd/entra/m365group/m365group-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-remove.mdx @@ -53,7 +53,7 @@ m365 entra m365group remove --id 28beab62-7540-4db1-a23f-29a6018a3848 Remove group with displayName _Finance_ without prompting for confirmation ```sh -m365 entra m365group remove --displayName 28beab62-7540-4db1-a23f-29a6018a3848 --force +m365 entra m365group remove --displayName Finance --force ``` Remove group with id _28beab62-7540-4db1-a23f-29a6018a3848_ without prompting for confirmation and without moving it to the Recycle Bin diff --git a/src/m365/entra/commands/m365group/m365group-teamify.ts b/src/m365/entra/commands/m365group/m365group-teamify.ts index 0f71895ae41..39831cc5db3 100644 --- a/src/m365/entra/commands/m365group/m365group-teamify.ts +++ b/src/m365/entra/commands/m365group/m365group-teamify.ts @@ -73,7 +73,7 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'dispalyName', 'mailNickname'] }); + this.optionSets.push({ options: ['id', 'displayName', 'mailNickname'] }); } #initTypes(): void {