From 2bd0450cb492cef87d2ebf2df7f2597d991fc82c Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Tue, 19 May 2026 09:30:30 +0100 Subject: [PATCH] feat(catalog-backend-module-msgraph): filter out disabled users by default (#34165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(catalog-backend-module-msgraph): filter out disabled users by default The Microsoft Graph provider now always applies an `accountEnabled eq true` base filter when fetching users. Any custom `user.filter` is combined with the base filter using `and`, so adopters no longer need to manually add `accountEnabled eq true` to their configuration. Also removes the legacy mutual exclusivity check between `userFilter` and `userGroupMemberFilter` — these serve orthogonal purposes (user-level filtering vs group selection) and the downstream code already handles both being set. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: MT Lewis * chore: mark msgraph disabled-user filtering as breaking change Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: MT Lewis * docs(catalog-backend-module-msgraph): clarify automatic accountEnabled filter in docs Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: MT Lewis --------- Signed-off-by: MT Lewis Co-authored-by: Claude Opus 4.6 (1M context) --- .changeset/msgraph-filter-disabled-users.md | 6 ++ docs/integrations/azure/org.md | 10 +-- .../README.md | 3 +- .../catalog-backend-module-msgraph/README.md | 11 ++- .../config.d.ts | 9 ++- .../src/microsoftGraph/config.test.ts | 69 ++++--------------- .../src/microsoftGraph/config.ts | 41 ++++------- 7 files changed, 51 insertions(+), 98 deletions(-) create mode 100644 .changeset/msgraph-filter-disabled-users.md diff --git a/.changeset/msgraph-filter-disabled-users.md b/.changeset/msgraph-filter-disabled-users.md new file mode 100644 index 0000000000..b5bd94f6ba --- /dev/null +++ b/.changeset/msgraph-filter-disabled-users.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': minor +'@backstage/plugin-catalog-backend-module-msgraph-incremental': minor +--- + +**BREAKING**: Disabled user accounts are now filtered out by default. The provider automatically applies an `accountEnabled eq true` filter, combining it with any custom `user.filter` you provide. If you previously included `accountEnabled eq true` in your user filter, it is safe to remove it, but leaving it in will not cause any issues. diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index defe0aab41..cf18af8085 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -26,7 +26,7 @@ catalog: default: tenantId: ${AZURE_TENANT_ID} user: - filter: accountEnabled eq true and userType eq 'member' + filter: userType eq 'member' group: filter: > securityEnabled eq false @@ -121,8 +121,9 @@ To grant the managed identity the same permissions as mentioned in _App Registra ## Filtering imported Users and Groups -By default, the plugin will import all users and groups from your directory. -This can be customized through [filters](https://learn.microsoft.com/en-us/graph/filter-query-parameter) and [search](https://learn.microsoft.com/en-us/graph/search-query-parameter) queries. Keep in mind that if you omit filters and search queries for the user or group properties, the plugin will automatically import all available users or groups. +By default, the plugin will import all **enabled** users and all groups from your directory. +Disabled user accounts (`accountEnabled eq false`) are automatically excluded. +This can be further customized through [filters](https://learn.microsoft.com/en-us/graph/filter-query-parameter) and [search](https://learn.microsoft.com/en-us/graph/search-query-parameter) queries. Any custom `user.filter` is combined with the base `accountEnabled eq true` filter using `and`. ### Groups @@ -156,12 +157,13 @@ By default the provider will get groups using the msgraph `/group` endpoint, but ### Users There are two modes for importing users - You can import all user objects matching a `filter`. +The `accountEnabled eq true` base filter is applied automatically and combined with any custom filter you provide. ```yaml microsoftGraphOrg: providerId: user: - filter: accountEnabled eq true and userType eq 'member' + filter: userType eq 'member' ``` Alternatively you can import users that are members of specific groups. diff --git a/plugins/catalog-backend-module-msgraph-incremental/README.md b/plugins/catalog-backend-module-msgraph-incremental/README.md index 342148f049..79b3cdbce4 100644 --- a/plugins/catalog-backend-module-msgraph-incremental/README.md +++ b/plugins/catalog-backend-module-msgraph-incremental/README.md @@ -55,7 +55,8 @@ catalog: clientSecret: ${AZURE_CLIENT_SECRET} queryMode: advanced user: - filter: 'accountEnabled eq true' + # accountEnabled eq true is applied by default; add extra filters here + filter: "userType eq 'member'" group: filter: 'securityEnabled eq true' schedule: diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index 8e8c84de37..da466f7553 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -52,8 +52,9 @@ catalog: # Optional filter for user, see Microsoft Graph API for the syntax # See https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter - # This and userGroupMemberFilter are mutually exclusive, only one can be specified - filter: accountEnabled eq true and userType eq 'member' + # This is combined with the base `accountEnabled eq true` filter + # that is always applied automatically. + filter: userType eq 'member' # Set to false to not load user photos. loadPhotos: true # See https://docs.microsoft.com/en-us/graph/api/resources/schemaextension?view=graph-rest-1.0 @@ -64,12 +65,10 @@ catalog: userGroupMember: # Optional filter for users, use group membership to get users. # (Filtered groups and fetch their members.) - # This and userFilter are mutually exclusive, only one can be specified # See https://docs.microsoft.com/en-us/graph/search-query-parameter filter: "displayName eq 'Backstage Users'" # Optional search for users, use group membership to get users. # (Search for groups and fetch their members.) - # This and userFilter are mutually exclusive, only one can be specified search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' # Optional /groups by default but allow to query groups from different msgraph endpoints path: /groups @@ -102,9 +101,7 @@ catalog: initialDelay: { seconds: 15}, ``` -`user.filter` and `userGroupMember.filter` are mutually exclusive, only one can be provided. If both are provided, an error will be thrown. - -By default, all users are loaded. If you want to filter users based on their attributes, use `user.filter`. `userGroupMember.filter` can be used if you want to load users based on their group membership. +By default, all enabled users are loaded (disabled accounts are automatically filtered out). If you want to further filter users based on their attributes, use `user.filter`. `userGroupMember.filter` 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 diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index dfcad06593..dec4532599 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -60,8 +60,9 @@ export interface Config { /** * The filter to apply to extract users. + * Combined with the base `accountEnabled eq true` filter. * - * E.g. "accountEnabled eq true and userType eq 'member'" + * E.g. "userType eq 'member'" */ userFilter?: string; /** @@ -162,8 +163,9 @@ export interface Config { expand?: string; /** * The filter to apply to extract users. + * Combined with the base `accountEnabled eq true` filter * - * E.g. "accountEnabled eq true and userType eq 'member'" + * E.g. "userType eq 'member'" */ filter?: string; /** @@ -295,8 +297,9 @@ export interface Config { expand?: string; /** * The filter to apply to extract users. + * Combined with the base `accountEnabled eq true` filter * - * E.g. "accountEnabled eq true and userType eq 'member'" + * E.g. "userType eq 'member'" */ filter?: string; /** 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 182c4842dd..6d9a6cc332 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -34,6 +34,7 @@ describe('readMicrosoftGraphConfig', () => { id: 'target', target: 'target', tenantId: 'tenantId', + userFilter: 'accountEnabled eq true', }, ]; expect(actual).toEqual(expected); @@ -50,7 +51,7 @@ describe('readMicrosoftGraphConfig', () => { clientSecret: 'clientSecret', authority: 'https://login.example.com/', userExpand: 'manager', - userFilter: 'accountEnabled eq true', + userFilter: "userType eq 'member'", userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], @@ -68,7 +69,7 @@ describe('readMicrosoftGraphConfig', () => { clientSecret: 'clientSecret', authority: 'https://login.example.com/', userExpand: 'manager', - userFilter: 'accountEnabled eq true', + userFilter: "accountEnabled eq true and (userType eq 'member')", userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], @@ -78,32 +79,6 @@ describe('readMicrosoftGraphConfig', () => { expect(actual).toEqual(expected); }); - it('should fail if both userFilter and userGroupMemberFilter are set', () => { - const config = { - providers: [ - { - tenantId: 'tenantId', - userFilter: 'accountEnabled eq true', - userGroupMemberFilter: 'any', - }, - ], - }; - expect(() => readMicrosoftGraphConfig(new ConfigReader(config))).toThrow(); - }); - - it('should fail if both userFilter and userGroupMemberSearch are set', () => { - const config = { - providers: [ - { - tenantId: 'tenantId', - userFilter: 'accountEnabled eq true', - userGroupMemberSearch: 'any', - }, - ], - }; - expect(() => readMicrosoftGraphConfig(new ConfigReader(config))).toThrow(); - }); - it('should fail if clientId is set without clientSecret', () => { const config = { providers: [ @@ -148,6 +123,7 @@ describe('readProviderConfigs', () => { id: 'customProviderId', target: 'https://graph.microsoft.com/v1.0', tenantId: 'tenantId', + userFilter: 'accountEnabled eq true', userPath: 'users', groupPath: 'groups', }, @@ -169,7 +145,7 @@ describe('readProviderConfigs', () => { queryMode: 'advanced', user: { expand: 'manager', - filter: 'accountEnabled eq true', + filter: "userType eq 'member'", select: ['id', 'displayName', 'department'], path: '/groups/{groupId}/members', }, @@ -202,7 +178,7 @@ describe('readProviderConfigs', () => { authority: 'https://login.example.com/', queryMode: 'advanced', userExpand: 'manager', - userFilter: 'accountEnabled eq true', + userFilter: "accountEnabled eq true and (userType eq 'member')", userSelect: ['id', 'displayName', 'department'], userPath: '/groups/{groupId}/members', groupExpand: 'member', @@ -221,7 +197,7 @@ describe('readProviderConfigs', () => { expect(actual).toEqual(expected); }); - it('should fail if both userFilter and userGroupMemberFilter are set', () => { + it('should combine custom filter with accountEnabled filter by default', () => { const config = { catalog: { providers: { @@ -229,38 +205,17 @@ describe('readProviderConfigs', () => { customProviderId: { tenantId: 'tenantId', user: { - filter: 'accountEnabled eq true', - }, - userGroupMember: { - filter: 'any', + filter: "userType eq 'member'", }, }, }, }, }, }; - expect(() => readProviderConfigs(new ConfigReader(config))).toThrow(); - }); - - it('should fail if both userFilter and userGroupMemberSearch are set', () => { - const config = { - catalog: { - providers: { - microsoftGraphOrg: { - customProviderId: { - tenantId: 'tenantId', - user: { - filter: 'accountEnabled eq true', - }, - userGroupMember: { - search: 'any', - }, - }, - }, - }, - }, - }; - expect(() => readProviderConfigs(new ConfigReader(config))).toThrow(); + const [result] = readProviderConfigs(new ConfigReader(config)); + expect(result.userFilter).toBe( + "accountEnabled eq true and (userType eq 'member')", + ); }); it('should fail if clientId is set without clientSecret', () => { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 5feabcb9b2..0eb939022e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -61,9 +61,10 @@ export type MicrosoftGraphProviderConfig = { */ clientSecret?: string; /** - * The filter to apply to extract users. + * The filter to apply to extract users. This is combined with the base + * `accountEnabled eq true` filter that is always applied automatically. * - * E.g. "accountEnabled eq true and userType eq 'member'" + * E.g. "userType eq 'member'" */ userFilter?: string; /** @@ -192,7 +193,9 @@ export function readMicrosoftGraphConfig( const clientSecret = providerConfig.getOptionalString('clientSecret'); const userExpand = providerConfig.getOptionalString('userExpand'); - const userFilter = providerConfig.getOptionalString('userFilter'); + const userFilter = buildUserFilter( + providerConfig.getOptionalString('userFilter'), + ); const userSelect = providerConfig.getOptionalStringArray('userSelect'); const userGroupMemberFilter = providerConfig.getOptionalString( 'userGroupMemberFilter', @@ -204,17 +207,6 @@ export function readMicrosoftGraphConfig( 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.`, - ); - } - const groupSelect = providerConfig.getOptionalStringArray('groupSelect'); const queryMode = providerConfig.getOptionalString('queryMode'); if ( @@ -312,7 +304,7 @@ export function readProviderConfig( const clientSecret = config.getOptionalString('clientSecret'); const userExpand = config.getOptionalString('user.expand'); - const userFilter = config.getOptionalString('user.filter'); + const userFilter = buildUserFilter(config.getOptionalString('user.filter')); const userSelect = config.getOptionalStringArray('user.select'); const userPath = config.getOptionalString('user.path') ?? 'users'; const loadUserPhotos = config.getOptionalBoolean('user.loadPhotos'); @@ -343,17 +335,6 @@ export function readProviderConfig( ); const userGroupMemberPath = config.getOptionalString('userGroupMember.path'); - 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.`, - ); - } - if (clientId && !clientSecret) { throw new Error(`clientSecret must be provided when clientId is defined.`); } @@ -393,3 +374,11 @@ export function readProviderConfig( schedule, }; } + +function buildUserFilter(rawFilter: string | undefined): string { + const base = 'accountEnabled eq true'; + if (rawFilter) { + return `${base} and (${rawFilter})`; + } + return base; +}