From 55136128c5e1baae355e7950cb0f22141cf846a0 Mon Sep 17 00:00:00 2001 From: Nanddeep Nachan Date: Thu, 9 Nov 2023 11:39:53 +0000 Subject: [PATCH] Extends 'aad m365group user list' command with extra options and deprecation. Closes #5557 --- .../cmd/aad/m365group/m365group-user-list.mdx | 56 ++-- .../m365group/m365group-user-list.spec.ts | 249 +++++++++++++++--- .../commands/m365group/m365group-user-list.ts | 132 ++++++++-- 3 files changed, 353 insertions(+), 84 deletions(-) diff --git a/docs/docs/cmd/aad/m365group/m365group-user-list.mdx b/docs/docs/cmd/aad/m365group/m365group-user-list.mdx index 4f374c792d9..642377760fc 100644 --- a/docs/docs/cmd/aad/m365group/m365group-user-list.mdx +++ b/docs/docs/cmd/aad/m365group/m365group-user-list.mdx @@ -15,33 +15,52 @@ m365 aad m365group user list [options] ## Options ```md definition-list -`-i, --groupId ` -: The ID of the Microsoft 365 group for which to list users +`-i, --groupId [groupId]` +: The ID of the Microsoft 365 group. Specify `groupId` or `groupDisplayName` 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: `Owner`, `Member`, `Guest` +: Filter the results to only users with the given role. Allowed values: `Owner`, `Member`, or (Deprecated) `Guest`. + +`-p, --properties [properties]` +: Comma-separated list of properties to retrieve. + +`-f, --filter [filter]` +: OData filter to use to query the list of users with. ``` +## Remarks + +When the `properties` option includes values with a `/`, for example: `manager/displayName`, an additional `$expand` query parameter will be included on `manager`. + ## Examples -List all users and their role in the specified Microsoft 365 group +List all users and their role from Microsoft 365 group specified by ID. ```sh -m365 aad m365group user list --groupId '00000000-0000-0000-0000-000000000000' +m365 aad m365group user list --groupId 00000000-0000-0000-0000-000000000000 +``` + +List all owners from Microsoft 365 group specified by display name. + +``` +m365 aad m365group user list --groupDisplayName Developers --role Owner ``` -List all owners and their role in the specified Microsoft 365 group +List specific properties for all group users from a group specified by ID. ```sh -m365 aad m365group user list --groupId '00000000-0000-0000-0000-000000000000' --role Owner +m365 aad m365group user list --groupId 03cba9da-3974-46c1-afaf-79caa2e45bbe --properties "id,jobTitle,companyName,accountEnabled" ``` - List all guests and their role in the specified Microsoft 365 group +List all group members that are guest users. ```sh -m365 aad m365group user list --groupId '00000000-0000-0000-0000-000000000000' --role Guest +m365 aad m365group user list --groupDisplayName Developers --filter "userType eq 'Guest'" ``` ## Response @@ -55,7 +74,12 @@ m365 aad m365group user list --groupId '00000000-0000-0000-0000-000000000000' -- "id": "da52218e-4822-4ac6-b41d-255e2059655e", "displayName": "Adele Vance", "userPrincipalName": "AdeleV@contoso.OnMicrosoft.com", - "userType": "Member" + "givenName": "Adele", + "surname": "Vance", + "roles": [ + "Owner", + "Member" + ] } ] ``` @@ -64,17 +88,17 @@ m365 aad m365group user list --groupId '00000000-0000-0000-0000-000000000000' -- ```text - id displayName userPrincipalName userType - ------------------------------------ -------------------- ------------------------------------ -------- - da52218e-4822-4ac6-b41d-255e2059655e Adele Vance AdeleV@contoso.OnMicrosoft.com Member + id displayName userPrincipalName userType roles + ------------------------------------ -------------------- ------------------------------------ -------- -------- + da52218e-4822-4ac6-b41d-255e2059655e Adele Vance AdeleV@contoso.OnMicrosoft.com Owner,Member Owner,Member ``` ```csv - id,displayName,userPrincipalName,userType - da52218e-4822-4ac6-b41d-255e2059655e,Adele Vance,AdeleV@contoso.OnMicrosoft.com,Member + id,displayName,userPrincipalName,givenName,surname,userType + da52218e-4822-4ac6-b41d-255e2059655e,Adele Vance,AdeleV@contoso.OnMicrosoft.com,Adele,Vance,Member ``` @@ -91,6 +115,8 @@ m365 aad m365group user list --groupId '00000000-0000-0000-0000-000000000000' -- ---------|------- id | da52218e-4822-4ac6-b41d-255e2059655e displayName | Adele Vance + givenName | Adele + surname | Vance userPrincipalName | AdeleV@contoso.OnMicrosoft.com userType | Member ``` diff --git a/src/m365/aad/commands/m365group/m365group-user-list.spec.ts b/src/m365/aad/commands/m365group/m365group-user-list.spec.ts index 274ecbb3d6a..92fa3329688 100644 --- a/src/m365/aad/commands/m365group/m365group-user-list.spec.ts +++ b/src/m365/aad/commands/m365group/m365group-user-list.spec.ts @@ -66,7 +66,7 @@ describe(commands.M365GROUP_USER_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if the groupId is not a valid guid.', async () => { + it('fails validation if the groupId is not a valid guid', async () => { const actual = await command.validate({ options: { groupId: 'not-c49b-4fd4-8223-28f0ac3a6402' @@ -124,18 +124,19 @@ describe(commands.M365GROUP_USER_LIST, () => { assert.strictEqual(actual, true); }); - it('correctly lists all users in a Microsoft 365 group', async () => { + it('correctly lists all users in a Microsoft 365 group by group id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners?$select=id,displayName,userPrincipalName,userType`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "userType": "Member" }] + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }] }; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members?$select=id,displayName,userPrincipalName,userType`) { + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { return { "value": [ - { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "userType": "Member" }, - { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "userType": "Member" } + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson", "userType": "Member" } ] }; } @@ -143,62 +144,112 @@ describe(commands.M365GROUP_USER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000" } }); - assert(loggerLogSpy.calledWith([ + await command.action(logger, { options: { verbose: true, groupId: "00000000-0000-0000-0000-000000000000" } }); + assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", - "userType": "Owner" + "userType": "Owner", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner", "Member"] }, + { + "id": "00000000-0000-0000-0000-000000000001", + "displayName": "Karl Matteson", + "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", + "userType": "Member", + "givenName": "Karl", + "surname": "Matteson", + "roles": ["Member"] + } + ])); + }); + + it('correctly lists all users in a Microsoft 365 group by group name', async () => { + sinon.stub(aadGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson", "userType": "Member" } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, groupDisplayName: "CLI Test Group" } }); + assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", - "userType": "Member" + "userType": "Owner", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner", "Member"] }, { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", - "userType": "Member" + "userType": "Member", + "givenName": "Karl", + "surname": "Matteson", + "roles": ["Member"] } ])); }); - it('correctly lists all owners in a Microsoft 365 group', async () => { + it('correctly lists all owners in a Microsoft 365 group by group id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners?$select=id,displayName,userPrincipalName,userType`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "userType": "Member" }] + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }] }; } + throw 'Invalid request'; }); await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", role: "Owner" } }); - assert(loggerLogSpy.calledWith([ + assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", - "userType": "Owner" + "userType": "Owner", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner"] } ])); }); - it('correctly lists all members in a Microsoft 365 group', async () => { + it('correctly lists all members in a Microsoft 365 group by group id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners?$select=id,displayName,userPrincipalName,userType`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "userType": "Member" }] + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }] }; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members?$select=id,displayName,userPrincipalName,userType`) { + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { return { "value": [ - { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "userType": "Member" }, - { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "userType": "Member" } + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson", "userType": "Member" } ] }; } @@ -207,34 +258,41 @@ describe(commands.M365GROUP_USER_LIST, () => { }); await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", role: "Member" } }); - assert(loggerLogSpy.calledWith([ + assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", - "userType": "Member" + "userType": "Member", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Member"] }, { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", - "userType": "Member" + "userType": "Member", + "givenName": "Karl", + "surname": "Matteson", + "roles": ["Member"] } ])); }); - it('correctly lists all users in a Microsoft 365 group (debug)', async () => { + it('correctly lists all guests in a Microsoft 365 group by group id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners?$select=id,displayName,userPrincipalName,userType`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "userType": "Member" }] + "value": [{ "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson", "userType": "Member" }] }; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members?$select=id,displayName,userPrincipalName,userType`) { + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { return { "value": [ - { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "userType": "Member" }, - { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "userType": "Member" } + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "annematthews_gmail.com#EXT#@nachan365.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Guest" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson", "userType": "Member" } ] }; } @@ -242,25 +300,122 @@ describe(commands.M365GROUP_USER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, groupId: "00000000-0000-0000-0000-000000000000" } }); - assert(loggerLogSpy.calledWith([ + await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", role: "Guest" } }); + assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", - "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", - "userType": "Owner" - }, + "userPrincipalName": "annematthews_gmail.com#EXT#@nachan365.onmicrosoft.com", + "userType": "Guest", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Member"] + } + ])); + }); + + it('correctly lists all users in a Microsoft 365 group by group id (debug)', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Member" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson", "userType": "Member" } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, groupId: "00000000-0000-0000-0000-000000000000" } }); + assert(loggerLogSpy.calledOnceWithExactly([ { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", - "userType": "Member" + "userType": "Owner", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner", "Member"] }, { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", - "userType": "Member" + "userType": "Member", + "givenName": "Karl", + "surname": "Matteson", + "roles": ["Member"] + } + ])); + }); + + it('correctly lists properties for all users in a Microsoft 365 group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=displayName,mail,id&$expand=memberof($select=id),memberof($select=displayName)`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Karl Matteson", "mail": "karl.matteson@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0453" }] } + ] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=displayName,mail,id&$expand=memberof($select=id),memberof($select=displayName)`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Anne Matthews", "mail": "anne.matthews@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0454" }] } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { groupId: "2c1ba4c4-cd9b-4417-832f-92a34bc34b2a", properties: "displayName,mail,memberof/id,memberof/displayName" } }); + + assert(loggerLogSpy.calledOnceWithExactly([ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Karl Matteson", "mail": "karl.matteson@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0453" }], "roles": ["Owner"], "userType": "Owner" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Anne Matthews", "mail": "anne.matthews@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0454" }], "roles": ["Member"] } + ])); + }); + + it('correctly lists all guest users in a Microsoft 365 group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType&$filter=userType%20eq%20'Guest'&$count=true`) { + return { + "value": [] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname,userType&$filter=userType%20eq%20'Guest'&$count=true`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "annematthews_gmail.com#EXT#@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews", "userType": "Guest" } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { groupId: "2c1ba4c4-cd9b-4417-832f-92a34bc34b2a", filter: "userType eq 'Guest'" } }); + + assert(loggerLogSpy.calledOnceWithExactly([ + { + "id": "00000000-0000-0000-0000-000000000000", + "displayName": "Anne Matthews", + "userPrincipalName": "annematthews_gmail.com#EXT#@contoso.onmicrosoft.com", + "givenName": "Anne", + "surname": "Matthews", + "userType": "Guest", + "roles": ["Member"] } ])); }); @@ -272,13 +427,23 @@ describe(commands.M365GROUP_USER_LIST, () => { new CommandError('An error has occurred')); }); - it('throws error when the group is not a unified group', async () => { + it('throws error when the group by id is not a unified group', async () => { const groupId = '3f04e370-cbc6-4091-80fe-1d038be2ad06'; sinonUtil.restore(aadGroup.isUnifiedGroup); sinon.stub(aadGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { groupId: groupId } } as any), - new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); + await assert.rejects(command.action(logger, { options: { verbose: true, groupId: groupId } } as any), + new CommandError(`Specified group '${groupId}' is not a Microsoft 365 group.`)); + }); + + it('throws error when the group by name is not a unified group', async () => { + const groupDisplayName = 'CLI Test Group'; + + sinonUtil.restore(aadGroup.isUnifiedGroup); + sinon.stub(aadGroup, '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.`)); }); }); diff --git a/src/m365/aad/commands/m365group/m365group-user-list.ts b/src/m365/aad/commands/m365group/m365group-user-list.ts index 1336fe137c6..2447c927510 100644 --- a/src/m365/aad/commands/m365group/m365group-user-list.ts +++ b/src/m365/aad/commands/m365group/m365group-user-list.ts @@ -6,14 +6,22 @@ import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { aadGroup } from '../../../../utils/aadGroup.js'; +import { CliRequestOptions } from '../../../../request.js'; interface CommandArgs { options: Options; } interface Options extends GlobalOptions { + filter?: string; + groupId?: string; + groupDisplayName?: string; + properties?: string; role?: string; - groupId: string; +} + +interface ExtendedUser extends User { + roles: string[]; } class AadM365GroupUserListCommand extends GraphCommand { @@ -30,13 +38,18 @@ class AadM365GroupUserListCommand extends GraphCommand { this.#initTelemetry(); this.#initOptions(); + this.#initOptionSets(); this.#initValidators(); } #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { - role: args.options.role + groupId: typeof args.options.groupId !== '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' }); }); } @@ -44,11 +57,28 @@ class AadM365GroupUserListCommand extends GraphCommand { #initOptions(): void { this.options.unshift( { - option: "-i, --groupId " + option: "-i, --groupId [groupId]" + }, + { + option: "-n, --groupDisplayName [groupDisplayName]" }, { option: "-r, --role [type]", autocomplete: ["Owner", "Member", "Guest"] + }, + { + option: "-p, --properties [properties]" + }, + { + option: "-f, --filter [filter]" + } + ); + } + + #initOptionSets(): void { + this.optionSets.push( + { + options: ['groupId', 'groupDisplayName'] } ); } @@ -56,7 +86,7 @@ class AadM365GroupUserListCommand 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`; } @@ -73,17 +103,36 @@ class AadM365GroupUserListCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - const isUnifiedGroup = await aadGroup.isUnifiedGroup(args.options.groupId); + if (args.options.role === 'Guest') { + this.warn(logger, `Value 'Guest' for the option role is deprecated. Use --filter "userType eq 'Guest'" instead.`); + } + + const groupId = await this.getGroupId(args.options, logger); + const isUnifiedGroup = await aadGroup.isUnifiedGroup(groupId); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.groupId}' 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[] = []; + if (!args.options.role || args.options.role === 'Owner') { + const owners = await this.getUsers(args.options, 'Owners', groupId, logger); + owners.forEach(owner => users.push({ ...owner, roles: ['Owner'], userType: 'Owner' })); } - let users = await this.getOwners(args.options.groupId, logger); + if (!args.options.role || args.options.role === 'Member' || args.options.role === 'Guest') { + const members = await this.getUsers(args.options, 'Members', groupId, logger); + + members.forEach((member: ExtendedUser) => { + const user = users.find((u: ExtendedUser) => u.id === member.id); - if (args.options.role !== 'Owner') { - const membersAndGuests = await this.getMembersAndGuests(args.options.groupId, logger); - users = users.concat(membersAndGuests); + if (user !== undefined) { + user.roles.push('Member'); + } + else { + users.push({ ...member, roles: ['Member'] }); + } + }); } if (args.options.role) { @@ -97,31 +146,60 @@ class AadM365GroupUserListCommand extends GraphCommand { } } - private async getOwners(groupId: string, logger: Logger): Promise { - if (this.verbose) { - await logger.logToStderr(`Retrieving owners of the group with id ${groupId}`); + private async getGroupId(options: Options, logger: Logger): Promise { + if (options.groupId) { + return options.groupId; } - const endpoint: string = `${this.resource}/v1.0/groups/${groupId}/owners?$select=id,displayName,userPrincipalName,userType`; - - const users = await odata.getAllItems(endpoint); - - // Currently there is a bug in the Microsoft Graph that returns Owners as - // userType 'member'. We therefore update all returned user as owner - users.forEach(user => { - user.userType = 'Owner'; - }); + if (this.verbose) { + await logger.logToStderr('Retrieving Group Id...'); + } - return users; + return await aadGroup.getGroupIdByDisplayName(options.groupDisplayName!); } - private async getMembersAndGuests(groupId: string, logger: Logger): Promise { + private async getUsers(options: Options, role: string, groupId: string, logger: Logger): Promise { + const { properties, filter } = options; + if (this.verbose) { - await logger.logToStderr(`Retrieving members of the group with id ${groupId}`); + await logger.logToStderr(`Retrieving ${role} of the group with id ${groupId}`); } - const endpoint: string = `${this.resource}/v1.0/groups/${groupId}/members?$select=id,displayName,userPrincipalName,userType`; - return await odata.getAllItems(endpoint); + const selectProperties: string = properties ? + `${properties.split(',').filter(f => f.toLowerCase() !== 'id').concat('id').map(p => p.trim()).join(',')}` : + 'id,displayName,userPrincipalName,givenName,surname,userType'; + const allSelectProperties: string[] = selectProperties.split(','); + const propertiesWithSlash: string[] = allSelectProperties.filter(item => item.includes('/')); + + const fieldsToExpand: string[] = []; + propertiesWithSlash.forEach(p => { + const propertiesSplit: string[] = p.split('/'); + fieldsToExpand.push(`${propertiesSplit[0]}($select=${propertiesSplit[1]})`); + }); + + const fieldExpand: string = fieldsToExpand.join(','); + + const expandParam = fieldExpand.length > 0 ? `&$expand=${fieldExpand}` : ''; + const selectParam = allSelectProperties.filter(item => !item.includes('/')); + const endpoint: string = `${this.resource}/v1.0/groups/${groupId}/${role}/microsoft.graph.user?$select=${selectParam}${expandParam}`; + + if (filter) { + // While using the filter, we need to specify the ConsistencyLevel header. + // Can be refactored when the header is no longer necessary. + const requestOptions: CliRequestOptions = { + url: `${endpoint}&$filter=${encodeURIComponent(filter)}&$count=true`, + headers: { + accept: 'application/json;odata.metadata=none', + ConsistencyLevel: 'eventual' + }, + responseType: 'json' + }; + + return await odata.getAllItems(requestOptions); + } + else { + return await odata.getAllItems(endpoint); + } } }