diff --git a/.changeset/tender-dogs-behave.md b/.changeset/tender-dogs-behave.md new file mode 100644 index 0000000000..f59842c70a --- /dev/null +++ b/.changeset/tender-dogs-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +add configuration to use search criteria to select groups diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index dd967a8dd4..7ad603e6ca 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -80,9 +80,16 @@ export class MicrosoftGraphClient { ): Promise; getUserProfile(userId: string): Promise; getUsers(query?: ODataQuery): AsyncIterable; - requestApi(path: string, query?: ODataQuery): Promise; + requestApi( + path: string, + query?: ODataQuery, + headers?: Record, + ): Promise; requestCollection(path: string, query?: ODataQuery): AsyncIterable; - requestRaw(url: string): Promise; + requestRaw( + url: string, + headers?: Record, + ): Promise; } // @public @@ -153,7 +160,9 @@ export type MicrosoftGraphProviderConfig = { userFilter?: string; userExpand?: string[]; userGroupMemberFilter?: string; + userGroupMemberSearch?: string; groupFilter?: string; + groupSearch?: string; }; // @public @@ -161,6 +170,7 @@ export function normalizeEntityName(name: string): string; // @public export type ODataQuery = { + search?: string; filter?: string; expand?: string[]; select?: string[]; @@ -183,7 +193,9 @@ export function readMicrosoftGraphOrg( options: { userExpand?: string[]; userFilter?: string; + userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index c7eb5ae8e4..d47abf60bb 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -73,12 +73,24 @@ export interface Config { * E.g. "securityEnabled eq false and mailEnabled eq true" */ groupFilter?: string; + /** + * The search criteria to apply to extract users by groups memberships. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + groupSearch?: string; /** * The filter to apply to extract users by groups memberships. * * E.g. "displayName eq 'Backstage Users'" */ userGroupMemberFilter?: string; + /** + * The search criteria to apply to extract groups. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + userGroupMemberSearch?: string; }>; }; }; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 827c9fc14d..db749a536e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -27,6 +27,10 @@ import { MicrosoftGraphProviderConfig } from './config'; * @public */ export type ODataQuery = { + /** + * search resources within a collection matching a free-text search expression. + */ + search?: string; /** * filter a collection of resources */ @@ -102,7 +106,15 @@ export class MicrosoftGraphClient { path: string, query?: ODataQuery, ): AsyncIterable { - let response = await this.requestApi(path, query); + const headers: Record = query?.search + ? { + // Eventual consistency is required to use $search. + // If a new user/group is not found, it'll eventually be imported on a subsequent read + ConsistencyLevel: 'eventual', + } + : {}; + + let response = await this.requestApi(path, query, headers); for (;;) { if (response.status !== 200) { @@ -121,7 +133,7 @@ export class MicrosoftGraphClient { return; } - response = await this.requestRaw(result['@odata.nextLink']); + response = await this.requestRaw(result['@odata.nextLink'], headers); } } @@ -131,10 +143,16 @@ export class MicrosoftGraphClient { * @public * @param path - Resource in Microsoft Graph * @param query - OData Query {@link ODataQuery} + * @param headers - optional HTTP headers */ - async requestApi(path: string, query?: ODataQuery): Promise { + async requestApi( + path: string, + query?: ODataQuery, + headers?: Record, + ): Promise { const queryString = qs.stringify( { + $search: query?.search, $filter: query?.filter, $select: query?.select?.join(','), $expand: query?.expand?.join(','), @@ -146,15 +164,22 @@ export class MicrosoftGraphClient { }, ); - return await this.requestRaw(`${this.baseUrl}/${path}${queryString}`); + return await this.requestRaw( + `${this.baseUrl}/${path}${queryString}`, + headers, + ); } /** * Makes a HTTP call to Graph API with token * * @param url - HTTP Endpoint of Graph API + * @param headers - optional HTTP headers */ - async requestRaw(url: string): Promise { + async requestRaw( + url: string, + headers?: Record, + ): 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'], @@ -166,6 +191,7 @@ export class MicrosoftGraphClient { return await fetch(url, { headers: { + ...headers, Authorization: `Bearer ${token.accessToken}`, }, }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index c401bc2553..6809144f80 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -64,12 +64,24 @@ export type MicrosoftGraphProviderConfig = { * E.g. "displayName eq 'Backstage Users'" */ userGroupMemberFilter?: string; + /** + * The search criteria to apply to extract users by groups memberships. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + userGroupMemberSearch?: string; /** * The filter to apply to extract groups. * * E.g. "securityEnabled eq false and mailEnabled eq true" */ groupFilter?: string; + /** + * The search criteria to apply to extract groups. + * + * E.g. "\"displayName:-team\"" would only match groups which contain '-team' + */ + groupSearch?: string; }; /** @@ -98,13 +110,22 @@ export function readMicrosoftGraphConfig( const userGroupMemberFilter = providerConfig.getOptionalString( 'userGroupMemberFilter', ); + const userGroupMemberSearch = providerConfig.getOptionalString( + 'userGroupMemberSearch', + ); const groupFilter = providerConfig.getOptionalString('groupFilter'); + const groupSearch = providerConfig.getOptionalString('groupSearch'); if (userFilter && userGroupMemberFilter) { throw new Error( `userFilter and userGroupMemberFilter are mutually exclusive, only one can be specified.`, ); } + if (userFilter && userGroupMemberSearch) { + throw new Error( + `userGroupMemberSearch cannot be specified when userFilter is defined.`, + ); + } providers.push({ target, @@ -114,7 +135,9 @@ export function readMicrosoftGraphConfig( clientSecret, userFilter, userGroupMemberFilter, + userGroupMemberSearch, groupFilter, + groupSearch, }); } diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 58e60ab76a..f862fd5ace 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -137,6 +137,7 @@ export async function readMicrosoftGraphUsers( export async function readMicrosoftGraphUsersInGroups( client: MicrosoftGraphClient, options: { + userGroupMemberSearch?: string; userGroupMemberFilter?: string; transformer?: UserTransformer; logger: Logger; @@ -155,6 +156,7 @@ export async function readMicrosoftGraphUsersInGroups( const groupMemberUsers: Set = new Set(); for await (const group of client.getGroups({ + search: options?.userGroupMemberSearch, filter: options?.userGroupMemberFilter, })) { // Process all groups in parallel, otherwise it can take quite some time @@ -324,6 +326,7 @@ export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, options?: { + groupSearch?: string; groupFilter?: string; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -351,6 +354,7 @@ export async function readMicrosoftGraphGroups( const promises: Promise[] = []; for await (const group of client.getGroups({ + search: options?.groupSearch, filter: options?.groupFilter, })) { // Process all groups in parallel, otherwise it can take quite some time @@ -504,7 +508,9 @@ export async function readMicrosoftGraphOrg( options: { userExpand?: string[]; userFilter?: string; + userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; @@ -519,6 +525,7 @@ export async function readMicrosoftGraphOrg( client, { userGroupMemberFilter: options.userGroupMemberFilter, + userGroupMemberSearch: options.userGroupMemberSearch, transformer: options.userTransformer, logger: options.logger, }, @@ -535,6 +542,7 @@ export async function readMicrosoftGraphOrg( } const { groups, rootGroup, groupMember, groupMemberOf } = await readMicrosoftGraphGroups(client, tenantId, { + groupSearch: options?.groupSearch, groupFilter: options?.groupFilter, groupTransformer: options?.groupTransformer, organizationTransformer: options?.organizationTransformer, diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 92836893f0..7d27e87d73 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -123,7 +123,9 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { { userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, + userGroupMemberSearch: provider.userGroupMemberSearch, groupFilter: provider.groupFilter, + groupSearch: provider.groupSearch, groupTransformer: this.options.groupTransformer, userTransformer: this.options.userTransformer, organizationTransformer: this.options.organizationTransformer, diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 2725075c1c..4db3a9b1a0 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -108,7 +108,9 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { userExpand: provider.userExpand, userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, + userGroupMemberSearch: provider.userGroupMemberSearch, groupFilter: provider.groupFilter, + groupSearch: provider.groupSearch, userTransformer: this.userTransformer, groupTransformer: this.groupTransformer, organizationTransformer: this.organizationTransformer,