Fix LDAP sync member removal on server errors
Resolves #6054.
This is a follow-up to Fix LDAP sync member removal on server errors (!224135 - merged) after it being reverted. More details here.
What does this MR do and why?
When LDAP group sync runs and the LDAP server returns a non-zero error response code (e.g., bind account locked, invalid credentials), Adapter#check_empty_response_code only logs a warning and returns an empty result set. The sync interprets this as "the LDAP group has zero members" and removes all existing group members.
This MR gates a fix behind an ops feature flag (ldap_raise_on_search_error, disabled by default). When enabled, non-zero response codes not in NON_ERROR_LDAP_RESPONSE_CODES (0, 3, 4, 10, 32) raise Net::LDAP::Error, triggering the existing retry logic. The sync marks the group as failed and preserves existing membership. An audit event (ldap_group_sync_failed) is also emitted.
References
- Reverted MR: !224135 (merged)
- Revert MR: !226910 (merged)
How to set up and validate locally
Setup
-
Enable LDAP in
config/gitlab.yml:ldap: enabled: true prevent_ldap_sign_in: false servers: main: label: "LDAP" host: "127.0.0.1" port: 3890 uid: "uid" encryption: "plain" base: "dc=example,dc=com" user_filter: "" group_base: "ou=groups,dc=example,dc=com" admin_group: "" active_directory: false -
Restart GDK
-
Create test data in the Rails console:
group = Group.last user = User.last extern_uid = "uid=#{user.username.downcase},ou=people,dc=example,dc=com" Identity.find_or_create_by!(user: user, provider: 'ldapmain', extern_uid: extern_uid) LdapGroupLink.find_or_create_by!(group: group, cn: 'developers', group_access: Gitlab::Access::DEVELOPER, provider: 'ldapmain') group.add_member(user, Gitlab::Access::DEVELOPER, ldap: true) -
Verify the user is a member:
group.member?(user) # => true
Scenario 1: Feature flag enabled — membership preserved on LDAP error
-
Enable the feature flag:
Feature.enable(:ldap_raise_on_search_error) -
Monkeypatch
Net::LDAPto simulate a bind failure (response code 49):mock_result = Struct.new(:code, :message).new(49, 'Invalid credentials') fake_ldap = Object.new fake_ldap.define_singleton_method(:search) { |*args| nil } fake_ldap.define_singleton_method(:get_operation_result) { mock_result } fake_ldap.define_singleton_method(:auth) { |*args| nil } fake_ldap.define_singleton_method(:encryption) { |*args| nil } Net::LDAP.define_singleton_method(:open) do |*args, &block| block.call(fake_ldap) end Net::LDAP.define_singleton_method(:new) do |*args| fake_ldap end -
Run the sync:
EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group) -
Verify that the sync fails and the user is still a member:
group.reload group.ldap_sync_status # => "failed" group.member?(user) # => true AuditEvent.where(entity_id: group.id, entity_type: 'Group').last&.details # => {:event_name=>"ldap_group_sync_failed", ...}
Scenario 2: Feature flag disabled (default) — original behavior
-
Start a fresh Rails console and disable the feature flag:
Feature.disable(:ldap_raise_on_search_error) -
Reset the test group's LDAP sync status
group.update!(ldap_sync_status: 'ready') -
Apply the same monkeypatch as above, then run the sync. Members will be removed as before (original behavior).
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.