Add support for the group profile element to Microsoft Graph processor
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Support `profile` of groups including `displayName` and `email` in
|
||||
`MicrosoftGraphOrgReaderProcessor`. Importing `picture` doesn't work yet, as
|
||||
the Microsoft Graph API does not expose them correctly.
|
||||
@@ -210,7 +210,7 @@ describe('MicrosoftGraphClient', () => {
|
||||
expect(photo).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should load profile photo', async () => {
|
||||
it('should load user profile photo', async () => {
|
||||
worker.use(
|
||||
rest.get('https://example.com/users/user-id/photo/*', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.text('911')),
|
||||
@@ -222,7 +222,7 @@ describe('MicrosoftGraphClient', () => {
|
||||
expect(photo).toEqual('data:image/jpeg;base64,OTEx');
|
||||
});
|
||||
|
||||
it('should load profile photo for size 120', async () => {
|
||||
it('should load user profile photo for size 120', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://example.com/users/user-id/photos/120/*',
|
||||
@@ -252,6 +252,46 @@ describe('MicrosoftGraphClient', () => {
|
||||
expect(values).toEqual([{ surname: 'Example' }]);
|
||||
});
|
||||
|
||||
it('should load group profile photo with max size of 120', async () => {
|
||||
worker.use(
|
||||
rest.get('https://example.com/groups/group-id/photos', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
value: [
|
||||
{
|
||||
height: 120,
|
||||
id: 120,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://example.com/groups/group-id/photos/120/*',
|
||||
(_, res, ctx) => res(ctx.status(200), ctx.text('911')),
|
||||
),
|
||||
);
|
||||
|
||||
const photo = await client.getGroupPhotoWithSizeLimit('group-id', 120);
|
||||
|
||||
expect(photo).toEqual('data:image/jpeg;base64,OTEx');
|
||||
});
|
||||
|
||||
it('should load group profile photo', async () => {
|
||||
worker.use(
|
||||
rest.get('https://example.com/groups/group-id/photo/*', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.text('911')),
|
||||
),
|
||||
);
|
||||
|
||||
const photo = await client.getGroupPhoto('group-id');
|
||||
|
||||
expect(photo).toEqual('data:image/jpeg;base64,OTEx');
|
||||
});
|
||||
|
||||
it('should load groups', async () => {
|
||||
worker.use(
|
||||
rest.get('https://example.com/groups', (_, res, ctx) =>
|
||||
|
||||
@@ -117,59 +117,34 @@ export class MicrosoftGraphClient {
|
||||
userId: string,
|
||||
maxSize: number,
|
||||
): Promise<string | undefined> {
|
||||
const response = await this.requestApi(`users/${userId}/photos`);
|
||||
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
} else if (response.status !== 200) {
|
||||
await this.handleError('user photos', response);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const photos = result.value as MicrosoftGraph.ProfilePhoto[];
|
||||
let selectedPhoto: MicrosoftGraph.ProfilePhoto | undefined = undefined;
|
||||
|
||||
// Find the biggest picture that is smaller than the max size
|
||||
for (const p of photos) {
|
||||
if (
|
||||
!selectedPhoto ||
|
||||
(p.height! >= selectedPhoto.height! && p.height! <= maxSize)
|
||||
) {
|
||||
selectedPhoto = p;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedPhoto) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await this.getUserPhoto(userId, selectedPhoto.id!);
|
||||
return await this.getPhotoWithSizeLimit('users', userId, maxSize);
|
||||
}
|
||||
|
||||
async getUserPhoto(
|
||||
userId: string,
|
||||
sizeId?: string,
|
||||
): Promise<string | undefined> {
|
||||
const path = sizeId
|
||||
? `users/${userId}/photos/${sizeId}/$value`
|
||||
: `users/${userId}/photo/$value`;
|
||||
const response = await this.requestApi(path);
|
||||
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
} else if (response.status !== 200) {
|
||||
await this.handleError('photo', response);
|
||||
}
|
||||
|
||||
return `data:image/jpeg;base64,${Buffer.from(
|
||||
await response.arrayBuffer(),
|
||||
).toString('base64')}`;
|
||||
return await this.getPhoto('users', userId, sizeId);
|
||||
}
|
||||
|
||||
async *getUsers(query?: ODataQuery): AsyncIterable<MicrosoftGraph.User> {
|
||||
yield* this.requestCollection<MicrosoftGraph.User>(`users`, query);
|
||||
}
|
||||
|
||||
async getGroupPhotoWithSizeLimit(
|
||||
groupId: string,
|
||||
maxSize: number,
|
||||
): Promise<string | undefined> {
|
||||
return await this.getPhotoWithSizeLimit('groups', groupId, maxSize);
|
||||
}
|
||||
|
||||
async getGroupPhoto(
|
||||
groupId: string,
|
||||
sizeId?: string,
|
||||
): Promise<string | undefined> {
|
||||
return await this.getPhoto('groups', groupId, sizeId);
|
||||
}
|
||||
|
||||
async *getGroups(query?: ODataQuery): AsyncIterable<MicrosoftGraph.Group> {
|
||||
yield* this.requestCollection<MicrosoftGraph.Group>(`groups`, query);
|
||||
}
|
||||
@@ -190,6 +165,61 @@ export class MicrosoftGraphClient {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async getPhotoWithSizeLimit(
|
||||
entityName: string,
|
||||
id: string,
|
||||
maxSize: number,
|
||||
): Promise<string | undefined> {
|
||||
const response = await this.requestApi(`${entityName}/${id}/photos`);
|
||||
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
} else if (response.status !== 200) {
|
||||
await this.handleError(`${entityName} photos`, response);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const photos = result.value as MicrosoftGraph.ProfilePhoto[];
|
||||
let selectedPhoto: MicrosoftGraph.ProfilePhoto | undefined = undefined;
|
||||
|
||||
// Find the biggest picture that is smaller than the max size
|
||||
for (const p of photos) {
|
||||
if (
|
||||
!selectedPhoto ||
|
||||
(p.height! >= selectedPhoto.height! && p.height! <= maxSize)
|
||||
) {
|
||||
selectedPhoto = p;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedPhoto) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await this.getPhoto(entityName, id, selectedPhoto.id!);
|
||||
}
|
||||
|
||||
private async getPhoto(
|
||||
entityName: string,
|
||||
id: string,
|
||||
sizeId?: string,
|
||||
): Promise<string | undefined> {
|
||||
const path = sizeId
|
||||
? `${entityName}/${id}/photos/${sizeId}/$value`
|
||||
: `${entityName}/${id}/photo/$value`;
|
||||
const response = await this.requestApi(path);
|
||||
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
} else if (response.status !== 200) {
|
||||
await this.handleError('photo', response);
|
||||
}
|
||||
|
||||
return `data:image/jpeg;base64,${Buffer.from(
|
||||
await response.arrayBuffer(),
|
||||
).toString('base64')}`;
|
||||
}
|
||||
|
||||
private async handleError(path: string, response: Response): Promise<void> {
|
||||
const result = await response.json();
|
||||
const error = result.error as MicrosoftGraph.PublicError;
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('read microsoft graph', () => {
|
||||
getGroups: jest.fn(),
|
||||
getGroupMembers: jest.fn(),
|
||||
getUserPhotoWithSizeLimit: jest.fn(),
|
||||
getGroupPhotoWithSizeLimit: jest.fn(),
|
||||
getOrganization: jest.fn(),
|
||||
} as any;
|
||||
|
||||
@@ -150,6 +151,9 @@ describe('read microsoft graph', () => {
|
||||
},
|
||||
spec: {
|
||||
type: 'root',
|
||||
profile: {
|
||||
displayName: 'Organization Name',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -165,6 +169,8 @@ describe('read microsoft graph', () => {
|
||||
yield {
|
||||
id: 'groupid',
|
||||
displayName: 'Group Name',
|
||||
description: 'Group Description',
|
||||
mail: 'group@example.com',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -185,6 +191,9 @@ describe('read microsoft graph', () => {
|
||||
id: 'tenantid',
|
||||
displayName: 'Organization Name',
|
||||
});
|
||||
client.getGroupPhotoWithSizeLimit.mockResolvedValue(
|
||||
'data:image/jpeg;base64,...',
|
||||
);
|
||||
|
||||
const {
|
||||
groups,
|
||||
@@ -205,6 +214,9 @@ describe('read microsoft graph', () => {
|
||||
},
|
||||
spec: {
|
||||
type: 'root',
|
||||
profile: {
|
||||
displayName: 'Organization Name',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(groups).toEqual([
|
||||
@@ -215,10 +227,17 @@ describe('read microsoft graph', () => {
|
||||
'graph.microsoft.com/group-id': 'groupid',
|
||||
},
|
||||
name: 'group_name',
|
||||
description: 'Group Name',
|
||||
description: 'Group Description',
|
||||
},
|
||||
spec: {
|
||||
type: 'team',
|
||||
profile: {
|
||||
displayName: 'Group Name',
|
||||
email: 'group@example.com',
|
||||
// TODO: Loading groups doesn't work right now as Microsoft Graph
|
||||
// doesn't allows this yet
|
||||
/* picture: 'data:image/jpeg;base64,...',*/
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
@@ -230,10 +249,12 @@ describe('read microsoft graph', () => {
|
||||
expect(client.getGroups).toBeCalledTimes(1);
|
||||
expect(client.getGroups).toBeCalledWith({
|
||||
filter: 'securityEnabled eq false',
|
||||
select: ['id', 'displayName', 'mailNickname'],
|
||||
select: ['id', 'displayName', 'description', 'mail', 'mailNickname'],
|
||||
});
|
||||
expect(client.getGroupMembers).toBeCalledTimes(1);
|
||||
expect(client.getGroupMembers).toBeCalledWith('groupid');
|
||||
expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1);
|
||||
expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function readMicrosoftGraphUsers(
|
||||
users: UserEntity[]; // With all relations empty
|
||||
}> {
|
||||
const entities: UserEntity[] = [];
|
||||
const picturePromises: Promise<void>[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
const limiter = limiterFactory(10);
|
||||
|
||||
for await (const user of client.getUsers({
|
||||
@@ -82,12 +82,12 @@ export async function readMicrosoftGraphUsers(
|
||||
);
|
||||
});
|
||||
|
||||
picturePromises.push(loadPhoto);
|
||||
promises.push(loadPhoto);
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// Wait for all photos to be downloaded
|
||||
await Promise.all(picturePromises);
|
||||
await Promise.all(promises);
|
||||
|
||||
return { users: entities };
|
||||
}
|
||||
@@ -113,6 +113,9 @@ export async function readMicrosoftGraphOrganization(
|
||||
},
|
||||
spec: {
|
||||
type: 'root',
|
||||
profile: {
|
||||
displayName: organization.displayName!,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
@@ -139,11 +142,11 @@ export async function readMicrosoftGraphGroups(
|
||||
groupMember.set(rootGroup.metadata.name, new Set<string>());
|
||||
groups.push(rootGroup);
|
||||
|
||||
const groupMemberPromises: Promise<void>[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for await (const group of client.getGroups({
|
||||
filter: options?.groupFilter,
|
||||
select: ['id', 'displayName', 'mailNickname'],
|
||||
select: ['id', 'displayName', 'description', 'mail', 'mailNickname'],
|
||||
})) {
|
||||
if (!group.id || !group.displayName) {
|
||||
continue;
|
||||
@@ -155,14 +158,17 @@ export async function readMicrosoftGraphGroups(
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
name: name,
|
||||
description: group.displayName,
|
||||
description: group.description ?? undefined,
|
||||
annotations: {
|
||||
[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'team',
|
||||
// TODO: We could include a group email and picture
|
||||
profile: {
|
||||
displayName: group.displayName,
|
||||
email: group.mail ?? undefined,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
@@ -184,12 +190,25 @@ export async function readMicrosoftGraphGroups(
|
||||
}
|
||||
});
|
||||
|
||||
groupMemberPromises.push(loadGroupMembers);
|
||||
// TODO: Loading groups doesn't work right now as Microsoft Graph doesn't
|
||||
// allows this yet: https://microsoftgraph.uservoice.com/forums/920506-microsoft-graph-feature-requests/suggestions/37884922-allow-application-to-set-or-update-a-group-s-photo
|
||||
/*/ / Download the photos in parallel, otherwise it can take quite some time
|
||||
const loadPhoto = limiter(async () => {
|
||||
entity.spec.profile!.picture = await client.getGroupPhotoWithSizeLimit(
|
||||
group.id!,
|
||||
// We are limiting the photo size, as groups with full resolution photos
|
||||
// can make the Backstage API slow
|
||||
120,
|
||||
);
|
||||
});
|
||||
|
||||
promises.push(loadPhoto);*/
|
||||
promises.push(loadGroupMembers);
|
||||
groups.push(entity);
|
||||
}
|
||||
|
||||
// Wait for all group members to be loaded
|
||||
await Promise.all(groupMemberPromises);
|
||||
// Wait for all group members and photos to be loaded
|
||||
await Promise.all(promises);
|
||||
|
||||
return {
|
||||
groups,
|
||||
|
||||
Reference in New Issue
Block a user