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 <Patrick.Jungermann@gmail.com>
This commit is contained in:
Patrick Jungermann
2022-02-25 21:47:07 +01:00
parent 400261b68e
commit c820a49426
8 changed files with 130 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
add config option `groupExpand` to allow expanding a single relationship
@@ -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
@@ -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;
@@ -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',
},
];
@@ -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,
});
@@ -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<GroupMember> {
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 {
@@ -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<void>[] = [];
const userPromises: Promise<void>[] = [];
const groupMemberUsers: Set<string> = 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<void>[] = [];
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;
@@ -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,