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:
@@ -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;
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user