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:
@@ -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.
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user