Log and surface the real reason LDAP group sync fails
What does this MR do and why?
When EE::Gitlab::Auth::Ldap::Sync::Group.execute/.execute_all_providers
finish an LDAP group sync, they call group.finish_ldap_sync to persist
the started -> ready state transition. If the group is invalid at that
exact moment — most commonly because a synced member's email violates the
group's "Restrict membership by email domain" setting — that save fails.
The ldap_sync_status state machine's after_transition callbacks only
run after a successful save (that's how the state_machines-activerecord
integration works), so when the save fails, the callback never runs at
all: nothing gets persisted, and the group is left stuck in the started
state forever, with no error anywhere — not in the logs, not in the UI,
not even queryable without a Rails console session.
This MR:
- Checks
group.finish_ldap_sync's return value at the call site and, on failure, routes through the existingmark_ldap_sync_as_failedmechanism (skip_validation: true, the same approach already used for the "stuck for over an hour" case) so the sync actually fails instead of silently hanging. - Surfaces the real, specific reason instead of the generic
"Members and requesters is invalid"association-validation rollup — which member, and why — by reading the already-loaded, already-invalidMember/Requesterrecords offgroup.members_and_requesters(the same objects an admin previously had to dig for viagroup.members.map(&:errors)in a Rails console) and including their own validation messages. Capped at 5 members per message to avoid an unbounded string if many fail at once. - Logs that detailed reason via
Gitlab::AppLogger.warn. - Surfaces the same
ldap_sync_errorto group admins as a danger alert on the group members page — the column already existed but nothing displayed it.
Closes #348226 (closed) and #504298 (closed), which are the same underlying bug reported from two different angles: #348226 (closed) is the original 2021 feature request to "send these errors to the logs and/or the UI", and #504298 (closed) is a 2024 customer-reported case of the sync getting silently stuck with zero diagnostics.
Reproduction
Before this fix, the following bin/rails runner script reproduces the
silent-stuck-sync bug described in both issues (a domain-restricted group
plus an LDAP-synced member whose email doesn't match the allowed domain):
group = Group.find_by(path: 'some-group')
FactoryBot.create(:allowed_email_domain, group: group, domain: 'allowed-domain.example')
group.start_ldap_sync
group.add_member(some_user_with_a_disallowed_email, Gitlab::Access::DEVELOPER, ldap: true)
result = group.finish_ldap_sync
puts result # => false
puts group.ldap_sync_status # => "started" (stuck, forever, no retry will fix it)
puts group.ldap_sync_error # => nil
# `group.errors.full_messages` is also unpopulated by the time anything
# outside this method could observe it -- the only way to see *why* was
# `group.members.map(&:errors).map(&:full_messages)` in a live console
# session, exactly as the original #348226 reporter had to do.After this fix, the same script gives:
puts result # => false (still, since save legitimately failed)
puts group.ldap_sync_status # => "failed" (no longer stuck)
puts group.ldap_sync_error
# => "user 'jdoe': The member's email address is not allowed for this
# group. Check with your administrator."and the same message is written to Gitlab::AppLogger and shown to group
admins on the group members page (see screenshot below).
Runnable reproduction script
Please see the script that sets up required data fixtures for GDK — to
minimize the verification time and effort. Run it with
bin/rails runner repro_ldap_sync_stuck.rb. It creates its own group,
domain restriction, and disallowed-email user from scratch, then drives
the sync through the real
EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers entry point
(only the LDAP network round trip is stubbed out; every other line —
start/finish transition, failure handling, the error message — runs
unmodified production code). Verified genuinely red on master (prints
STUCK) and green on this branch (prints FIXED with the real
per-member error message).
Screenshots or screen recordings
Reproduced end-to-end against a local GDK: a group with a
group_allowed_email_domains restriction, LDAP-synced against a member
whose email doesn't match, driven through the real
EE::Gitlab::Auth::Ldap::Sync::Group.execute code path (not stubbed).
Updated 2026-08-31 after the UX/TechWriting round (title + bulleted list + admin guidance):
How to set up and validate locally
bin/rspec ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb
bin/rspec ee/spec/views/groups/group_members/_ldap_sync.html.haml_spec.rbTwo new specs in group_spec.rb reproduce the bug end-to-end (not just
via mocks) using the real Members::CreatorService/domain-restriction
validation path and assert the sync fails with the specific member
message. I verified these are genuinely red without the fix (reverted the
ee/lib and .haml changes locally, confirmed 5/6 new examples failed
for the expected reason, then restored the fix and confirmed all green
again) before committing.
Manual verification on GDK (no real LDAP server needed — this drives the same state-machine/model methods the sync code itself calls, so it exercises the real banner rendering path):
Gitlab.config.ldap['enabled'] = true
group = Group.create!(
name: 'ldap-sync-error-demo', path: 'ldap-sync-error-demo',
organization: Organizations::Organization.first,
visibility_level: Gitlab::VisibilityLevel::PRIVATE
)
group.ldap_group_links.create!(cn: 'developers', group_access: Gitlab::Access::DEVELOPER, provider: 'ldapmain')
group.start_ldap_sync
group.mark_ldap_sync_as_failed(
"user 'alice': Email address is not allowed for this group. Check with your administrator.; " \
"user 'bob': Email address is not allowed for this group. Check with your administrator.",
skip_validation: true
)Then visit that group's /-/group_members page as an owner/admin to see
the new title + bulleted banner. Output captured on this branch on a
local GDK:
ldap_synced?: true
ldap_sync_failed?: true
ldap_sync_error: user 'alice': Email address is not allowed for this group. Check with your administrator.; user 'bob': Email address is not allowed for this group. Check with your administrator.Disclosure
Root cause investigation, implementation, and the reproduction script
above were AI-assisted (Claude Code). All claims about the failure
mechanism (the after_transition callback timing relative to a failed
save) were verified empirically against this GDK checkout, not taken on
faith — see the reproduction section. Test coverage was written first,
confirmed red against the pre-fix code, then confirmed green after the
fix, using the real domain-restriction validation path rather than pure
mocks.
