diff --git a/.changeset/metal-badgers-carry.md b/.changeset/metal-badgers-carry.md index 11f1e2f01b..b82e638c6f 100644 --- a/.changeset/metal-badgers-carry.md +++ b/.changeset/metal-badgers-carry.md @@ -6,6 +6,20 @@ Move `MicrosoftGraphOrgReaderProcessor` from `@backstage/plugin-catalog-backend` to `@backstage/plugin-catalog-backend-module-msgraph`. -For now `MicrosoftGraphOrgReaderProcessor` is only deprecated in -`@backstage/plugin-catalog-backend`, but will be removed in the future. While it -is now registered by default, it has to be registered manually in the future. +The `MicrosoftGraphOrgReaderProcessor` isn't registered by default anymore, if +you want to continue using it you have to register it manually at the catalog +builder: + +1. Add dependency to `@backstage/plugin-catalog-backend-module-msgraph` to the `package.json` of your backend. +2. Add the processor to the catalog builder: + +```typescript +// packages/backend/src/plugins/catalog.ts +builder.addProcessor( + MicrosoftGraphOrgReaderProcessor.fromConfig(config, { + logger, + }), +); +``` + +For more configuration details, see the [README of the `@backstage/plugin-catalog-backend-module-msgraph` package](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md). diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 20fcd975cf..5417a84741 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -362,54 +362,6 @@ export interface Config { roleArn?: string; }; }; - - /** - * MicrosoftGraphOrgReaderProcessor configuration - */ - microsoftGraphOrg?: { - /** - * The configuration parameters for each single Microsoft Graph provider. - */ - providers: Array<{ - /** - * The prefix of the target that this matches on, e.g. - * "https://graph.microsoft.com/v1.0", with no trailing slash. - */ - target: string; - /** - * The auth authority used. - * - * Default value "https://login.microsoftonline.com" - */ - authority?: string; - /** - * The tenant whose org data we are interested in. - */ - tenantId: string; - /** - * The OAuth client ID to use for authenticating requests. - */ - clientId: string; - /** - * The OAuth client secret to use for authenticating requests. - * - * @visibility secret - */ - clientSecret: string; - /** - * The filter to apply to extract users. - * - * E.g. "accountEnabled eq true and userType eq 'member'" - */ - userFilter?: string; - /** - * The filter to apply to extract groups. - * - * E.g. "securityEnabled eq false and mailEnabled eq true" - */ - groupFilter?: string; - }>; - }; }; }; } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 6bc53b43da..8e9f54504e 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -29,7 +29,6 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@azure/msal-node": "^1.0.0-beta.3", "@backstage/backend-common": "^0.8.3", "@backstage/catalog-client": "^0.3.13", "@backstage/catalog-model": "^0.8.3", @@ -38,7 +37,6 @@ "@backstage/integration": "^0.5.6", "@backstage/plugin-search-backend-node": "^0.2.1", "@backstage/search-common": "^0.1.2", - "@microsoft/microsoft-graph-types": "^1.25.0", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "@types/ldapjs": "^1.0.9", diff --git a/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts deleted file mode 100644 index 40a7251fcf..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/MicrosoftGraphOrgReaderProcessor.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { LocationSpec } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import { Logger } from 'winston'; -import { - MicrosoftGraphClient, - MicrosoftGraphProviderConfig, - readMicrosoftGraphConfig, - readMicrosoftGraphOrg, -} from './microsoftGraph'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; - -// TODO: Remove this deprecated processor, the related code in -// ./microsoftGraph/, and the config section in the future. - -/** - * Extracts teams and users out of a the Microsoft Graph API. - * - * @deprecated Use the MicrosoftGraphOrgReaderProcessor from package - * @backstage/plugin-catalog-backend-module-msgraph instead. - */ -export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { - private readonly providers: MicrosoftGraphProviderConfig[]; - private readonly logger: Logger; - - static fromConfig(config: Config, options: { logger: Logger }) { - const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg'); - return new MicrosoftGraphOrgReaderProcessor({ - ...options, - providers: c ? readMicrosoftGraphConfig(c) : [], - }); - } - - constructor(options: { - providers: MicrosoftGraphProviderConfig[]; - logger: Logger; - }) { - this.providers = options.providers; - this.logger = options.logger; - } - - async readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise { - if (location.type !== 'microsoft-graph-org') { - return false; - } - - this.logger.warn( - 'MicrosoftGraphOrgReaderProcessor from @backstage/plugin-catalog is deprecated and will be removed in the future. Please migrate to the new one from @backstage/plugin-catalog-backend-module-msgraph instead.', - ); - - const provider = this.providers.find(p => - location.target.startsWith(p.target), - ); - if (!provider) { - throw new Error( - `There is no Microsoft Graph Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.microsoftGraphOrg.providers.`, - ); - } - - // Read out all of the raw data - const startTimestamp = Date.now(); - this.logger.info('Reading Microsoft Graph users and groups'); - - // We create a client each time as we need one that matches the specific provider - const client = MicrosoftGraphClient.create(provider); - const { users, groups } = await readMicrosoftGraphOrg( - client, - provider.tenantId, - { - userFilter: provider.userFilter, - groupFilter: provider.groupFilter, - }, - ); - - const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); - this.logger.debug( - `Read ${users.length} users and ${groups.length} groups from Microsoft Graph in ${duration} seconds`, - ); - - // Done! - for (const group of groups) { - emit(results.entity(location, group)); - } - for (const user of users) { - emit(results.entity(location, user)); - } - - return true; - } -} diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index a92cc9ed3b..8b87e18018 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -27,7 +27,6 @@ export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; export { LocationEntityProcessor } from './LocationEntityProcessor'; -export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; export type { PlaceholderResolver } from './PlaceholderProcessor'; export { StaticLocationProcessor } from './StaticLocationProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.test.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.test.ts deleted file mode 100644 index 5ef82432bb..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.test.ts +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as msal from '@azure/msal-node'; -import { msw } from '@backstage/test-utils'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { MicrosoftGraphClient } from './client'; - -describe('MicrosoftGraphClient', () => { - const confidentialClientApplication: jest.Mocked = { - acquireTokenByClientCredential: jest.fn(), - } as any; - let client: MicrosoftGraphClient; - const worker = setupServer(); - - msw.setupDefaultHandlers(worker); - - beforeEach(() => { - confidentialClientApplication.acquireTokenByClientCredential.mockResolvedValue( - { token: 'ACCESS_TOKEN' } as any, - ); - client = new MicrosoftGraphClient( - 'https://example.com', - confidentialClientApplication, - ); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should perform raw request', async () => { - worker.use( - rest.get('https://other.example.com/', (_, res, ctx) => - res(ctx.status(200), ctx.json({ value: 'example' })), - ), - ); - - const response = await client.requestRaw('https://other.example.com/'); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ value: 'example' }); - expect( - confidentialClientApplication.acquireTokenByClientCredential, - ).toBeCalledTimes(1); - expect( - confidentialClientApplication.acquireTokenByClientCredential, - ).toBeCalledWith({ scopes: ['https://graph.microsoft.com/.default'] }); - }); - - it('should perform simple api request', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res(ctx.status(200), ctx.json({ value: 'example' })), - ), - ); - - const response = await client.requestApi('users'); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ value: 'example' }); - }); - - it('should perform api request with filter, select and expand', async () => { - worker.use( - rest.get('https://example.com/users', (req, res, ctx) => - res(ctx.status(200), ctx.json({ queryString: req.url.search })), - ), - ); - - const response = await client.requestApi('users', { - filter: 'test eq true', - expand: ['children'], - select: ['id', 'children'], - }); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - queryString: - '?$filter=test%20eq%20true&$select=id,children&$expand=children', - }); - }); - - it('should perform collection request for a single page', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: ['first'], - }), - ), - ), - ); - - const values = await collectAsyncIterable( - client.requestCollection('users'), - ); - - expect(values).toEqual(['first']); - }); - - it('should perform collection request for multiple pages', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: ['first'], - '@odata.nextLink': 'https://example.com/users2', - }), - ), - ), - ); - worker.use( - rest.get('https://example.com/users2', (_, res, ctx) => - res(ctx.status(200), ctx.json({ value: ['second'] })), - ), - ); - - const values = await collectAsyncIterable( - client.requestCollection('users'), - ); - - expect(values).toEqual(['first', 'second']); - }); - - it('should load user profile', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - surname: 'Example', - }), - ), - ), - ); - - const userProfile = await client.getUserProfile('user-id'); - - expect(userProfile).toEqual({ surname: 'Example' }); - }); - - it('should throw expection if load user profile fails', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res(ctx.status(404)), - ), - ); - - await expect(() => client.getUserProfile('user-id')).rejects.toThrowError(); - }); - - it('should load user profile photo with max size of 120', async () => { - worker.use( - rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [ - { - height: 120, - id: 120, - }, - { - height: 500, - id: 500, - }, - ], - }), - ), - ), - ); - worker.use( - rest.get( - 'https://example.com/users/user-id/photos/120/*', - (_, res, ctx) => res(ctx.status(200), ctx.text('911')), - ), - ); - - const photo = await client.getUserPhotoWithSizeLimit('user-id', 120); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should not fail if user has no profile photo', async () => { - worker.use( - rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => - res(ctx.status(404)), - ), - ); - - const photo = await client.getUserPhotoWithSizeLimit('user-id', 120); - - expect(photo).toBeFalsy(); - }); - - 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')), - ), - ); - - const photo = await client.getUserPhoto('user-id'); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should load user profile photo for size 120', async () => { - worker.use( - rest.get( - 'https://example.com/users/user-id/photos/120/*', - (_, res, ctx) => res(ctx.status(200), ctx.text('911')), - ), - ); - - const photo = await client.getUserPhoto('user-id', '120'); - - expect(photo).toEqual('data:image/jpeg;base64,OTEx'); - }); - - it('should load users', async () => { - worker.use( - rest.get('https://example.com/users', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [{ surname: 'Example' }], - }), - ), - ), - ); - - const values = await collectAsyncIterable(client.getUsers()); - - 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) => - res( - ctx.status(200), - ctx.json({ - value: [{ displayName: 'Example' }], - }), - ), - ), - ); - - const values = await collectAsyncIterable(client.getGroups()); - - expect(values).toEqual([{ displayName: 'Example' }]); - }); - - it('should load group members', async () => { - worker.use( - rest.get('https://example.com/groups/group-id/members', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - value: [ - { '@odata.type': '#microsoft.graph.user' }, - { '@odata.type': '#microsoft.graph.group' }, - ], - }), - ), - ), - ); - - const values = await collectAsyncIterable( - client.getGroupMembers('group-id'), - ); - - expect(values).toEqual([ - { '@odata.type': '#microsoft.graph.user' }, - { '@odata.type': '#microsoft.graph.group' }, - ]); - }); - - it('should load organization', async () => { - worker.use( - rest.get('https://example.com/organization/tentant-id', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - displayName: 'Example', - }), - ), - ), - ); - - const organization = await client.getOrganization('tentant-id'); - - expect(organization).toEqual({ displayName: 'Example' }); - }); -}); - -async function collectAsyncIterable( - iterable: AsyncIterable, -): Promise { - const values = []; - for await (const value of iterable) { - values.push(value); - } - return values; -} diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.ts deleted file mode 100644 index 3dfc58e773..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/client.ts +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as msal from '@azure/msal-node'; -import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; -import fetch from 'cross-fetch'; -import qs from 'qs'; -import { MicrosoftGraphProviderConfig } from './config'; - -export type ODataQuery = { - filter?: string; - expand?: string[]; - select?: string[]; -}; - -export type GroupMember = - | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.user' }) - | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.group' }); - -export class MicrosoftGraphClient { - static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient { - const clientConfig: msal.Configuration = { - auth: { - clientId: config.clientId, - clientSecret: config.clientSecret, - authority: `${config.authority}/${config.tenantId}`, - }, - }; - const pca = new msal.ConfidentialClientApplication(clientConfig); - return new MicrosoftGraphClient(config.target, pca); - } - - constructor( - private readonly baseUrl: string, - private readonly pca: msal.ConfidentialClientApplication, - ) {} - - async *requestCollection( - path: string, - query?: ODataQuery, - ): AsyncIterable { - let response = await this.requestApi(path, query); - - for (;;) { - if (response.status !== 200) { - await this.handleError(path, response); - } - - const result = await response.json(); - const elements: T[] = result.value; - - yield* elements; - - // Follow cursor to the next page if one is available - if (!result['@odata.nextLink']) { - return; - } - - response = await this.requestRaw(result['@odata.nextLink']); - } - } - - async requestApi(path: string, query?: ODataQuery): Promise { - const queryString = qs.stringify( - { - $filter: query?.filter, - $select: query?.select?.join(','), - $expand: query?.expand?.join(','), - }, - { - addQueryPrefix: true, - // Microsoft Graph doesn't like an encoded query string - encode: false, - }, - ); - - return await this.requestRaw(`${this.baseUrl}/${path}${queryString}`); - } - - async requestRaw(url: string): Promise { - // Make sure that we always have a valid access token (might be cached) - const token = await this.pca.acquireTokenByClientCredential({ - scopes: ['https://graph.microsoft.com/.default'], - }); - - if (!token) { - throw new Error('Error while requesting token for Microsoft Graph'); - } - - return await fetch(url, { - headers: { - Authorization: `Bearer ${token.accessToken}`, - }, - }); - } - - async getUserProfile(userId: string): Promise { - const response = await this.requestApi(`users/${userId}`); - - if (response.status !== 200) { - await this.handleError('user profile', response); - } - - return await response.json(); - } - - async getUserPhotoWithSizeLimit( - userId: string, - maxSize: number, - ): Promise { - return await this.getPhotoWithSizeLimit('users', userId, maxSize); - } - - async getUserPhoto( - userId: string, - sizeId?: string, - ): Promise { - return await this.getPhoto('users', userId, sizeId); - } - - async *getUsers(query?: ODataQuery): AsyncIterable { - yield* this.requestCollection(`users`, query); - } - - async getGroupPhotoWithSizeLimit( - groupId: string, - maxSize: number, - ): Promise { - return await this.getPhotoWithSizeLimit('groups', groupId, maxSize); - } - - async getGroupPhoto( - groupId: string, - sizeId?: string, - ): Promise { - return await this.getPhoto('groups', groupId, sizeId); - } - - async *getGroups(query?: ODataQuery): AsyncIterable { - yield* this.requestCollection(`groups`, query); - } - - async *getGroupMembers(groupId: string): AsyncIterable { - yield* this.requestCollection(`groups/${groupId}/members`); - } - - async getOrganization( - tenantId: string, - ): Promise { - 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 { - 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 { - 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 { - const result = await response.json(); - const error = result.error as MicrosoftGraph.PublicError; - - throw new Error( - `Error while reading ${path} from Microsoft Graph: ${error.code} - ${error.message}`, - ); - } -} diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.test.ts deleted file mode 100644 index 4671fd23ae..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigReader } from '@backstage/config'; -import { readMicrosoftGraphConfig } from './config'; - -describe('readMicrosoftGraphConfig', () => { - it('applies all of the defaults', () => { - const config = { - providers: [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - }, - ], - }; - const actual = readMicrosoftGraphConfig(new ConfigReader(config)); - const expected = [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - authority: 'https://login.microsoftonline.com', - userFilter: undefined, - groupFilter: undefined, - }, - ]; - expect(actual).toEqual(expected); - }); - - it('reads all the values', () => { - const config = { - providers: [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - authority: 'https://login.example.com/', - userFilter: 'accountEnabled eq true', - groupFilter: 'securityEnabled eq false', - }, - ], - }; - const actual = readMicrosoftGraphConfig(new ConfigReader(config)); - const expected = [ - { - target: 'target', - tenantId: 'tenantId', - clientId: 'clientId', - clientSecret: 'clientSecret', - authority: 'https://login.example.com', - userFilter: 'accountEnabled eq true', - groupFilter: 'securityEnabled eq false', - }, - ]; - expect(actual).toEqual(expected); - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.ts deleted file mode 100644 index 72416a63ee..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/config.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Config } from '@backstage/config'; - -/** - * The configuration parameters for a single Microsoft Graph provider. - */ -export type MicrosoftGraphProviderConfig = { - /** - * The prefix of the target that this matches on, e.g. - * "https://graph.microsoft.com/v1.0", with no trailing slash. - */ - target: string; - /** - * The auth authority used. - * - * E.g. "https://login.microsoftonline.com" - */ - authority?: string; - /** - * The tenant whose org data we are interested in. - */ - tenantId: string; - /** - * The OAuth client ID to use for authenticating requests. - */ - clientId: string; - /** - * The OAuth client secret to use for authenticating requests. - * - * @visibility secret - */ - clientSecret: string; - /** - * The filter to apply to extract users. - * - * E.g. "accountEnabled eq true and userType eq 'member'" - */ - userFilter?: string; - /** - * The filter to apply to extract groups. - * - * E.g. "securityEnabled eq false and mailEnabled eq true" - */ - groupFilter?: string; -}; - -export function readMicrosoftGraphConfig( - config: Config, -): MicrosoftGraphProviderConfig[] { - const providers: MicrosoftGraphProviderConfig[] = []; - const providerConfigs = config.getOptionalConfigArray('providers') ?? []; - - for (const providerConfig of providerConfigs) { - const target = providerConfig.getString('target').replace(/\/+$/, ''); - const authority = - providerConfig.getOptionalString('authority')?.replace(/\/+$/, '') || - 'https://login.microsoftonline.com'; - const tenantId = providerConfig.getString('tenantId'); - const clientId = providerConfig.getString('clientId'); - const clientSecret = providerConfig.getString('clientSecret'); - const userFilter = providerConfig.getOptionalString('userFilter'); - const groupFilter = providerConfig.getOptionalString('groupFilter'); - - providers.push({ - target, - authority, - tenantId, - clientId, - clientSecret, - userFilter, - groupFilter, - }); - } - - return providers; -} diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/constants.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/constants.ts deleted file mode 100644 index 6d34d0c159..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/constants.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * The tenant id used by the Microsoft Graph API - */ -export const MICROSOFT_GRAPH_TENANT_ID_ANNOTATION = - 'graph.microsoft.com/tenant-id'; - -/** - * The group id used by the Microsoft Graph API - */ -export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = - 'graph.microsoft.com/group-id'; - -/** - * The user id used by the Microsoft Graph API - */ -export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id'; diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/index.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/index.ts deleted file mode 100644 index 882125fd84..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export { MicrosoftGraphClient } from './client'; -export type { MicrosoftGraphProviderConfig } from './config'; -export { readMicrosoftGraphConfig } from './config'; -export { readMicrosoftGraphOrg } from './read'; -export { - MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, - MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, - MICROSOFT_GRAPH_USER_ID_ANNOTATION, -} from './constants'; diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.test.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.test.ts deleted file mode 100644 index 07d540c848..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.test.ts +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; -import merge from 'lodash/merge'; -import { RecursivePartial } from '../../../util'; -import { GroupMember, MicrosoftGraphClient } from './client'; -import { - normalizeEntityName, - readMicrosoftGraphGroups, - readMicrosoftGraphOrganization, - readMicrosoftGraphUsers, - resolveRelations, -} from './read'; - -function user(data: RecursivePartial): UserEntity { - return merge( - {}, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { name: 'name' }, - spec: { profile: {}, memberOf: [] }, - } as UserEntity, - data, - ); -} - -function group(data: RecursivePartial): GroupEntity { - return merge( - {}, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: 'name', - }, - spec: { - children: [], - type: 'team', - }, - } as GroupEntity, - data, - ); -} - -describe('read microsoft graph', () => { - const client: jest.Mocked = { - getUsers: jest.fn(), - getGroups: jest.fn(), - getGroupMembers: jest.fn(), - getUserPhotoWithSizeLimit: jest.fn(), - getGroupPhotoWithSizeLimit: jest.fn(), - getOrganization: jest.fn(), - } as any; - - afterEach(() => jest.resetAllMocks()); - - describe('normalizeEntityName', () => { - it('should normalize name to valid entity name', () => { - expect(normalizeEntityName('User Name')).toBe('user_name'); - }); - - it('should normalize e-mail to valid entity name', () => { - expect(normalizeEntityName('user.name@example.com')).toBe( - 'user.name_example.com', - ); - }); - }); - - describe('readMicrosoftGraphUsers', () => { - it('should read users', async () => { - async function* getExampleUsers() { - yield { - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } - - client.getUsers.mockImplementation(getExampleUsers); - client.getUserPhotoWithSizeLimit.mockResolvedValue( - 'data:image/jpeg;base64,...', - ); - - const { users } = await readMicrosoftGraphUsers(client, { - userFilter: 'accountEnabled eq true', - }); - - expect(users).toEqual([ - user({ - metadata: { - annotations: { - 'graph.microsoft.com/user-id': 'userid', - }, - name: 'user.name_example.com', - }, - spec: { - profile: { - displayName: 'User Name', - email: 'user.name@example.com', - picture: 'data:image/jpeg;base64,...', - }, - }, - }), - ]); - - expect(client.getUsers).toBeCalledTimes(1); - expect(client.getUsers).toBeCalledWith({ - filter: 'accountEnabled eq true', - select: ['id', 'displayName', 'mail'], - }); - expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); - expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); - }); - }); - - describe('readMicrosoftGraphOrganization', () => { - it('should read organization', async () => { - client.getOrganization.mockResolvedValue({ - id: 'tenantid', - displayName: 'Organization Name', - }); - - const { rootGroup } = await readMicrosoftGraphOrganization( - client, - 'tenantid', - ); - - expect(rootGroup).toEqual( - group({ - metadata: { - annotations: { - 'graph.microsoft.com/tenant-id': 'tenantid', - }, - name: 'organization_name', - description: 'Organization Name', - }, - spec: { - type: 'root', - profile: { - displayName: 'Organization Name', - }, - }, - }), - ); - - expect(client.getOrganization).toBeCalledTimes(1); - expect(client.getOrganization).toBeCalledWith('tenantid'); - }); - }); - - describe('readMicrosoftGraphGroups', () => { - it('should read groups', 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', { - 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', - }, - }, - }); - 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 doesn't work right now as Microsoft Graph - // doesn't allows this yet - /* picture: 'data:image/jpeg;base64,...',*/ - }, - }, - }), - ]); - 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({ - filter: 'securityEnabled eq false', - select: ['id', 'displayName', 'description', 'mail', 'mailNickname'], - }); - expect(client.getGroupMembers).toBeCalledTimes(1); - expect(client.getGroupMembers).toBeCalledWith('groupid'); - // TODO: Loading groups doesn't work right now as Microsoft Graph - // doesn't allows this yet - // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); - // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); - }); - }); - - describe('resolveRelations', () => { - it('should resolve relations', async () => { - const rootGroup = group({ - metadata: { - annotations: { - 'graph.microsoft.com/tenant-id': 'tenant-id-root', - }, - name: 'root', - }, - spec: { - type: 'root', - }, - }); - const groupA = group({ - metadata: { - annotations: { - 'graph.microsoft.com/group-id': 'group-id-a', - }, - name: 'a', - }, - }); - const groupB = group({ - metadata: { - annotations: { - 'graph.microsoft.com/group-id': 'group-id-b', - }, - name: 'b', - }, - }); - const groupC = group({ - metadata: { - annotations: { - 'graph.microsoft.com/group-id': 'group-id-c', - }, - name: 'c', - }, - }); - const user1 = user({ - metadata: { - annotations: { - 'graph.microsoft.com/user-id': 'user-id-1', - }, - name: 'user1', - }, - }); - const user2 = user({ - metadata: { - annotations: { - 'graph.microsoft.com/user-id': 'user-id-2', - }, - name: 'user2', - }, - }); - const groups = [rootGroup, groupA, groupB, groupC]; - const users = [user1, user2]; - const groupMember = new Map>(); - groupMember.set('group-id-b', new Set(['group-id-c'])); - const groupMemberOf = new Map>(); - groupMemberOf.set('user-id-1', new Set(['group-id-a'])); - groupMemberOf.set('user-id-2', new Set(['group-id-c'])); - - // We have a root groups - // We have three groups: a, b, c. c is child of b - // we have two users: u1, u2. u1 is member of a, u2 is member of c - resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); - - expect(rootGroup.spec.parent).toBeUndefined(); - expect(rootGroup.spec.children).toEqual( - expect.arrayContaining(['a', 'b']), - ); - - expect(groupA.spec.parent).toEqual('root'); - expect(groupA.spec.children).toEqual(expect.arrayContaining([])); - - expect(groupB.spec.parent).toEqual('root'); - expect(groupB.spec.children).toEqual(expect.arrayContaining(['c'])); - - expect(groupC.spec.parent).toEqual('b'); - expect(groupC.spec.children).toEqual(expect.arrayContaining([])); - - expect(user1.spec.memberOf).toEqual(expect.arrayContaining(['a'])); - expect(user2.spec.memberOf).toEqual(expect.arrayContaining(['b', 'c'])); - }); - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.ts b/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.ts deleted file mode 100644 index 4409422645..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/microsoftGraph/read.ts +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; -import limiterFactory from 'p-limit'; -import { buildMemberOf, buildOrgHierarchy } from '../util/org'; -import { MicrosoftGraphClient } from './client'; -import { - MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, - MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, - MICROSOFT_GRAPH_USER_ID_ANNOTATION, -} from './constants'; - -export function normalizeEntityName(name: string): string { - return name - .trim() - .toLocaleLowerCase() - .replace(/[^a-zA-Z0-9_\-\.]/g, '_'); -} - -export async function readMicrosoftGraphUsers( - client: MicrosoftGraphClient, - options?: { userFilter?: string }, -): Promise<{ - users: UserEntity[]; // With all relations empty -}> { - const entities: UserEntity[] = []; - const promises: Promise[] = []; - const limiter = limiterFactory(10); - - for await (const user of client.getUsers({ - filter: options?.userFilter, - select: ['id', 'displayName', 'mail'], - })) { - if (!user.id || !user.displayName || !user.mail) { - continue; - } - - const name = normalizeEntityName(user.mail); - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name, - annotations: { - [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, - }, - }, - spec: { - profile: { - displayName: user.displayName!, - email: user.mail!, - - // TODO: Additional fields? - // jobTitle: user.jobTitle || undefined, - // officeLocation: user.officeLocation || undefined, - // mobilePhone: user.mobilePhone || undefined, - }, - memberOf: [], - }, - }; - - // Download the photos in parallel, otherwise it can take quite some time - const loadPhoto = limiter(async () => { - entity.spec.profile!.picture = await client.getUserPhotoWithSizeLimit( - user.id!, - // We are limiting the photo size, as users with full resolution photos - // can make the Backstage API slow - 120, - ); - }); - - promises.push(loadPhoto); - entities.push(entity); - } - - // Wait for all photos to be downloaded - await Promise.all(promises); - - return { users: entities }; -} - -export async function readMicrosoftGraphOrganization( - client: MicrosoftGraphClient, - tenantId: string, -): Promise<{ - rootGroup: GroupEntity; // With all relations empty -}> { - // For now we expect a single root organization - const organization = await client.getOrganization(tenantId); - const name = normalizeEntityName(organization.displayName!); - const rootGroup: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - description: organization.displayName!, - annotations: { - [MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]: organization.id!, - }, - }, - spec: { - type: 'root', - profile: { - displayName: organization.displayName!, - }, - children: [], - }, - }; - - return { rootGroup }; -} - -export async function readMicrosoftGraphGroups( - client: MicrosoftGraphClient, - tenantId: string, - options?: { groupFilter?: string }, -): Promise<{ - groups: GroupEntity[]; // With all relations empty - rootGroup: GroupEntity | undefined; // With all relations empty - groupMember: Map>; - groupMemberOf: Map>; -}> { - const groups: GroupEntity[] = []; - const groupMember: Map> = new Map(); - const groupMemberOf: Map> = new Map(); - const limiter = limiterFactory(10); - - const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId); - groupMember.set(rootGroup.metadata.name, new Set()); - groups.push(rootGroup); - - const promises: Promise[] = []; - - for await (const group of client.getGroups({ - filter: options?.groupFilter, - select: ['id', 'displayName', 'description', 'mail', 'mailNickname'], - })) { - if (!group.id || !group.displayName) { - continue; - } - - const name = normalizeEntityName(group.mailNickname || group.displayName); - const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: name, - annotations: { - [MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]: group.id, - }, - }, - spec: { - type: 'team', - profile: {}, - children: [], - }, - }; - - if (group.description) { - entity.metadata.description = group.description; - } - if (group.displayName) { - entity.spec.profile!.displayName = group.displayName; - } - if (group.mail) { - entity.spec.profile!.email = group.mail; - } - - // Download the members in parallel, otherwise it can take quite some time - const loadGroupMembers = limiter(async () => { - for await (const member of client.getGroupMembers(group.id!)) { - if (!member.id) { - continue; - } - - if (member['@odata.type'] === '#microsoft.graph.user') { - ensureItem(groupMemberOf, member.id, group.id!); - } - - if (member['@odata.type'] === '#microsoft.graph.group') { - ensureItem(groupMember, group.id!, member.id); - } - } - }); - - // 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 and photos to be loaded - await Promise.all(promises); - - return { - groups, - rootGroup, - groupMember, - groupMemberOf, - }; -} - -export function resolveRelations( - rootGroup: GroupEntity | undefined, - groups: GroupEntity[], - users: UserEntity[], - groupMember: Map>, - groupMemberOf: Map>, -) { - // Build reference lookup tables, we reference them by the id the the graph - const groupMap: Map = new Map(); // by group-id or tenant-id - - for (const group of groups) { - if (group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION]) { - groupMap.set( - group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION], - group, - ); - } - if (group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]) { - groupMap.set( - group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION], - group, - ); - } - } - - // Resolve all member relationships into the reverse direction - const parentGroups = new Map>(); - - groupMember.forEach((members, groupId) => - members.forEach(m => ensureItem(parentGroups, m, groupId)), - ); - - // Make sure every group (except root) has at least one parent. If the parent is missing, add the root. - if (rootGroup) { - const tenantId = rootGroup.metadata.annotations![ - MICROSOFT_GRAPH_TENANT_ID_ANNOTATION - ]; - - groups.forEach(group => { - const groupId = group.metadata.annotations![ - MICROSOFT_GRAPH_GROUP_ID_ANNOTATION - ]; - - if (!groupId) { - return; - } - - if (retrieveItems(parentGroups, groupId).size === 0) { - ensureItem(parentGroups, groupId, tenantId); - ensureItem(groupMember, tenantId, groupId); - } - }); - } - - groups.forEach(group => { - const id = - group.metadata.annotations![MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] ?? - group.metadata.annotations![MICROSOFT_GRAPH_TENANT_ID_ANNOTATION]; - - retrieveItems(groupMember, id).forEach(m => { - const childGroup = groupMap.get(m); - if (childGroup) { - group.spec.children.push(childGroup.metadata.name); - } - }); - - retrieveItems(parentGroups, id).forEach(p => { - const parentGroup = groupMap.get(p); - if (parentGroup) { - // TODO: Only having a single parent group might not match every companies model, but fine for now. - group.spec.parent = parentGroup.metadata.name; - } - }); - }); - - // Make sure that all groups have proper parents and children - buildOrgHierarchy(groups); - - // Set relations for all users - users.forEach(user => { - const id = user.metadata.annotations![MICROSOFT_GRAPH_USER_ID_ANNOTATION]; - - retrieveItems(groupMemberOf, id).forEach(p => { - const parentGroup = groupMap.get(p); - if (parentGroup) { - user.spec.memberOf.push(parentGroup.metadata.name); - } - }); - }); - - // Make sure all transitive memberships are available - buildMemberOf(groups, users); -} - -export async function readMicrosoftGraphOrg( - client: MicrosoftGraphClient, - tenantId: string, - options?: { userFilter?: string; groupFilter?: string }, -): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> { - const { users } = await readMicrosoftGraphUsers(client, { - userFilter: options?.userFilter, - }); - const { - groups, - rootGroup, - groupMember, - groupMemberOf, - } = await readMicrosoftGraphGroups(client, tenantId, { - groupFilter: options?.groupFilter, - }); - - resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); - users.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); - groups.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); - - return { users, groups }; -} - -function ensureItem( - target: Map>, - key: string, - value: string, -) { - let set = target.get(key); - if (!set) { - set = new Set(); - target.set(key, set); - } - set!.add(value); -} - -function retrieveItems( - target: Map>, - key: string, -): Set { - return target.get(key) ?? new Set(); -} diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index 2551c16879..0bb5cd0aee 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -50,7 +50,6 @@ import { GithubDiscoveryProcessor, GithubOrgReaderProcessor, LdapOrgReaderProcessor, - MicrosoftGraphOrgReaderProcessor, PlaceholderProcessor, PlaceholderResolver, UrlReaderProcessor, @@ -373,7 +372,6 @@ export class NextCatalogBuilder { GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), LdapOrgReaderProcessor.fromConfig(config, { logger }), - MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), CodeOwnersProcessor.fromConfig(config, { logger, reader }), new AnnotateLocationEntityProcessor({ integrations }), diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 9bec4442d7..1901d18f72 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -51,7 +51,6 @@ import { LdapOrgReaderProcessor, LocationEntityProcessor, LocationReaders, - MicrosoftGraphOrgReaderProcessor, PlaceholderProcessor, PlaceholderResolver, StaticLocationProcessor, @@ -319,7 +318,6 @@ export class CatalogBuilder { GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), LdapOrgReaderProcessor.fromConfig(config, { logger }), - MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), CodeOwnersProcessor.fromConfig(config, { logger, reader }), new LocationEntityProcessor({ integrations }), diff --git a/yarn.lock b/yarn.lock index ab538fb58a..0b1af46a72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,7 +225,7 @@ dependencies: debug "^4.1.1" -"@azure/msal-node@1.0.0-beta.3", "@azure/msal-node@^1.0.0-beta.3": +"@azure/msal-node@1.0.0-beta.3": version "1.0.0-beta.3" resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.3.tgz#c84c7948028b39e48b901f5fac35bdedcbc8772e" integrity sha512-/KfYRfrsOIrZONvo/0Vi5umuqbPBtCWNtmRvkse64uI0C4CP/W4WXwRD42VMws/8LtKvr1I5rYlYgFzt5zDz/A==