Skip to content

Commit

Permalink
Add support for the group profile element to Microsoft Graph processor
Browse files Browse the repository at this point in the history
  • Loading branch information
Fox32 committed Dec 15, 2020
1 parent c911061 commit 0097057
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 45 deletions.
7 changes: 7 additions & 0 deletions .changeset/witty-scissors-divide.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand All @@ -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/*',
Expand Down Expand Up @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,65 @@ export class MicrosoftGraphClient {
userId: string,
maxSize: number,
): Promise<string | undefined> {
const response = await this.requestApi(`users/${userId}/photos`);
return await this.getPhotoWithSizeLimit('users', userId, maxSize);
}

async getUserPhoto(
userId: string,
sizeId?: string,
): Promise<string | undefined> {
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);
}

async *getGroupMembers(groupId: string): AsyncIterable<GroupMember> {
yield* this.requestCollection<GroupMember>(`groups/${groupId}/members`);
}

async getOrganization(
tenantId: string,
): Promise<MicrosoftGraph.Organization> {
const response = await this.requestApi(`organization/${tenantId}`);

if (response.status !== 200) {
await this.handleError(`organization/${tenantId}`, response);
}

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('user photos', response);
await this.handleError(`${entityName} photos`, response);
}

const result = await response.json();
Expand All @@ -143,16 +196,17 @@ export class MicrosoftGraphClient {
return undefined;
}

return await this.getUserPhoto(userId, selectedPhoto.id!);
return await this.getPhoto(entityName, id, selectedPhoto.id!);
}

async getUserPhoto(
userId: string,
private async getPhoto(
entityName: string,
id: string,
sizeId?: string,
): Promise<string | undefined> {
const path = sizeId
? `users/${userId}/photos/${sizeId}/$value`
: `users/${userId}/photo/$value`;
? `${entityName}/${id}/photos/${sizeId}/$value`
: `${entityName}/${id}/photo/$value`;
const response = await this.requestApi(path);

if (response.status === 404) {
Expand All @@ -166,30 +220,6 @@ export class MicrosoftGraphClient {
).toString('base64')}`;
}

async *getUsers(query?: ODataQuery): AsyncIterable<MicrosoftGraph.User> {
yield* this.requestCollection<MicrosoftGraph.User>(`users`, query);
}

async *getGroups(query?: ODataQuery): AsyncIterable<MicrosoftGraph.Group> {
yield* this.requestCollection<MicrosoftGraph.Group>(`groups`, query);
}

async *getGroupMembers(groupId: string): AsyncIterable<GroupMember> {
yield* this.requestCollection<GroupMember>(`groups/${groupId}/members`);
}

async getOrganization(
tenantId: string,
): Promise<MicrosoftGraph.Organization> {
const response = await this.requestApi(`organization/${tenantId}`);

if (response.status !== 200) {
await this.handleError(`organization/${tenantId}`, response);
}

return await response.json();
}

private async handleError(path: string, response: Response): Promise<void> {
const result = await response.json();
const error = result.error as MicrosoftGraph.PublicError;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -150,6 +151,9 @@ describe('read microsoft graph', () => {
},
spec: {
type: 'root',
profile: {
displayName: 'Organization Name',
},
},
}),
);
Expand All @@ -165,6 +169,8 @@ describe('read microsoft graph', () => {
yield {
id: 'groupid',
displayName: 'Group Name',
description: 'Group Description',
mail: 'group@example.com',
};
}

Expand All @@ -185,6 +191,9 @@ describe('read microsoft graph', () => {
id: 'tenantid',
displayName: 'Organization Name',
});
client.getGroupPhotoWithSizeLimit.mockResolvedValue(
'data:image/jpeg;base64,...',
);

const {
groups,
Expand All @@ -205,6 +214,9 @@ describe('read microsoft graph', () => {
},
spec: {
type: 'root',
profile: {
displayName: 'Organization Name',
},
},
});
expect(groups).toEqual([
Expand All @@ -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,...',*/
},
},
}),
]);
Expand All @@ -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);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 };
}
Expand All @@ -113,6 +113,9 @@ export async function readMicrosoftGraphOrganization(
},
spec: {
type: 'root',
profile: {
displayName: organization.displayName!,
},
children: [],
},
};
Expand All @@ -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;
Expand All @@ -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: [],
},
};
Expand All @@ -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,
Expand Down

0 comments on commit 0097057

Please sign in to comment.