feat(catalog-backend-module-msgraph): filter out disabled users by default (#34165)

* 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) <noreply@anthropic.com>
Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>

* chore: mark msgraph disabled-user filtering as breaking change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>

* docs(catalog-backend-module-msgraph): clarify automatic accountEnabled filter in docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>

---------

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
MT Lewis
2026-05-19 09:30:30 +01:00
committed by GitHub
parent a29edc58fb
commit 2bd0450cb4
7 changed files with 51 additions and 98 deletions
@@ -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.
+6 -4
View File
@@ -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.
@@ -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:
@@ -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
+6 -3
View File
@@ -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;
/**
@@ -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', () => {
@@ -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;
}