From 406dcf06e5fe3ce5d2c4abd74c6a7359605f55f3 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 14 Oct 2021 12:31:12 +0200 Subject: [PATCH 1/4] Add `MicrosoftGraphOrgEntityProvider` Signed-off-by: Oliver Sand --- .changeset/wise-hats-remember.md | 5 + .../MicrosoftGraphOrgEntityProvider.ts | 187 ++++++++++++++++++ .../src/processors/index.ts | 1 + 3 files changed, 193 insertions(+) create mode 100644 .changeset/wise-hats-remember.md create mode 100644 plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts diff --git a/.changeset/wise-hats-remember.md b/.changeset/wise-hats-remember.md new file mode 100644 index 0000000000..6666502bee --- /dev/null +++ b/.changeset/wise-hats-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Add `MicrosoftGraphOrgEntityProvider` as an alternative to `MicrosoftGraphOrgReaderProcessor` that automatically handles user and group deletions. diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts new file mode 100644 index 0000000000..47f9167fbc --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -0,0 +1,187 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { + Entity, + LOCATION_ANNOTATION, + ORIGIN_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend'; +import { merge } from 'lodash'; +import { Logger } from 'winston'; +import { + GroupTransformer, + MicrosoftGraphClient, + MicrosoftGraphProviderConfig, + MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, + MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, + MICROSOFT_GRAPH_USER_ID_ANNOTATION, + OrganizationTransformer, + readMicrosoftGraphConfig, + readMicrosoftGraphOrg, + UserTransformer, +} from '../microsoftGraph'; + +/** + * Reads user and group entries out of Microsoft Graph, and provides them as + * User and Group entities for the catalog. + */ +export class MicrosoftGraphOrgEntityProvider implements EntityProvider { + private connection?: EntityProviderConnection; + + static fromConfig( + config: Config, + options: { + id: string; + target: string; + logger: Logger; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; + }, + ) { + const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg'); + const providers = c ? readMicrosoftGraphConfig(c) : []; + const provider = providers.find(p => options.target.startsWith(p.target)); + + if (!provider) { + throw new Error( + `There is no Microsoft Graph Org provider that matches ${options.target}. Please add a configuration entry for it under catalog.processors.microsoftGraphOrg.providers.`, + ); + } + + if (provider.userFilter && provider.userGroupMemberFilter) { + throw new Error( + `userFilter and userGroupMemberFilter are mutually exclusive, only one can be specified.`, + ); + } + + const logger = options.logger.child({ + target: options.target, + }); + + return new MicrosoftGraphOrgEntityProvider({ + id: options.id, + userTransformer: options.userTransformer, + groupTransformer: options.groupTransformer, + organizationTransformer: options.organizationTransformer, + logger, + provider, + }); + } + + constructor( + private options: { + id: string; + provider: MicrosoftGraphProviderConfig; + logger: Logger; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; + }, + ) {} + + getProviderName() { + return `MicrosoftGraphOrgEntityProvider:${this.options.id}`; + } + + async connect(connection: EntityProviderConnection) { + this.connection = connection; + } + + async read() { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const provider = this.options.provider; + const { markReadComplete } = trackProgress(this.options.logger); + const client = MicrosoftGraphClient.create(this.options.provider); + + const { users, groups } = await readMicrosoftGraphOrg( + client, + provider.tenantId, + { + userFilter: provider.userFilter, + userGroupMemberFilter: provider.userGroupMemberFilter, + groupFilter: provider.groupFilter, + groupTransformer: this.options.groupTransformer, + userTransformer: this.options.userTransformer, + organizationTransformer: this.options.organizationTransformer, + logger: this.options.logger, + }, + ); + + const { markCommitComplete } = markReadComplete({ users, groups }); + + await this.connection.applyMutation({ + type: 'full', + entities: [...users, ...groups].map(entity => ({ + locationKey: `msgraph-org-provider:${this.options.id}`, + entity: withLocations(this.options.id, entity), + })), + }); + + markCommitComplete(); + } +} + +// Helps wrap the timing and logging behaviors +function trackProgress(logger: Logger) { + let timestamp = Date.now(); + let summary: string; + + logger.info('Reading msgraph users and groups'); + + function markReadComplete(read: { users: unknown[]; groups: unknown[] }) { + summary = `${read.users.length} msgraph users and ${read.groups.length} msgraph groups`; + const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1); + timestamp = Date.now(); + logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`); + return { markCommitComplete }; + } + + function markCommitComplete() { + const commitDuration = ((Date.now() - timestamp) / 1000).toFixed(1); + logger.info(`Committed ${summary} in ${commitDuration} seconds.`); + } + + return { markReadComplete }; +} + +// Makes sure that emitted entities have a proper location based on their uuid +function withLocations(providerId: string, entity: Entity): Entity { + const dn = + entity.metadata.annotations?.[MICROSOFT_GRAPH_USER_ID_ANNOTATION] || + entity.metadata.annotations?.[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] || + entity.metadata.annotations?.[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION] || + entity.metadata.name; + const location = `msgraph:${providerId}/${encodeURIComponent(dn)}`; + return merge( + { + metadata: { + annotations: { + [LOCATION_ANNOTATION]: location, + [ORIGIN_LOCATION_ANNOTATION]: location, + }, + }, + }, + entity, + ) as Entity; +} diff --git a/plugins/catalog-backend-module-msgraph/src/processors/index.ts b/plugins/catalog-backend-module-msgraph/src/processors/index.ts index f5dc101563..7f70bde994 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export { MicrosoftGraphOrgEntityProvider } from './MicrosoftGraphOrgEntityProvider'; export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor'; From 4630d393813af22884d0c48a29ebb7ae63ed4f3b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 14 Oct 2021 12:56:54 +0200 Subject: [PATCH 2/4] Add docs Signed-off-by: Oliver Sand --- .../catalog-backend-module-msgraph/README.md | 92 +++++++++++++------ .../api-report.md | 34 +++++++ 2 files changed, 97 insertions(+), 29 deletions(-) diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index 4c41d42915..b89f3af460 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -1,41 +1,24 @@ # Catalog Backend Module for Microsoft Graph This is an extension module to the `plugin-catalog-backend` plugin, providing a -`MicrosoftGraphOrgReaderProcessor` that can be used to ingest organization data -from the Microsoft Graph API. This processor is useful if you want to import -users and groups from Azure Active Directory or Office 365. +`MicrosoftGraphOrgReaderProcessor` and a `MicrosoftGraphOrgEntityProvider` that +can be used to ingest organization data from the Microsoft Graph API. This +processor is useful if you want to import users and groups from Azure Active +Directory or Office 365. ## Getting Started -1. The processor is not installed by default, therefore you have to add a - dependency to `@backstage/plugin-catalog-backend-module-msgraph` to your - backend package. +First you need to decide whether you want to use an [entity provider or a processor](https://backstage.io/docs/features/software-catalog/life-of-an-entity#stitching) to ingest the organization data. +If you want groups and users deleted from the source to be automatically deleted +from Backstage, choose the entity provider. -```bash -# From your Backstage root directory -cd packages/backend -yarn add @backstage/plugin-catalog-backend-module-msgraph -``` - -2. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you - have to register it in the catalog plugin: - -```typescript -// packages/backend/src/plugins/catalog.ts -builder.addProcessor( - MicrosoftGraphOrgReaderProcessor.fromConfig(config, { - logger, - }), -); -``` - -3. Create or use an existing App registration in the [Microsoft Azure Portal](https://portal.azure.com/). +1. Create or use an existing App registration in the [Microsoft Azure Portal](https://portal.azure.com/). The App registration requires at least the API permissions `Group.Read.All`, `GroupMember.Read.All`, `User.Read` and `User.Read.All` for Microsoft Graph (if you still run into errors about insufficient privileges, add `Team.ReadBasic.All` and `TeamMember.Read.All` too). -4. Configure the processor: +2. Configure the processor or entity provider: ```yaml # app-config.yaml @@ -69,6 +52,56 @@ catalog: By default, all users are loaded. If you want to filter users based on their attributes, use `userFilter`. `userGroupMemberFilter` can be used if you want to load users based on their group membership. +3. The package is not installed by default, therefore you have to add a + dependency to `@backstage/plugin-catalog-backend-module-msgraph` to your + backend package. + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-catalog-backend-module-msgraph +``` + +### Using the Entity Provider + +4. The `MicrosoftGraphOrgEntityProvider` is not registered by default, so you + have to register it in the catalog plugin. Pass the target to reference a + provider from the configuration. As entity providers are not part of the + entity refresh loop, you have to run them manually. + +```typescript +// packages/backend/src/plugins/catalog.ts +const msGraphOrgEntityProvider = MicrosoftGraphOrgEntityProvider.fromConfig( + env.config, + { + id: 'https://graph.microsoft.com/v1.0', + target: 'https://graph.microsoft.com/v1.0', + logger: env.logger, + }, +); +builder.addEntityProvider(msGraphOrgEntityProvider); + +// Trigger a read every 5 minutes +useHotCleanup( + module, + runPeriodically(async () => msGraphOrgEntityProvider.read(), 5 * 60 * 1000), +); +``` + +### Using the Processor + +4. The `MicrosoftGraphOrgReaderProcessor` is not registered by default, so you + have to register it in the catalog plugin: + +```typescript +// packages/backend/src/plugins/catalog.ts +builder.addProcessor( + MicrosoftGraphOrgReaderProcessor.fromConfig(config, { + logger, + }), +); +``` + 5. Add a location that ingests from Microsoft Graph: ```yaml @@ -84,10 +117,11 @@ catalog: … ``` -## Customize the Processor +## Customize the Processor or Entity Provider -In case you want to customize the ingested entities, the `MicrosoftGraphOrgReaderProcessor` -allows to pass transformers for users, groups and the organization. +In case you want to customize the ingested entities, both the `MicrosoftGraphOrgReaderProcessor` +and the `MicrosoftGraphOrgEntityProvider` allows to pass transformers for users, +groups and the organization. 1. Create a transformer: diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index d290fcf9b6..1357659739 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -6,6 +6,8 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-backend'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; @@ -93,6 +95,38 @@ export class MicrosoftGraphClient { requestRaw(url: string): Promise; } +// Warning: (ae-missing-release-tag) "MicrosoftGraphOrgEntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class MicrosoftGraphOrgEntityProvider implements EntityProvider { + constructor(options: { + id: string; + provider: MicrosoftGraphProviderConfig; + logger: Logger_2; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; + }); + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + config: Config, + options: { + id: string; + target: string; + logger: Logger_2; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; + }, + ): MicrosoftGraphOrgEntityProvider; + // (undocumented) + getProviderName(): string; + // (undocumented) + read(): Promise; +} + // Warning: (ae-missing-release-tag) "MicrosoftGraphOrgReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public From fc5150de4152cd0157cd6f99885c5748b7cb7a3a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 14 Oct 2021 14:46:17 +0200 Subject: [PATCH 3/4] Fix typo Signed-off-by: Oliver Sand --- docs/integrations/azure/org.md | 2 +- .../src/processors/MicrosoftGraphOrgEntityProvider.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index c359960f4d..8e17bb2cc1 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -7,7 +7,7 @@ description: Importing users and groups from a Microsoft Azure Active Directory --- The Backstage catalog can be set up to ingest organizational data - users and -teams - directly from an tenant in Microsoft Azure Active Directory via the +teams - directly from a tenant in Microsoft Azure Active Directory via the Microsoft Graph API. More details on this are available in the diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 47f9167fbc..55b0933b99 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -167,12 +167,12 @@ function trackProgress(logger: Logger) { // Makes sure that emitted entities have a proper location based on their uuid function withLocations(providerId: string, entity: Entity): Entity { - const dn = + const uuid = entity.metadata.annotations?.[MICROSOFT_GRAPH_USER_ID_ANNOTATION] || entity.metadata.annotations?.[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] || entity.metadata.annotations?.[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION] || entity.metadata.name; - const location = `msgraph:${providerId}/${encodeURIComponent(dn)}`; + const location = `msgraph:${providerId}/${encodeURIComponent(uuid)}`; return merge( { metadata: { From 8ba7481eb8ac7ff28aab2ec89c917d6fd6462e3a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 20 Oct 2021 15:16:12 +0200 Subject: [PATCH 4/4] Add more tests Signed-off-by: Oliver Sand --- .../catalog-backend-module-msgraph/README.md | 2 +- .../src/microsoftGraph/config.test.ts | 17 ++ .../src/microsoftGraph/config.ts | 6 + .../MicrosoftGraphOrgEntityProvider.test.ts | 174 ++++++++++++++++++ .../MicrosoftGraphOrgEntityProvider.ts | 8 +- .../MicrosoftGraphOrgReaderProcessor.test.ts | 143 ++++++++++++++ .../MicrosoftGraphOrgReaderProcessor.ts | 6 - 7 files changed, 342 insertions(+), 14 deletions(-) create mode 100644 plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts create mode 100644 plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.test.ts diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index b89f3af460..0e088d3a4e 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -84,7 +84,7 @@ builder.addEntityProvider(msGraphOrgEntityProvider); // Trigger a read every 5 minutes useHotCleanup( module, - runPeriodically(async () => msGraphOrgEntityProvider.read(), 5 * 60 * 1000), + runPeriodically(() => msGraphOrgEntityProvider.read(), 5 * 60 * 1000), ); ``` diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index 5164439448..efbbca1f5c 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -72,4 +72,21 @@ describe('readMicrosoftGraphConfig', () => { ]; expect(actual).toEqual(expected); }); + + it('should fail if both userFilter and userGroupMemberFilter are set', () => { + const config = { + providers: [ + { + target: 'target', + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + authority: 'https://login.example.com/', + userFilter: 'accountEnabled eq true', + userGroupMemberFilter: 'any', + }, + ], + }; + expect(() => readMicrosoftGraphConfig(new ConfigReader(config))).toThrow(); + }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index fe0678372a..91a6d49b57 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -87,6 +87,12 @@ export function readMicrosoftGraphConfig( ); const groupFilter = providerConfig.getOptionalString('groupFilter'); + if (userFilter && userGroupMemberFilter) { + throw new Error( + `userFilter and userGroupMemberFilter are mutually exclusive, only one can be specified.`, + ); + } + providers.push({ target, authority, diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts new file mode 100644 index 0000000000..eab2d93526 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts @@ -0,0 +1,174 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { + GroupEntity, + LOCATION_ANNOTATION, + ORIGIN_LOCATION_ANNOTATION, + UserEntity, +} from '@backstage/catalog-model'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { + MicrosoftGraphClient, + MICROSOFT_GRAPH_USER_ID_ANNOTATION, + readMicrosoftGraphOrg, +} from '../microsoftGraph'; +import { + MicrosoftGraphOrgEntityProvider, + withLocations, +} from './MicrosoftGraphOrgEntityProvider'; + +jest.mock('../microsoftGraph', () => { + return { + ...jest.requireActual('../microsoftGraph'), + readMicrosoftGraphOrg: jest.fn(), + }; +}); + +const readMicrosoftGraphOrgMocked = readMicrosoftGraphOrg as jest.Mock< + Promise<{ users: UserEntity[]; groups: GroupEntity[] }> +>; + +describe('MicrosoftGraphOrgEntityProvider', () => { + afterEach(() => jest.resetAllMocks()); + + it('should apply mutation', async () => { + jest + .spyOn(MicrosoftGraphClient, 'create') + .mockReturnValue({} as unknown as MicrosoftGraphClient); + + readMicrosoftGraphOrgMocked.mockResolvedValue({ + users: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'u1', + }, + spec: { + memberOf: [], + }, + }, + ], + groups: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'g1', + }, + spec: { + type: 'team', + children: [], + }, + }, + ], + }); + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + }; + const provider = new MicrosoftGraphOrgEntityProvider({ + id: 'test', + logger: getVoidLogger(), + provider: { + target: 'https://example.com', + tenantId: 'tenant', + clientId: 'clientid', + clientSecret: 'clientsecret', + }, + }); + + provider.connect(entityProviderConnection); + + await provider.read(); + + expect(entityProviderConnection.applyMutation).toBeCalledWith({ + entities: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'msgraph:test/u1', + 'backstage.io/managed-by-origin-location': 'msgraph:test/u1', + }, + name: 'u1', + }, + spec: { + memberOf: [], + }, + }, + locationKey: 'msgraph-org-provider:test', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'msgraph:test/g1', + 'backstage.io/managed-by-origin-location': 'msgraph:test/g1', + }, + name: 'g1', + }, + spec: { + children: [], + type: 'team', + }, + }, + locationKey: 'msgraph-org-provider:test', + }, + ], + type: 'full', + }); + }); +}); + +describe('withLocations', () => { + it('should set location annotations', () => { + expect( + withLocations('test', { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'u1', + annotations: { + [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: 'uid', + }, + }, + spec: { + memberOf: [], + }, + }), + ).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'u1', + annotations: { + [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: 'uid', + [LOCATION_ANNOTATION]: 'msgraph:test/uid', + [ORIGIN_LOCATION_ANNOTATION]: 'msgraph:test/uid', + }, + }, + spec: { + memberOf: [], + }, + }); + }); +}); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 55b0933b99..ea396162dd 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -66,12 +66,6 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { ); } - if (provider.userFilter && provider.userGroupMemberFilter) { - throw new Error( - `userFilter and userGroupMemberFilter are mutually exclusive, only one can be specified.`, - ); - } - const logger = options.logger.child({ target: options.target, }); @@ -166,7 +160,7 @@ function trackProgress(logger: Logger) { } // Makes sure that emitted entities have a proper location based on their uuid -function withLocations(providerId: string, entity: Entity): Entity { +export function withLocations(providerId: string, entity: Entity): Entity { const uuid = entity.metadata.annotations?.[MICROSOFT_GRAPH_USER_ID_ANNOTATION] || entity.metadata.annotations?.[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] || diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.test.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.test.ts new file mode 100644 index 0000000000..8236dc52e1 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { MicrosoftGraphClient, readMicrosoftGraphOrg } from '../microsoftGraph'; +import { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor'; + +jest.mock('../microsoftGraph', () => { + return { + ...jest.requireActual('../microsoftGraph'), + readMicrosoftGraphOrg: jest.fn(), + }; +}); + +const readMicrosoftGraphOrgMocked = readMicrosoftGraphOrg as jest.Mock< + Promise<{ users: UserEntity[]; groups: GroupEntity[] }> +>; + +describe('MicrosoftGraphOrgReaderProcessor', () => { + const emit = jest.fn(); + let processor: MicrosoftGraphOrgReaderProcessor; + + beforeEach(() => { + processor = new MicrosoftGraphOrgReaderProcessor({ + providers: [ + { + target: 'https://example.com', + tenantId: 'tenant', + clientId: 'clientid', + clientSecret: 'clientsecret', + }, + ], + logger: getVoidLogger(), + }); + + jest + .spyOn(MicrosoftGraphClient, 'create') + .mockReturnValue({} as unknown as MicrosoftGraphClient); + }); + + afterEach(() => jest.resetAllMocks()); + + it('should process microsoft-graph-org locations', async () => { + const location = { + type: 'microsoft-graph-org', + target: 'https://example.com', + }; + + readMicrosoftGraphOrgMocked.mockResolvedValue({ + users: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'u1', + }, + spec: { + memberOf: [], + }, + }, + ], + groups: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'g1', + }, + spec: { + type: 'team', + children: [], + }, + }, + ], + }); + + const processed = await processor.readLocation(location, false, emit); + + expect(processed).toBe(true); + expect(emit).toBeCalledTimes(2); + expect(emit).toBeCalledWith({ + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'g1', + }, + spec: { + children: [], + type: 'team', + }, + }, + location: { + target: 'https://example.com', + type: 'microsoft-graph-org', + }, + type: 'entity', + }); + expect(emit).toBeCalledWith({ + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'u1', + }, + spec: { + memberOf: [], + }, + }, + location: { + target: 'https://example.com', + type: 'microsoft-graph-org', + }, + type: 'entity', + }); + }); + + it('should ignore other locations', async () => { + const location = { + type: 'url', + target: 'https://example.com', + }; + + const processed = await processor.readLocation(location, false, emit); + + expect(processed).toBe(false); + expect(emit).toBeCalledTimes(0); + }); +}); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 7f4e55c0cc..cb7729e04a 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -90,12 +90,6 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { ); } - if (provider.userFilter && provider.userGroupMemberFilter) { - throw new Error( - `userFilter and userGroupMemberFilter are mutually exclusive, only one can be specified.`, - ); - } - // Read out all of the raw data const startTimestamp = Date.now(); this.logger.info('Reading Microsoft Graph users and groups');