Add audit event when a user is blocked by LDAP sync

Summary

When LDAP user sync determines a user should be blocked (because they no longer exist in LDAP or are disabled in Active Directory), no audit event is recorded. Customers need user_blocked events traceable to LDAP sync in their audit logs.

This issue is scoped specifically to LDAP user sync blocking. The parent issue #3592 covers broader LDAP audit event gaps including group sync changes.

Related: #3592

Problem

The LDAP blocking logic lives in lib/gitlab/auth/ldap/access.rb in the block_user method. It calls user.ldap_block and logs to Gitlab::AppLogger, but does not create an audit event. This means:

  • Admins cannot see in the Audit Log when or why a user was blocked by LDAP sync
  • There is no audit trail distinguishing LDAP-initiated blocks from admin-initiated blocks
  • Customers relying on audit events for compliance have a gap in their records

The existing user_blocked audit event (defined in ee/config/audit_events/types/user_blocked.yml) is only triggered via Users::BlockService (EE override in ee/app/services/ee/users/block_service.rb), which is used for admin-initiated blocks. The LDAP sync path bypasses this service entirely.

Blocking scenarios to audit

The block_user method in lib/gitlab/auth/ldap/access.rb is called in three cases within the allowed? method:

  1. User no longer exists in LDAP - reason: "does not exist anymore"
  2. User is disabled in Active Directory - reason: "is disabled in Active Directory"
  3. User has no LDAP provider (via Users::RepairLdapBlockedService) - reason: "Account is not provided by LDAP"

Proposed implementation

1. Create a new audit event type definition

Create ee/config/audit_events/types/user_blocked_by_ldap_sync.yml:

---
name: user_blocked_by_ldap_sync
description: A user is blocked by LDAP synchronization
introduced_by_issue: <this issue URL>
introduced_by_mr: <MR URL>
feature_category: system_access
milestone: 'TBD'
saved_to_database: true
streamed: true
scope: [User]

2. Add a hook method in CE lib/gitlab/auth/ldap/access.rb

Add a no-op log_audit_event_for_block(user, reason) method in the CE block_user method, called after user.ldap_block:

def block_user(user, reason)
  user.ldap_block

  # existing logging...

  log_audit_event_for_block(user, reason)
end

def log_audit_event_for_block(user, reason)
  # no-op in CE, overridden in EE
end

3. Override in EE ee/lib/ee/gitlab/auth/ldap/access.rb

Add the audit event call using the established pattern:

override :log_audit_event_for_block
def log_audit_event_for_block(user, reason)
  return unless user.ldap_blocked?

  message = if provider
              "Blocked user (LDAP account \"#{ldap_identity.extern_uid}\" #{reason})"
            else
              "Blocked user (account is not provided by LDAP)"
            end

  ::Gitlab::Audit::Auditor.audit({
    name: 'user_blocked_by_ldap_sync',
    message: message,
    author: ::Gitlab::Audit::UnauthenticatedAuthor.new(name: '(LDAP Sync)'),
    scope: user,
    target: user,
    target_details: user.username,
    additional_details: {
      system_event: true,
      reason: reason
    }
  })
end

Key design decisions:

  • Uses UnauthenticatedAuthor with name '(LDAP Sync)' since this is a system process with no authenticated user (same pattern as SCIM uses '(System)' in ee/app/services/ee/members/destroy_service.rb)
  • Uses system_event: true in additional_details to mark it as a system-initiated event
  • Includes the reason in additional_details for programmatic access
  • Guard return unless user.ldap_blocked? ensures the audit event is only logged when the state transition actually succeeded (e.g., not on read-only instances)
  • Creates a new event type user_blocked_by_ldap_sync rather than reusing user_blocked, so customers can filter specifically for LDAP-initiated blocks

4. Add specs in ee/spec/lib/gitlab/auth/ldap/access_spec.rb

Test cases needed:

  • User blocked because they no longer exist in LDAP → audit event created with correct message
  • User blocked because disabled in Active Directory → audit event created with correct message
  • User not blocked (still exists and active) → no audit event
  • Not licensed (admin_audit_log: false, audit_events: false, extended_audit_events: false) → no audit event
  • Read-only instance → no audit event (because ldap_block is a no-op, so user.ldap_blocked? returns false)

Files to modify

File Change
ee/config/audit_events/types/user_blocked_by_ldap_sync.yml New audit event type definition
lib/gitlab/auth/ldap/access.rb Add log_audit_event_for_block no-op hook in block_user
ee/lib/ee/gitlab/auth/ldap/access.rb Override log_audit_event_for_block with audit event logic
ee/spec/lib/gitlab/auth/ldap/access_spec.rb Add audit event test cases
spec/lib/gitlab/auth/ldap/access_spec.rb Verify no-op hook doesn't break CE behavior
Edited by 🤖 GitLab Bot 🤖