Skip to content

LDAP: remove users deleted in LDAP during full sync - #55026

Open
eastana wants to merge 2 commits into
keycloak:mainfrom
eastana:ldap-remove-deleted-users-on-sync
Open

eastana wants to merge 2 commits into
keycloak:mainfrom
eastana:ldap-remove-deleted-users-on-sync

Conversation

@eastana

@eastana eastana commented Nov 11, 2026

Copy link
Copy Markdown

Adds 'removeInvalidUsersOnSync' option (default off) to the LDAP provider. Full sync collects LDAP_IDs returned by LDAP and removes local users linked to the provider whose LDAP_ID is missing. Removal is skipped if LDAP returns no users at all.

Closes #35235

Adds 'removeInvalidUsersOnSync' option (default off) to the LDAP provider.
Full sync collects LDAP_IDs returned by LDAP and removes local users linked
to the provider whose LDAP_ID is missing. Removal is skipped if LDAP returns
no users at all.

Closes keycloak#35235

Signed-off-by: Ibragim <57891324+eastana@users.noreply.github.com>
@eastana
eastana requested review from a team as code owners November 11, 2026 08:21
Copilot AI balanced review requested due to automatic review settings November 11, 2026 08:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

LDAP cleanup can delete valid users after incomplete queries or concurrent account changes, leaving critical data-loss risks unresolved.

6 open issues
What changed in this PR

Adds opt-in removal of stale LDAP-imported users during full synchronization, addressing #35235. The option defaults to off and skips removal when LDAP returns no users.

Changes:

  • Collects LDAP IDs during full sync and removes linked local users absent from the results.
  • Adds a paginated local-storage API for reading imported user IDs and attributes without LDAP validation.
  • Exposes the option in the Admin Console and adds an integration test.
File Description
testsuite/​integration-arquillian/​tests/​base/​src/​test/​java/​org/​keycloak/​testsuite/​federation/​ldap/​LDAPSyncTest.java Tests cleanup with the option disabled and enabled.
server-spi/​src/​main/​java/​org/​keycloak/​models/​UserProvider.java Defines the imported-user attribute query API.
server-spi-private/​src/​main/​java/​org/​keycloak/​models/​LDAPConstants.java Defines the new configuration key.
model/​storage-private/​src/​main/​java/​org/​keycloak/​storage/​UserStorageManager.java Delegates the query to local storage.
model/​jpa/​src/​main/​java/​org/​keycloak/​models/​jpa/​JpaUserProvider.java Implements the paginated database query.
js/​apps/​admin-ui/​src/​user-federation/​ldap/​LdapSettingsSynchronization.tsx Adds the cleanup toggle, disabled when imports are off.
js/​apps/​admin-ui/​maven-resources/​theme/​keycloak.v2/​admin/​messages/​messages_en.properties Adds the label and explanatory warnings.
federation/​ldap/​src/​main/​java/​org/​keycloak/​storage/​ldap/​LDAPStorageProviderFactory.java Collects LDAP IDs and performs local-user cleanup.
federation/​ldap/​src/​main/​java/​org/​keycloak/​storage/​ldap/​LDAPConfig.java Reads the cleanup option.

Review effort: Balanced

Comment on lines +652 to +653
if (removeInvalidUsers) {
removeUsersNotFoundInLdap(sessionFactory, realmId, model, ldapIds, syncResult);
- Skip removal when the LDAP scan may be incomplete (paged results
  response control missing, entries without UUID)
- Only consider users snapshotted before the LDAP scan
- Re-check federation link and LDAP_ID before removing a user
- Publish UserPreRemovedEvent before removal
- Support LDAP_ID values stored in longValue
- Persist the option in the test, as the sync task reloads the model

Signed-off-by: Ibragim <57891324+eastana@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 11, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Incomplete LDAP scans and a non-atomic ownership re-check can wrongly delete imported users still present in LDAP or users concurrently unlinked from LDAP.

5 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Increment the removal count only after transaction commit

federation/​ldap/​src/​main/​java/​org/​keycloak/​storage/​ldap/​LDAPStorageProviderFactory.java:880

The removed count is incremented before runJobInTransaction() commits, so a commit failure caught below reports the user as both removed and failed even though deletion rolled back. Return the deletion result from runJobInTransactionWithResult() and increment the counter only after the transaction completes successfully.

5 resolved since last review

Review effort: Balanced

Comment on lines +741 to +743
if (scan != null) {
scan.collect(users);
}
return; // already removed concurrently
}
// re-check the user was not unlinked or re-linked to a different LDAP entry since the snapshot
if (!model.getId().equals(user.getFederationLink()) || !ldapId.equals(user.getFirstAttribute(LDAPConstants.LDAP_ID))) {
* See KEYCLOAK-19036
*/
identityQuery.getPaginationContext().setCookie(null);
identityQuery.getPaginationContext().markIncomplete();
Comment on lines +833 to +836
if (!scan.isComplete()) {
logger.warnf("LDAP user query for federation provider '%s' in realm '%s' may have returned incomplete results. Skipping removal of invalid users to prevent accidental data loss.",
model.getName(), realmId);
return;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Managing User Deletion and LDAP Synchronization in Keycloak

2 participants