Summary
When "User creation restriction" or "Group Object ID" is configured in the Microsoft 365 Integration sync settings, the plugin still fetches all users from the Azure AD tenant via the Graph API /users endpoint. Filtering is applied locally after all users have been downloaded. In large multi-company tenants (200K+ users), this causes severe performance issues, memory exhaustion, and API throttling — even when the admin only needs to sync a small subset of users.
Request: Apply the configured restrictions at the Microsoft Graph API level ($filter parameter and/or group members endpoint) during the fetch stage, so only relevant users are ever downloaded.
Current Behavior
Admin configuration:
"User creation restriction" is set to filter by companyName for specific companies
"Group Object ID" is configured with a security group containing only target users.
Expected Behavior
- "User creation restriction" (plain text) should generate a $filter query
When the admin sets a plain-text restriction like Company A on field companyName, the plugin should call:
GET /v1.0/users?$filter=companyName eq 'Company A'&$select=...&$top=999
For comma-separated values like Company A, Company B, Company C:
GET /v1.0/users?$filter=companyName eq 'Company A' or companyName eq 'Company B' or companyName eq 'Company C'&$select=...&$top=999
This way the Graph API returns only matching users — no local filtering needed.
- "Group Object ID" should use the group members endpoint
When a Group Object ID is configured, the plugin should call:
GET /v1.0/groups/{group-id}/members/microsoft.graph.user?$select=...&$top=999
Instead of:
GET /v1.0/users?$select=...&$top=999
This fetches only group members directly, not the entire tenant.
- Regex restrictions should use a hybrid approach
When "Value is a regular expression" is enabled, the plugin should attempt to extract literal components from the regex pattern and build a partial $filter to reduce the dataset, then apply the full regex locally on the smaller result set.
For example, /^Company (A|B|C)$/ on companyName could generate startsWith(companyName, 'Company') as a partial server-side filter, then verify the full regex match locally.
If no filterable component can be extracted, fall back to current behavior but log a performance warning.
- Combined filters should work together
When both Group Object ID and User creation restriction are set, the plugin should fetch from the group members endpoint AND apply the $filter:
GET /v1.0/groups/{group-id}/members/microsoft.graph.user?$filter=companyName eq 'Company A'&$select=...
Impact
This issue affects any organization that:
Has a large Azure AD tenant with multiple companies/divisions
Needs to sync only a subset of users to Moodle
Experiences sync timeouts, memory errors, or slow cron execution
The performance improvement would be proportional to the filtering ratio. In our case: ~93% reduction in API calls, data transfer, and sync time (214K → 15K users).
Error handling
All filter-related errors should fall back gracefully to current behavior (fetch all + local filter). The sync process should never fail completely due to a filter issue.
Delta sync
The same filtering should apply to delta queries. If filter settings change between syncs, the delta token should be invalidated and a fresh filtered full sync should run.
Backward compatibility
When no restrictions or group IDs are configured, behavior remains identical to the current version.
Workarounds Attempted
Workaround Result
Set "User creation restriction" to company names Users still fetched at API level — filter only applied locally after download
Set "Group Object ID" with a security group All 214K users still fetched — group membership only checked after download
Dynamic security group in Azure AD Group works correctly in Azure, but plugin ignores it at fetch stage
Additional Context
Microsoft Graph API fully supports $filter on the /users endpoint for properties like companyName, department, mail, userPrincipalName, and others
The /groups/{id}/members endpoint is the standard way to fetch group members without querying the full directory
Both endpoints support pagination via @odata.nextLink, which preserves query parameters including $filter
Delta queries (/users/delta) also support $filter on select properties
I am happy to contribute a pull request for this if the maintainers agree with the approach.
Summary
When "User creation restriction" or "Group Object ID" is configured in the Microsoft 365 Integration sync settings, the plugin still fetches all users from the Azure AD tenant via the Graph API /users endpoint. Filtering is applied locally after all users have been downloaded. In large multi-company tenants (200K+ users), this causes severe performance issues, memory exhaustion, and API throttling — even when the admin only needs to sync a small subset of users.
Request: Apply the configured restrictions at the Microsoft Graph API level ($filter parameter and/or group members endpoint) during the fetch stage, so only relevant users are ever downloaded.
Current Behavior
Admin configuration:
"User creation restriction" is set to filter by companyName for specific companies
"Group Object ID" is configured with a security group containing only target users.
Expected Behavior
When the admin sets a plain-text restriction like Company A on field companyName, the plugin should call:
GET /v1.0/users?$filter=companyName eq 'Company A'&$select=...&$top=999
For comma-separated values like Company A, Company B, Company C:
GET /v1.0/users?$filter=companyName eq 'Company A' or companyName eq 'Company B' or companyName eq 'Company C'&$select=...&$top=999
This way the Graph API returns only matching users — no local filtering needed.
When a Group Object ID is configured, the plugin should call:
GET /v1.0/groups/{group-id}/members/microsoft.graph.user?$select=...&$top=999
Instead of:
GET /v1.0/users?$select=...&$top=999
This fetches only group members directly, not the entire tenant.
When "Value is a regular expression" is enabled, the plugin should attempt to extract literal components from the regex pattern and build a partial $filter to reduce the dataset, then apply the full regex locally on the smaller result set.
For example, /^Company (A|B|C)$/ on companyName could generate startsWith(companyName, 'Company') as a partial server-side filter, then verify the full regex match locally.
If no filterable component can be extracted, fall back to current behavior but log a performance warning.
When both Group Object ID and User creation restriction are set, the plugin should fetch from the group members endpoint AND apply the $filter:
GET /v1.0/groups/{group-id}/members/microsoft.graph.user?$filter=companyName eq 'Company A'&$select=...
Impact
This issue affects any organization that:
Has a large Azure AD tenant with multiple companies/divisions
Needs to sync only a subset of users to Moodle
Experiences sync timeouts, memory errors, or slow cron execution
The performance improvement would be proportional to the filtering ratio. In our case: ~93% reduction in API calls, data transfer, and sync time (214K → 15K users).
Error handling
All filter-related errors should fall back gracefully to current behavior (fetch all + local filter). The sync process should never fail completely due to a filter issue.
Delta sync
The same filtering should apply to delta queries. If filter settings change between syncs, the delta token should be invalidated and a fresh filtered full sync should run.
Backward compatibility
When no restrictions or group IDs are configured, behavior remains identical to the current version.
Workarounds Attempted
Workaround Result
Set "User creation restriction" to company names Users still fetched at API level — filter only applied locally after download
Set "Group Object ID" with a security group All 214K users still fetched — group membership only checked after download
Dynamic security group in Azure AD Group works correctly in Azure, but plugin ignores it at fetch stage
Additional Context
Microsoft Graph API fully supports $filter on the /users endpoint for properties like companyName, department, mail, userPrincipalName, and others
The /groups/{id}/members endpoint is the standard way to fetch group members without querying the full directory
Both endpoints support pagination via @odata.nextLink, which preserves query parameters including $filter
Delta queries (/users/delta) also support $filter on select properties
I am happy to contribute a pull request for this if the maintainers agree with the approach.