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

How to set up and validate locally

Setup

  1. 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
  2. Restart GDK

  3. 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)
  4. Verify the user is a member:

    group.member?(user) # => true

Scenario 1: Feature flag enabled — membership preserved on LDAP error

  1. Enable the feature flag:

    Feature.enable(:ldap_raise_on_search_error)
  2. Monkeypatch Net::LDAP to 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
  3. Run the sync:

    EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group)
  4. 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

  1. Start a fresh Rails console and disable the feature flag:

    Feature.disable(:ldap_raise_on_search_error)
  2. Reset the test group's LDAP sync status

    group.update!(ldap_sync_status: 'ready') 
  3. 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.

Edited by Paulo Barros

Merge request reports

Loading