From c820a49426647212298e78eaf286a5e2e4232e05 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Fri, 25 Feb 2022 21:47:07 +0100 Subject: [PATCH] feat(msgraph): add `groupExpand` config option Add `groupExpand` allowing to use the `$expand` query parameter by the Microsoft Graph API to expand a single relationship. Relates-to: issue #9819 Relates-to: PR #9826 Signed-off-by: Patrick Jungermann --- .changeset/fluffy-trees-occur.md | 5 + .../catalog-backend-module-msgraph/README.md | 6 ++ .../api-report.md | 2 + .../src/microsoftGraph/config.test.ts | 2 + .../src/microsoftGraph/config.ts | 8 ++ .../src/microsoftGraph/read.test.ts | 98 ++++++++++++++++++- .../src/microsoftGraph/read.ts | 13 ++- .../MicrosoftGraphOrgReaderProcessor.ts | 1 + 8 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 .changeset/fluffy-trees-occur.md diff --git a/.changeset/fluffy-trees-occur.md b/.changeset/fluffy-trees-occur.md new file mode 100644 index 0000000000..6e41a61069 --- /dev/null +++ b/.changeset/fluffy-trees-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +add config option `groupExpand` to allow expanding a single relationship diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index daaf1ec225..a253359471 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -51,6 +51,12 @@ catalog: # This and userFilter are mutually exclusive, only one can be specified # See https://docs.microsoft.com/en-us/graph/search-query-parameter userGroupMemberFilter: "displayName eq 'Backstage Users'" + # Optional parameter to include the expanded resource or collection referenced + # by a single relationship (navigation property) in your results. + # Only one relationship can be expanded in a single request. + # See https://docs.microsoft.com/en-us/graph/query-parameters#expand-parameter + # Can be combined with userGroupMember[...] instead of userFilter. + groupExpand: member # Optional search for users, use group membership to get users. # (Search for groups and fetch their members.) # This and userFilter are mutually exclusive, only one can be specified diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 574b948dea..8df06f54b1 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -164,6 +164,7 @@ export type MicrosoftGraphProviderConfig = { userExpand?: string; userGroupMemberFilter?: string; userGroupMemberSearch?: string; + groupExpand?: string; groupFilter?: string; groupSearch?: string; }; @@ -198,6 +199,7 @@ export function readMicrosoftGraphOrg( userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index 700e5cd8fe..cfc5c1cbb0 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -55,6 +55,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + groupExpand: 'member', groupFilter: 'securityEnabled eq false', }, ], @@ -69,6 +70,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com', userExpand: 'manager', userFilter: 'accountEnabled eq true', + groupExpand: 'member', groupFilter: 'securityEnabled eq false', }, ]; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index c2789d767a..b7fbcfb8a9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -70,6 +70,12 @@ export type MicrosoftGraphProviderConfig = { * E.g. "\"displayName:-team\"" would only match groups which contain '-team' */ userGroupMemberSearch?: string; + /** + * The "expand" argument to apply to groups. + * + * E.g. "member" + */ + groupExpand?: string; /** * The filter to apply to extract groups. * @@ -115,6 +121,7 @@ export function readMicrosoftGraphConfig( const userGroupMemberSearch = providerConfig.getOptionalString( 'userGroupMemberSearch', ); + const groupExpand = providerConfig.getOptionalString('groupExpand'); const groupFilter = providerConfig.getOptionalString('groupFilter'); const groupSearch = providerConfig.getOptionalString('groupSearch'); @@ -139,6 +146,7 @@ export function readMicrosoftGraphConfig( userFilter, userGroupMemberFilter, userGroupMemberSearch, + groupExpand, groupFilter, groupSearch, }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index dba3741c73..e3d6897d66 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -236,7 +236,7 @@ describe('read microsoft graph', () => { expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with userExpand and custom transformer', async () => { + it('should read users with userExpand, groupExpand and custom transformer', async () => { async function* getExampleGroups() { yield { id: 'groupid', @@ -272,6 +272,7 @@ describe('read microsoft graph', () => { const { users } = await readMicrosoftGraphUsersInGroups(client, { userExpand: 'manager', userGroupMemberFilter: 'securityEnabled eq true', + groupExpand: 'member', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', kind: 'User', @@ -292,6 +293,7 @@ describe('read microsoft graph', () => { expect(client.getGroups).toBeCalledTimes(1); expect(client.getGroups).toBeCalledWith({ + expand: 'member', filter: 'securityEnabled eq true', }); expect(client.getGroupMembers).toBeCalledTimes(1); @@ -453,6 +455,100 @@ describe('read microsoft graph', () => { // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); }); + it('should read groups with groupExpand', async () => { + async function* getExampleGroups() { + yield { + id: 'groupid', + displayName: 'Group Name', + description: 'Group Description', + mail: 'group@example.com', + }; + } + + async function* getExampleGroupMembers(): AsyncIterable { + yield { + '@odata.type': '#microsoft.graph.group', + id: 'childgroupid', + }; + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid', + }; + } + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { groups, groupMember, groupMemberOf, rootGroup } = + await readMicrosoftGraphGroups(client, 'tenantid', { + groupExpand: 'member', + groupFilter: 'securityEnabled eq false', + }); + + const expectedRootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }); + expect(groups).toEqual([ + expectedRootGroup, + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'groupid', + }, + name: 'group_name', + description: 'Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Group Name', + email: 'group@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + ]); + expect(rootGroup).toEqual(expectedRootGroup); + expect(groupMember.get('groupid')).toEqual(new Set(['childgroupid'])); + expect(groupMemberOf.get('userid')).toEqual(new Set(['groupid'])); + expect(groupMember.get('organization_name')).toEqual(new Set()); + + expect(client.getGroups).toBeCalledTimes(1); + expect(client.getGroups).toBeCalledWith({ + expand: 'member', + filter: 'securityEnabled eq false', + }); + expect(client.getGroupMembers).toBeCalledTimes(1); + expect(client.getGroupMembers).toBeCalledWith('groupid'); + // TODO: Loading groups photos doesn't work right now as Microsoft Graph + // doesn't allows this yet + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); + }); + it('should read security groups', async () => { async function* getExampleGroups() { yield { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 333eaa6b0a..ed6ca76e68 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -140,6 +140,7 @@ export async function readMicrosoftGraphUsersInGroups( userExpand?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; transformer?: UserTransformer; logger: Logger; }, @@ -150,15 +151,16 @@ export async function readMicrosoftGraphUsersInGroups( const limiter = limiterFactory(10); - const transformer = options?.transformer ?? defaultUserTransformer; + const transformer = options.transformer ?? defaultUserTransformer; const userGroupMemberPromises: Promise[] = []; const userPromises: Promise[] = []; const groupMemberUsers: Set = new Set(); for await (const group of client.getGroups({ - search: options?.userGroupMemberSearch, - filter: options?.userGroupMemberFilter, + expand: options.groupExpand, + search: options.userGroupMemberSearch, + filter: options.userGroupMemberFilter, })) { // Process all groups in parallel, otherwise it can take quite some time userGroupMemberPromises.push( @@ -329,8 +331,9 @@ export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, options?: { - groupSearch?: string; + groupExpand?: string; groupFilter?: string; + groupSearch?: string; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; }, @@ -357,6 +360,7 @@ export async function readMicrosoftGraphGroups( const promises: Promise[] = []; for await (const group of client.getGroups({ + expand: options?.groupExpand, search: options?.groupSearch, filter: options?.groupFilter, })) { @@ -513,6 +517,7 @@ export async function readMicrosoftGraphOrg( userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 942bde19eb..9037bd8728 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -109,6 +109,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, userGroupMemberSearch: provider.userGroupMemberSearch, + groupExpand: provider.groupExpand, groupFilter: provider.groupFilter, groupSearch: provider.groupSearch, userTransformer: this.userTransformer,