Fix LDAP sync member removal on server errors
Resolves Introduce additional connection logic to LDAP s... (#6054).
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, operations error), Adapter#check_empty_response_code only logged a warning and returned an empty result set. The sync interpreted this as "the LDAP group has zero members" and removed all existing group members.
This MR adds a raise_on_error keyword to ldap_search (defaults to false) and opts in the EE group sync methods (groups, group_members_in_range, nested_groups, filter_search). When raise_on_error: true and the LDAP server returns a non-zero response code, check_empty_response_code raises Net::LDAP::Error, triggering the existing retry logic and eventually LdapConnectionError. The sync then correctly marks the group as failed and preserves existing membership. All other callers retain the previous behavior (log a warning and return []).
Additionally, an audit event (ldap_group_sync_failed) is emitted on sync failure so admins can see failures in group Audit Events without digging through application logs.
References
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 # or reuse existing group with Group.find user = User.last # or reuse existing user with User.find 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: LDAP error response — membership should be preserved
- 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: Successful sync — sanity check
- Start a fresh Rails console (to clear the monkeypatch), then set up the mock proxy:
group = Group.last # or reuse existing group with Group.find group.update_column(:ldap_sync_status, 'ready') user = User.last # or reuse existing user with User.find $test_user = user mock_adapter_class = Class.new do def config mock_config = Object.new def mock_config.group_base; 'ou=groups,dc=example,dc=com'; end mock_config end end $mock_adapter_class = mock_adapter_class mock_proxy_class = Class.new do attr_reader :provider, :adapter def initialize(provider) @provider = provider @adapter = $mock_adapter_class.new end def dns_for_group_cn(cn) [$test_user.identities.find_by(provider: 'ldapmain').extern_uid] end end EE::Gitlab::Auth::Ldap::Sync::Proxy.define_singleton_method(:open) do |provider, &block| block.call(mock_proxy_class.new(provider)) end - Run the sync:
EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group) - Verify that the sync succeeds and the user remains a member:
group.reload group.ldap_sync_status # => "ready" group.member?(user) # => true
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.