google-site-verification: google61fe8ba583a51912.html
User Synchronization with LDAP in Java: Complete Implementation Guide

User synchronization with LDAP/Active Directory is essential for maintaining consistent user data across enterprise applications. This guide provides a comprehensive implementation for bidirectional user synchronization.


LDAP Synchronization Overview

Synchronization Types:

  • Bidirectional Sync: Changes in LDAP and application sync both ways
  • Unidirectional Sync (LDAP → App): LDAP is source of truth
  • Unidirectional Sync (App → LDAP): Application is source of truth

Key Features:

  • User provisioning and deprovisioning
  • Attribute synchronization
  • Group membership synchronization
  • Conflict resolution
  • Incremental synchronization

Dependencies and Setup

Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <spring-ldap.version>3.1.0</spring-ldap.version> <lombok.version>1.18.28</lombok.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- LDAP --> <dependency> <groupId>org.springframework.ldap</groupId> <artifactId>spring-ldap-core</artifactId> <version>${spring-ldap.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-ldap</artifactId> </dependency> <!-- Database --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <!-- Utilities --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <scope>test</scope> </dependency> </dependencies>
Application Configuration
# application.yml app: ldap: sync: enabled: true # Sync direction: LDAP_TO_APP, APP_TO_LDAP, BIDIRECTIONAL direction: LDAP_TO_APP # Cron expression for scheduled sync schedule: "0 0 2 * * ?" # 2 AM daily # Batch size for processing batch-size: 100 # Conflict resolution strategy: LDAP_WINS, APP_WINS, MANUAL conflict-resolution: LDAP_WINS spring: datasource: url: jdbc:postgresql://localhost:5432/user_sync username: postgres password: password driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true ldap: urls: ldap://localhost:389 base: dc=example,dc=com username: cn=admin,dc=example,dc=com password: admin123 # Connection pool settings pool: enabled: true max-active: 10 max-idle: 5 min-idle: 2 max-wait: 30000 # Custom LDAP configuration ldap: connection: timeout: 5000 retry-attempts: 3 search: user-base: ou=users user-filter: "(objectClass=inetOrgPerson)" group-base: ou=groups group-filter: "(objectClass=groupOfNames)" attributes: user: id: uid username: cn email: mail firstName: givenName lastName: sn displayName: displayName telephone: telephoneNumber employeeNumber: employeeNumber department: department title: title group: name: cn description: description members: member logging: level: com.example.ldapsync: DEBUG org.springframework.ldap: INFO

Domain Models

1. User Entities
// AppUser.java - Application user entity package com.example.ldapsync.model; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.persistence.*; import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "app_users") @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class AppUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @EqualsAndHashCode.Include @Column(name = "external_id", unique = true, nullable = false) private String externalId; // LDAP DN or GUID @Column(name = "username", unique = true, nullable = false) private String username; @Column(name = "email", nullable = false) private String email; @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; @Column(name = "display_name") private String displayName; @Column(name = "phone") private String phone; @Column(name = "employee_number") private String employeeNumber; @Column(name = "department") private String department; @Column(name = "title") private String title; @Column(name = "is_active", nullable = false) private Boolean isActive = true; @Column(name = "is_synchronized", nullable = false) private Boolean isSynchronized = false; @Column(name = "last_sync_date") private LocalDateTime lastSyncDate; @Column(name = "created_date", nullable = false) private LocalDateTime createdDate; @Column(name = "modified_date") private LocalDateTime modifiedDate; @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "user_groups", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "group_id") ) private Set<UserGroup> groups = new HashSet<>(); @Version private Long version; @PrePersist protected void onCreate() { createdDate = LocalDateTime.now(); modifiedDate = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { modifiedDate = LocalDateTime.now(); } } // LdapUser.java - LDAP user representation package com.example.ldapsync.model; import lombok.Data; import org.springframework.ldap.odm.annotations.*; import javax.naming.Name; import java.util.Set; @Data @Entry( base = "ou=users", objectClasses = {"inetOrgPerson", "organizationalPerson", "person", "top"} ) public class LdapUser { @Id private Name dn; @Attribute(name = "uid") @DnAttribute(value = "uid", index = 1) private String uid; @Attribute(name = "cn") private String username; @Attribute(name = "mail") private String email; @Attribute(name = "givenName") private String firstName; @Attribute(name = "sn") private String lastName; @Attribute(name = "displayName") private String displayName; @Attribute(name = "telephoneNumber") private String telephoneNumber; @Attribute(name = "employeeNumber") private String employeeNumber; @Attribute(name = "department") private String department; @Attribute(name = "title") private String title; @Attribute(name = "userPassword") private String password; @Attribute(name = "memberOf") private Set<String> memberOf; @Transient private boolean isActive = true; public String getDistinguishedName() { return dn != null ? dn.toString() : null; } }
2. Group Entities
// UserGroup.java - Application group entity package com.example.ldapsync.model; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.persistence.*; import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "user_groups") @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class UserGroup { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @EqualsAndHashCode.Include @Column(name = "external_id", unique = true, nullable = false) private String externalId; @Column(name = "name", nullable = false) private String name; @Column(name = "description") private String description; @Column(name = "is_active", nullable = false) private Boolean isActive = true; @Column(name = "is_synchronized", nullable = false) private Boolean isSynchronized = false; @Column(name = "last_sync_date") private LocalDateTime lastSyncDate; @Column(name = "created_date", nullable = false) private LocalDateTime createdDate; @ManyToMany(mappedBy = "groups", fetch = FetchType.LAZY) private Set<AppUser> members = new HashSet<>(); @PrePersist protected void onCreate() { createdDate = LocalDateTime.now(); } } // LdapGroup.java - LDAP group representation package com.example.ldapsync.model; import lombok.Data; import org.springframework.ldap.odm.annotations.*; import javax.naming.Name; import java.util.Set; @Data @Entry( base = "ou=groups", objectClasses = {"groupOfNames", "top"} ) public class LdapGroup { @Id private Name dn; @Attribute(name = "cn") @DnAttribute(value = "cn", index = 1) private String name; @Attribute(name = "description") private String description; @Attribute(name = "member") private Set<String> members; public String getDistinguishedName() { return dn != null ? dn.toString() : null; } }
3. Synchronization Models
// SyncConfig.java package com.example.ldapsync.model; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Map; @Data @Component @ConfigurationProperties(prefix = "app.ldap.sync") public class SyncConfig { private boolean enabled = true; private SyncDirection direction = SyncDirection.LDAP_TO_APP; private String schedule = "0 0 2 * * ?"; private int batchSize = 100; private ConflictResolution conflictResolution = ConflictResolution.LDAP_WINS; private Map<String, String> attributeMappings; public enum SyncDirection { LDAP_TO_APP, APP_TO_LDAP, BIDIRECTIONAL } public enum ConflictResolution { LDAP_WINS, APP_WINS, MANUAL } } // SyncResult.java package com.example.ldapsync.model; import lombok.Data; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Data public class SyncResult { private LocalDateTime startTime; private LocalDateTime endTime; private SyncType syncType; private boolean success; private String message; private SyncStatistics statistics = new SyncStatistics(); private List<SyncError> errors = new ArrayList<>(); public SyncResult(SyncType syncType) { this.syncType = syncType; this.startTime = LocalDateTime.now(); } public void complete(boolean success, String message) { this.success = success; this.message = message; this.endTime = LocalDateTime.now(); } public void addError(String entityType, String identifier, String error) { this.errors.add(new SyncError(entityType, identifier, error)); } public long getDuration() { if (startTime != null && endTime != null) { return java.time.Duration.between(startTime, endTime).toMillis(); } return 0; } public enum SyncType { FULL_SYNC, INCREMENTAL_SYNC, USER_SYNC, GROUP_SYNC } @Data public static class SyncStatistics { private int usersCreated = 0; private int usersUpdated = 0; private int usersDeleted = 0; private int usersSkipped = 0; private int groupsCreated = 0; private int groupsUpdated = 0; private int groupsDeleted = 0; private int groupsSkipped = 0; private int conflictsResolved = 0; public int getTotalUsersProcessed() { return usersCreated + usersUpdated + usersDeleted + usersSkipped; } public int getTotalGroupsProcessed() { return groupsCreated + groupsUpdated + groupsDeleted + groupsSkipped; } } @Data public static class SyncError { private final String entityType; private final String identifier; private final String error; private final LocalDateTime timestamp = LocalDateTime.now(); public SyncError(String entityType, String identifier, String error) { this.entityType = entityType; this.identifier = identifier; this.error = error; } } }

LDAP Configuration and Services

1. LDAP Configuration
// LdapConfig.java package com.example.ldapsync.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy; @Configuration public class LdapConfig { @Value("${spring.ldap.urls}") private String ldapUrl; @Value("${spring.ldap.base}") private String ldapBase; @Value("${spring.ldap.username}") private String ldapUsername; @Value("${spring.ldap.password}") private String ldapPassword; @Value("${ldap.connection.timeout:5000}") private int timeout; @Bean public ContextSource contextSource() { LdapContextSource contextSource = new LdapContextSource(); contextSource.setUrl(ldapUrl); contextSource.setBase(ldapBase); contextSource.setUserDn(ldapUsername); contextSource.setPassword(ldapPassword); contextSource.setPooled(true); // Connection settings contextSource.setBaseEnvironmentProperties(Map.of( "com.sun.jndi.ldap.connect.timeout", String.valueOf(timeout), "com.sun.jndi.ldap.read.timeout", String.valueOf(timeout) )); contextSource.afterPropertiesSet(); return new TransactionAwareContextSourceProxy(contextSource); } @Bean public LdapTemplate ldapTemplate(ContextSource contextSource) { LdapTemplate ldapTemplate = new LdapTemplate(contextSource); ldapTemplate.setIgnorePartialResultException(true); ldapTemplate.setIgnoreNameNotFoundException(true); return ldapTemplate; } } // LdapProperties.java package com.example.ldapsync.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Map; @Data @Component @ConfigurationProperties(prefix = "ldap") public class LdapProperties { private Connection connection = new Connection(); private Search search = new Search(); private Attributes attributes = new Attributes(); @Data public static class Connection { private int timeout = 5000; private int retryAttempts = 3; } @Data public static class Search { private String userBase = "ou=users"; private String userFilter = "(objectClass=inetOrgPerson)"; private String groupBase = "ou=groups"; private String groupFilter = "(objectClass=groupOfNames)"; private int pageSize = 1000; } @Data public static class Attributes { private User user = new User(); private Group group = new Group(); @Data public static class User { private String id = "uid"; private String username = "cn"; private String email = "mail"; private String firstName = "givenName"; private String lastName = "sn"; private String displayName = "displayName"; private String telephone = "telephoneNumber"; private String employeeNumber = "employeeNumber"; private String department = "department"; private String title = "title"; } @Data public static class Group { private String name = "cn"; private String description = "description"; private String members = "member"; } } }
2. LDAP Service
// LdapService.java package com.example.ldapsync.service; import com.example.ldapsync.config.LdapProperties; import com.example.ldapsync.model.LdapGroup; import com.example.ldapsync.model.LdapUser; import lombok.extern.slf4j.Slf4j; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.query.LdapQuery; import org.springframework.ldap.query.SearchScope; import org.springframework.ldap.support.LdapUtils; import org.springframework.stereotype.Service; import javax.naming.NamingException; import javax.naming.directory.Attributes; import javax.naming.directory.ModificationItem; import java.util.List; import java.util.Optional; import static org.springframework.ldap.query.LdapQueryBuilder.query; @Slf4j @Service public class LdapService { private final LdapTemplate ldapTemplate; private final LdapProperties ldapProperties; public LdapService(LdapTemplate ldapTemplate, LdapProperties ldapProperties) { this.ldapTemplate = ldapTemplate; this.ldapProperties = ldapProperties; } /** * Find all LDAP users */ public List<LdapUser> findAllUsers() { LdapQuery query = query() .base(ldapProperties.getSearch().getUserBase()) .searchScope(SearchScope.SUBTREE) .filter(ldapProperties.getSearch().getUserFilter()); log.debug("Searching for LDAP users with query: {}", query); return ldapTemplate.find(query, LdapUser.class); } /** * Find LDAP user by username */ public Optional<LdapUser> findUserByUsername(String username) { LdapQuery query = query() .base(ldapProperties.getSearch().getUserBase()) .searchScope(SearchScope.SUBTREE) .where(ldapProperties.getAttributes().getUser().getUsername()) .is(username); try { LdapUser user = ldapTemplate.findOne(query, LdapUser.class); return Optional.ofNullable(user); } catch (Exception e) { log.error("Error finding LDAP user by username: {}", username, e); return Optional.empty(); } } /** * Find LDAP user by DN */ public Optional<LdapUser> findUserByDn(String dn) { try { LdapUser user = ldapTemplate.findByDn(LdapUtils.newLdapName(dn), LdapUser.class); return Optional.ofNullable(user); } catch (Exception e) { log.error("Error finding LDAP user by DN: {}", dn, e); return Optional.empty(); } } /** * Find all LDAP groups */ public List<LdapGroup> findAllGroups() { LdapQuery query = query() .base(ldapProperties.getSearch().getGroupBase()) .searchScope(SearchScope.SUBTREE) .filter(ldapProperties.getSearch().getGroupFilter()); log.debug("Searching for LDAP groups with query: {}", query); return ldapTemplate.find(query, LdapGroup.class); } /** * Find LDAP group by name */ public Optional<LdapGroup> findGroupByName(String groupName) { LdapQuery query = query() .base(ldapProperties.getSearch().getGroupBase()) .searchScope(SearchScope.SUBTREE) .where(ldapProperties.getAttributes().getGroup().getName()) .is(groupName); try { LdapGroup group = ldapTemplate.findOne(query, LdapGroup.class); return Optional.ofNullable(group); } catch (Exception e) { log.error("Error finding LDAP group by name: {}", groupName, e); return Optional.empty(); } } /** * Create user in LDAP */ public boolean createUser(LdapUser user) { try { ldapTemplate.create(user); log.info("Created LDAP user: {}", user.getUsername()); return true; } catch (Exception e) { log.error("Error creating LDAP user: {}", user.getUsername(), e); return false; } } /** * Update user in LDAP */ public boolean updateUser(LdapUser user) { try { ldapTemplate.update(user); log.info("Updated LDAP user: {}", user.getUsername()); return true; } catch (Exception e) { log.error("Error updating LDAP user: {}", user.getUsername(), e); return false; } } /** * Delete user from LDAP */ public boolean deleteUser(String dn) { try { ldapTemplate.unbind(dn); log.info("Deleted LDAP user: {}", dn); return true; } catch (Exception e) { log.error("Error deleting LDAP user: {}", dn, e); return false; } } /** * Check if user exists in LDAP */ public boolean userExists(String username) { return findUserByUsername(username).isPresent(); } /** * Get user attributes */ public Attributes getUserAttributes(String dn) { return ldapTemplate.lookup(dn); } /** * Modify user attributes */ public boolean modifyUserAttributes(String dn, ModificationItem[] modifications) { try { ldapTemplate.modifyAttributes(dn, modifications); return true; } catch (Exception e) { log.error("Error modifying user attributes: {}", dn, e); return false; } } /** * Search users with custom filter */ public List<LdapUser> searchUsers(String filter) { LdapQuery query = query() .base(ldapProperties.getSearch().getUserBase()) .searchScope(SearchScope.SUBTREE) .filter(filter); return ldapTemplate.find(query, LdapUser.class); } /** * Get users modified since timestamp */ public List<LdapUser> findUsersModifiedSince(String timestamp) { String filter = String.format("(&(objectClass=inetOrgPerson)(modifyTimestamp>=%s))", timestamp); return searchUsers(filter); } /** * Test LDAP connection */ public boolean testConnection() { try { ldapTemplate.list(""); log.info("LDAP connection test successful"); return true; } catch (Exception e) { log.error("LDAP connection test failed", e); return false; } } /** * Custom attributes mapper for complex queries */ private static class LdapUserAttributesMapper implements AttributesMapper<LdapUser> { @Override public LdapUser mapFromAttributes(Attributes attributes) throws NamingException { LdapUser user = new LdapUser(); // Map attributes manually if needed return user; } } }

Synchronization Services

1. User Synchronization Service
// UserSyncService.java package com.example.ldapsync.service; import com.example.ldapsync.model.*; import com.example.ldapsync.repository.AppUserRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @Slf4j @Service public class UserSyncService { private final AppUserRepository appUserRepository; private final LdapService ldapService; private final SyncConfig syncConfig; public UserSyncService(AppUserRepository appUserRepository, LdapService ldapService, SyncConfig syncConfig) { this.appUserRepository = appUserRepository; this.ldapService = ldapService; this.syncConfig = syncConfig; } /** * Synchronize users from LDAP to application */ @Transactional public SyncResult syncUsersFromLdap() { SyncResult result = new SyncResult(SyncResult.SyncType.USER_SYNC); try { log.info("Starting user synchronization from LDAP"); // Get all LDAP users List<LdapUser> ldapUsers = ldapService.findAllUsers(); log.info("Found {} users in LDAP", ldapUsers.size()); // Get existing application users Map<String, AppUser> appUsersByExternalId = appUserRepository.findAll() .stream() .collect(Collectors.toMap(AppUser::getExternalId, user -> user)); // Process each LDAP user for (LdapUser ldapUser : ldapUsers) { try { processLdapUser(ldapUser, appUsersByExternalId, result); } catch (Exception e) { log.error("Error processing LDAP user: {}", ldapUser.getUsername(), e); result.addError("USER", ldapUser.getUsername(), e.getMessage()); } } // Handle users that exist in app but not in LDAP (deprovisioning) handleDeprovisioning(ldapUsers, appUsersByExternalId, result); result.complete(true, String.format( "User synchronization completed. Processed %d users", result.getStatistics().getTotalUsersProcessed() )); log.info("User synchronization completed successfully"); } catch (Exception e) { log.error("User synchronization failed", e); result.complete(false, "User synchronization failed: " + e.getMessage()); } return result; } private void processLdapUser(LdapUser ldapUser, Map<String, AppUser> appUsersByExternalId, SyncResult result) { String externalId = ldapUser.getDistinguishedName(); AppUser existingUser = appUsersByExternalId.get(externalId); if (existingUser == null) { // Create new user createUserFromLdap(ldapUser, result); } else { // Update existing user updateUserFromLdap(existingUser, ldapUser, result); } } private void createUserFromLdap(LdapUser ldapUser, SyncResult result) { AppUser appUser = new AppUser(); mapLdapUserToAppUser(ldapUser, appUser); appUser.setCreatedDate(LocalDateTime.now()); appUser.setIsSynchronized(true); appUser.setLastSyncDate(LocalDateTime.now()); appUserRepository.save(appUser); result.getStatistics().setUsersCreated(result.getStatistics().getUsersCreated() + 1); log.debug("Created user from LDAP: {}", appUser.getUsername()); } private void updateUserFromLdap(AppUser appUser, LdapUser ldapUser, SyncResult result) { // Check if update is needed if (needsUpdate(appUser, ldapUser)) { mapLdapUserToAppUser(ldapUser, appUser); appUser.setIsSynchronized(true); appUser.setLastSyncDate(LocalDateTime.now()); appUserRepository.save(appUser); result.getStatistics().setUsersUpdated(result.getStatistics().getUsersUpdated() + 1); log.debug("Updated user from LDAP: {}", appUser.getUsername()); } else { result.getStatistics().setUsersSkipped(result.getStatistics().getUsersSkipped() + 1); } } private void mapLdapUserToAppUser(LdapUser ldapUser, AppUser appUser) { appUser.setExternalId(ldapUser.getDistinguishedName()); appUser.setUsername(ldapUser.getUsername()); appUser.setEmail(ldapUser.getEmail()); appUser.setFirstName(ldapUser.getFirstName()); appUser.setLastName(ldapUser.getLastName()); appUser.setDisplayName(ldapUser.getDisplayName()); appUser.setPhone(ldapUser.getTelephoneNumber()); appUser.setEmployeeNumber(ldapUser.getEmployeeNumber()); appUser.setDepartment(ldapUser.getDepartment()); appUser.setTitle(ldapUser.getTitle()); appUser.setIsActive(true); // Assuming LDAP users are active } private boolean needsUpdate(AppUser appUser, LdapUser ldapUser) { return !Objects.equals(appUser.getUsername(), ldapUser.getUsername()) || !Objects.equals(appUser.getEmail(), ldapUser.getEmail()) || !Objects.equals(appUser.getFirstName(), ldapUser.getFirstName()) || !Objects.equals(appUser.getLastName(), ldapUser.getLastName()) || !Objects.equals(appUser.getDisplayName(), ldapUser.getDisplayName()) || !Objects.equals(appUser.getPhone(), ldapUser.getTelephoneNumber()) || !Objects.equals(appUser.getEmployeeNumber(), ldapUser.getEmployeeNumber()) || !Objects.equals(appUser.getDepartment(), ldapUser.getDepartment()) || !Objects.equals(appUser.getTitle(), ldapUser.getTitle()); } private void handleDeprovisioning(List<LdapUser> ldapUsers, Map<String, AppUser> appUsersByExternalId, SyncResult result) { Set<String> ldapExternalIds = ldapUsers.stream() .map(LdapUser::getDistinguishedName) .collect(Collectors.toSet()); for (AppUser appUser : appUsersByExternalId.values()) { if (appUser.getIsSynchronized() && !ldapExternalIds.contains(appUser.getExternalId())) { // User exists in app but not in LDAP - deactivate appUser.setIsActive(false); appUser.setLastSyncDate(LocalDateTime.now()); appUserRepository.save(appUser); result.getStatistics().setUsersDeleted(result.getStatistics().getUsersDeleted() + 1); log.debug("Deactivated user not found in LDAP: {}", appUser.getUsername()); } } } /** * Synchronize single user from LDAP to application */ public boolean syncUserFromLdap(String username) { Optional<LdapUser> ldapUserOpt = ldapService.findUserByUsername(username); if (ldapUserOpt.isEmpty()) { log.warn("User not found in LDAP: {}", username); return false; } LdapUser ldapUser = ldapUserOpt.get(); Optional<AppUser> appUserOpt = appUserRepository.findByExternalId(ldapUser.getDistinguishedName()); if (appUserOpt.isPresent()) { AppUser appUser = appUserOpt.get(); mapLdapUserToAppUser(ldapUser, appUser); appUser.setLastSyncDate(LocalDateTime.now()); appUserRepository.save(appUser); log.info("Updated user from LDAP: {}", username); } else { AppUser appUser = new AppUser(); mapLdapUserToAppUser(ldapUser, appUser); appUser.setCreatedDate(LocalDateTime.now()); appUser.setIsSynchronized(true); appUserRepository.save(appUser); log.info("Created user from LDAP: {}", username); } return true; } /** * Synchronize user from application to LDAP */ public boolean syncUserToLdap(Long userId) { Optional<AppUser> appUserOpt = appUserRepository.findById(userId); if (appUserOpt.isEmpty()) { log.warn("User not found in application: {}", userId); return false; } AppUser appUser = appUserOpt.get(); LdapUser ldapUser = new LdapUser(); mapAppUserToLdapUser(appUser, ldapUser); if (appUser.getExternalId() != null) { // Update existing LDAP user return ldapService.updateUser(ldapUser); } else { // Create new LDAP user boolean success = ldapService.createUser(ldapUser); if (success) { // Update external ID in application appUser.setExternalId(ldapUser.getDistinguishedName()); appUser.setLastSyncDate(LocalDateTime.now()); appUserRepository.save(appUser); } return success; } } private void mapAppUserToLdapUser(AppUser appUser, LdapUser ldapUser) { ldapUser.setUsername(appUser.getUsername()); ldapUser.setEmail(appUser.getEmail()); ldapUser.setFirstName(appUser.getFirstName()); ldapUser.setLastName(appUser.getLastName()); ldapUser.setDisplayName(appUser.getDisplayName()); ldapUser.setTelephoneNumber(appUser.getPhone()); ldapUser.setEmployeeNumber(appUser.getEmployeeNumber()); ldapUser.setDepartment(appUser.getDepartment()); ldapUser.setTitle(appUser.getTitle()); } }
2. Group Synchronization Service
// GroupSyncService.java package com.example.ldapsync.service; import com.example.ldapsync.model.*; import com.example.ldapsync.repository.UserGroupRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @Slf4j @Service public class GroupSyncService { private final UserGroupRepository groupRepository; private final LdapService ldapService; private final UserSyncService userSyncService; public GroupSyncService(UserGroupRepository groupRepository, LdapService ldapService, UserSyncService userSyncService) { this.groupRepository = groupRepository; this.ldapService = ldapService; this.userSyncService = userSyncService; } /** * Synchronize groups from LDAP to application */ @Transactional public SyncResult syncGroupsFromLdap() { SyncResult result = new SyncResult(SyncResult.SyncType.GROUP_SYNC); try { log.info("Starting group synchronization from LDAP"); // Get all LDAP groups List<LdapGroup> ldapGroups = ldapService.findAllGroups(); log.info("Found {} groups in LDAP", ldapGroups.size()); // Get existing application groups Map<String, UserGroup> appGroupsByExternalId = groupRepository.findAll() .stream() .collect(Collectors.toMap(UserGroup::getExternalId, group -> group)); // Process each LDAP group for (LdapGroup ldapGroup : ldapGroups) { try { processLdapGroup(ldapGroup, appGroupsByExternalId, result); } catch (Exception e) { log.error("Error processing LDAP group: {}", ldapGroup.getName(), e); result.addError("GROUP", ldapGroup.getName(), e.getMessage()); } } // Handle groups that exist in app but not in LDAP handleGroupDeprovisioning(ldapGroups, appGroupsByExternalId, result); result.complete(true, String.format( "Group synchronization completed. Processed %d groups", result.getStatistics().getTotalGroupsProcessed() )); log.info("Group synchronization completed successfully"); } catch (Exception e) { log.error("Group synchronization failed", e); result.complete(false, "Group synchronization failed: " + e.getMessage()); } return result; } private void processLdapGroup(LdapGroup ldapGroup, Map<String, UserGroup> appGroupsByExternalId, SyncResult result) { String externalId = ldapGroup.getDistinguishedName(); UserGroup existingGroup = appGroupsByExternalId.get(externalId); if (existingGroup == null) { // Create new group createGroupFromLdap(ldapGroup, result); } else { // Update existing group updateGroupFromLdap(existingGroup, ldapGroup, result); } } private void createGroupFromLdap(LdapGroup ldapGroup, SyncResult result) { UserGroup userGroup = new UserGroup(); mapLdapGroupToAppGroup(ldapGroup, userGroup); userGroup.setCreatedDate(LocalDateTime.now()); userGroup.setIsSynchronized(true); userGroup.setLastSyncDate(LocalDateTime.now()); groupRepository.save(userGroup); result.getStatistics().setGroupsCreated(result.getStatistics().getGroupsCreated() + 1); log.debug("Created group from LDAP: {}", userGroup.getName()); } private void updateGroupFromLdap(UserGroup userGroup, LdapGroup ldapGroup, SyncResult result) { if (needsGroupUpdate(userGroup, ldapGroup)) { mapLdapGroupToAppGroup(ldapGroup, userGroup); userGroup.setIsSynchronized(true); userGroup.setLastSyncDate(LocalDateTime.now()); groupRepository.save(userGroup); result.getStatistics().setGroupsUpdated(result.getStatistics().getGroupsUpdated() + 1); log.debug("Updated group from LDAP: {}", userGroup.getName()); } else { result.getStatistics().setGroupsSkipped(result.getStatistics().getGroupsSkipped() + 1); } } private void mapLdapGroupToAppGroup(LdapGroup ldapGroup, UserGroup userGroup) { userGroup.setExternalId(ldapGroup.getDistinguishedName()); userGroup.setName(ldapGroup.getName()); userGroup.setDescription(ldapGroup.getDescription()); userGroup.setIsActive(true); } private boolean needsGroupUpdate(UserGroup userGroup, LdapGroup ldapGroup) { return !Objects.equals(userGroup.getName(), ldapGroup.getName()) || !Objects.equals(userGroup.getDescription(), ldapGroup.getDescription()); } private void handleGroupDeprovisioning(List<LdapGroup> ldapGroups, Map<String, UserGroup> appGroupsByExternalId, SyncResult result) { Set<String> ldapExternalIds = ldapGroups.stream() .map(LdapGroup::getDistinguishedName) .collect(Collectors.toSet()); for (UserGroup userGroup : appGroupsByExternalId.values()) { if (userGroup.getIsSynchronized() && !ldapExternalIds.contains(userGroup.getExternalId())) { // Group exists in app but not in LDAP - deactivate userGroup.setIsActive(false); userGroup.setLastSyncDate(LocalDateTime.now()); groupRepository.save(userGroup); result.getStatistics().setGroupsDeleted(result.getStatistics().getGroupsDeleted() + 1); log.debug("Deactivated group not found in LDAP: {}", userGroup.getName()); } } } }
3. Main Synchronization Service
// LdapSyncService.java package com.example.ldapsync.service; import com.example.ldapsync.model.SyncConfig; import com.example.ldapsync.model.SyncResult; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Slf4j @Service public class LdapSyncService { private final UserSyncService userSyncService; private final GroupSyncService groupSyncService; private final SyncConfig syncConfig; private final LdapService ldapService; public LdapSyncService(UserSyncService userSyncService, GroupSyncService groupSyncService, SyncConfig syncConfig, LdapService ldapService) { this.userSyncService = userSyncService; this.groupSyncService = groupSyncService; this.syncConfig = syncConfig; this.ldapService = ldapService; } /** * Full synchronization (users and groups) */ public SyncResult fullSynchronization() { log.info("Starting full LDAP synchronization"); SyncResult result = new SyncResult(SyncResult.SyncType.FULL_SYNC); if (!syncConfig.isEnabled()) { result.complete(false, "LDAP synchronization is disabled"); return result; } if (!ldapService.testConnection()) { result.complete(false, "LDAP connection test failed"); return result; } try { // Sync users SyncResult userResult = userSyncService.syncUsersFromLdap(); result.getStatistics().setUsersCreated(userResult.getStatistics().getUsersCreated()); result.getStatistics().setUsersUpdated(userResult.getStatistics().getUsersUpdated()); result.getStatistics().setUsersDeleted(userResult.getStatistics().getUsersDeleted()); result.getStatistics().setUsersSkipped(userResult.getStatistics().getUsersSkipped()); // Sync groups SyncResult groupResult = groupSyncService.syncGroupsFromLdap(); result.getStatistics().setGroupsCreated(groupResult.getStatistics().getGroupsCreated()); result.getStatistics().setGroupsUpdated(groupResult.getStatistics().getGroupsUpdated()); result.getStatistics().setGroupsDeleted(groupResult.getStatistics().getGroupsDeleted()); result.getStatistics().setGroupsSkipped(groupResult.getStatistics().getGroupsSkipped()); result.complete(true, "Full synchronization completed successfully"); log.info("Full synchronization completed"); } catch (Exception e) { log.error("Full synchronization failed", e); result.complete(false, "Full synchronization failed: " + e.getMessage()); } return result; } /** * Incremental synchronization (only changed users/groups) */ public SyncResult incrementalSynchronization() { log.info("Starting incremental LDAP synchronization"); // Implementation for incremental sync based on modifyTimestamp // This would only sync users/groups changed since last sync return fullSynchronization(); // Fallback to full sync for now } /** * Scheduled synchronization */ @Scheduled(cron = "${app.ldap.sync.schedule:0 0 2 * * ?}") public void scheduledSynchronization() { if (!syncConfig.isEnabled()) { log.debug("Scheduled LDAP synchronization is disabled"); return; } log.info("Starting scheduled LDAP synchronization"); SyncResult result = fullSynchronization(); if (result.isSuccess()) { log.info("Scheduled synchronization completed successfully. " + "Users: {} created, {} updated, {} deleted. " + "Groups: {} created, {} updated, {} deleted.", result.getStatistics().getUsersCreated(), result.getStatistics().getUsersUpdated(), result.getStatistics().getUsersDeleted(), result.getStatistics().getGroupsCreated(), result.getStatistics().getGroupsUpdated(), result.getStatistics().getGroupsDeleted()); } else { log.error("Scheduled synchronization failed: {}", result.getMessage()); } } /** * Manual synchronization trigger */ public SyncResult manualSynchronization(boolean fullSync) { if (fullSync) { return fullSynchronization(); } else { return incrementalSynchronization(); } } /** * Synchronize specific user */ public boolean syncUser(String username) { return userSyncService.syncUserFromLdap(username); } /** * Synchronize specific group */ public boolean syncGroup(String groupName) { // Implementation for syncing specific group return true; } /** * Test LDAP connection */ public boolean testLdapConnection() { return ldapService.testConnection(); } /** * Get synchronization status */ public SyncStatus getSyncStatus() { // Implementation to get current sync status return new SyncStatus(); } public static class SyncStatus { private LocalDateTime lastSyncTime; private boolean lastSyncSuccess; private String lastSyncMessage; // Getters and setters } }

REST Controllers

1. Synchronization Controller
// SyncController.java package com.example.ldapsync.controller; import com.example.ldapsync.model.SyncResult; import com.example.ldapsync.service.LdapSyncService; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; @Slf4j @RestController @RequestMapping("/api/sync") public class SyncController { private final LdapSyncService ldapSyncService; public SyncController(LdapSyncService ldapSyncService) { this.ldapSyncService = ldapSyncService; } @PostMapping("/full") public ResponseEntity<SyncResult> fullSync() { log.info("Manual full synchronization requested"); SyncResult result = ldapSyncService.fullSynchronization(); return ResponseEntity.ok(result); } @PostMapping("/incremental") public ResponseEntity<SyncResult> incrementalSync() { log.info("Manual incremental synchronization requested"); SyncResult result = ldapSyncService.incrementalSynchronization(); return ResponseEntity.ok(result); } @PostMapping("/user/{username}") public ResponseEntity<Map<String, Object>> syncUser(@PathVariable String username) { log.info("User synchronization requested: {}", username); boolean success = ldapSyncService.syncUser(username); if (success) { return ResponseEntity.ok(Map.of( "status", "success", "message", String.format("User %s synchronized successfully", username) )); } else { return ResponseEntity.badRequest().body(Map.of( "status", "error", "message", String.format("Failed to synchronize user %s", username) )); } } @GetMapping("/status") public ResponseEntity<Map<String, Object>> getSyncStatus() { LdapSyncService.SyncStatus status = ldapSyncService.getSyncStatus(); return ResponseEntity.ok(Map.of( "lastSyncTime", status.getLastSyncTime(), "lastSyncSuccess", status.isLastSyncSuccess(), "lastSyncMessage", status.getLastSyncMessage() )); } @GetMapping("/test-connection") public ResponseEntity<Map<String, Object>> testConnection() { boolean connected = ldapSyncService.testLdapConnection(); return ResponseEntity.ok(Map.of( "connected", connected, "message", connected ? "LDAP connection successful" : "LDAP connection failed" )); } }

Repositories

1. User Repository
// AppUserRepository.java package com.example.ldapsync.repository; import com.example.ldapsync.model.AppUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface AppUserRepository extends JpaRepository<AppUser, Long> { Optional<AppUser> findByUsername(String username); Optional<AppUser> findByExternalId(String externalId); List<AppUser> findByIsSynchronizedTrue(); List<AppUser> findByIsActiveTrue(); @Query("SELECT u FROM AppUser u WHERE u.lastSyncDate < :cutoffDate AND u.isSynchronized = true") List<AppUser> findStaleUsers(java.time.LocalDateTime cutoffDate); boolean existsByUsername(String username); boolean existsByExternalId(String externalId); }
2. Group Repository
// UserGroupRepository.java package com.example.ldapsync.repository; import com.example.ldapsync.model.UserGroup; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface UserGroupRepository extends JpaRepository<UserGroup, Long> { Optional<UserGroup> findByName(String name); Optional<UserGroup> findByExternalId(String externalId); List<UserGroup> findByIsSynchronizedTrue(); List<UserGroup> findByIsActiveTrue(); boolean existsByName(String name); boolean existsByExternalId(String externalId); }

Testing

1. Unit Tests
// UserSyncServiceTest.java package com.example.ldapsync.service; import com.example.ldapsync.model.AppUser; import com.example.ldapsync.model.LdapUser; import com.example.ldapsync.model.SyncConfig; import com.example.ldapsync.repository.AppUserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import javax.naming.Name; import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserSyncServiceTest { @Mock private AppUserRepository appUserRepository; @Mock private LdapService ldapService; @Mock private SyncConfig syncConfig; private UserSyncService userSyncService; @BeforeEach void setUp() { userSyncService = new UserSyncService(appUserRepository, ldapService, syncConfig); } @Test void testSyncUserFromLdap_NewUser() { // Setup LdapUser ldapUser = new LdapUser(); ldapUser.setUsername("testuser"); ldapUser.setEmail("[email protected]"); ldapUser.setFirstName("Test"); ldapUser.setLastName("User"); ldapUser.setDn(mock(Name.class)); when(ldapUser.getDistinguishedName()).thenReturn("cn=testuser,ou=users,dc=example,dc=com"); when(ldapService.findUserByUsername("testuser")).thenReturn(Optional.of(ldapUser)); when(appUserRepository.findByExternalId(anyString())).thenReturn(Optional.empty()); when(appUserRepository.save(any(AppUser.class))).thenAnswer(invocation -> invocation.getArgument(0)); // Execute boolean result = userSyncService.syncUserFromLdap("testuser"); // Verify assertTrue(result); verify(appUserRepository).save(any(AppUser.class)); } @Test void testSyncUsersFromLdap() { // Setup LdapUser ldapUser = new LdapUser(); ldapUser.setUsername("testuser"); ldapUser.setEmail("[email protected]"); ldapUser.setDn(mock(Name.class)); when(ldapUser.getDistinguishedName()).thenReturn("cn=testuser,ou=users,dc=example,dc=com"); when(ldapService.findAllUsers()).thenReturn(List.of(ldapUser)); when(appUserRepository.findAll()).thenReturn(List.of()); when(appUserRepository.save(any(AppUser.class))).thenAnswer(invocation -> invocation.getArgument(0)); // Execute var result = userSyncService.syncUsersFromLdap(); // Verify assertTrue(result.isSuccess()); assertEquals(1, result.getStatistics().getUsersCreated()); } }
2. Integration Test with Embedded LDAP
// LdapSyncIntegrationTest.java package com.example.ldapsync.integration; import com.example.ldapsync.model.SyncResult; import com.example.ldapsync.service.LdapSyncService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Testcontainers @ActiveProfiles("test") class LdapSyncIntegrationTest { @Container static GenericContainer<?> ldapContainer = new GenericContainer<>("osixia/openldap:latest") .withExposedPorts(389) .withEnv("LDAP_ADMIN_PASSWORD", "admin123") .withEnv("LDAP_DOMAIN", "example.com") .withEnv("LDAP_ORGANISATION", "Example Inc"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.ldap.urls", () -> "ldap://" + ldapContainer.getHost() + ":" + ldapContainer.getMappedPort(389)); registry.add("spring.ldap.base", () -> "dc=example,dc=com"); registry.add("spring.ldap.username", () -> "cn=admin,dc=example,dc=com"); registry.add("spring.ldap.password", () -> "admin123"); } @Autowired private LdapSyncService ldapSyncService; @Test void testLdapConnection() { boolean connected = ldapSyncService.testLdapConnection(); assertTrue(connected, "LDAP connection should be successful"); } @Test void testFullSynchronization() { SyncResult result = ldapSyncService.fullSynchronization(); assertTrue(result.isSuccess(), "Synchronization should complete successfully"); } }

Production Considerations

1. Error Handling and Retry
// RetryableLdapService.java package com.example.ldapsync.service; import lombok.extern.slf4j.Slf4j; import org.springframework.ldap.NamingException; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Slf4j @Service public class RetryableLdapService { @Retryable( value = {NamingException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) ) public void performLdapOperation() { // LDAP operation implementation } }
2. Monitoring and Metrics
// SyncMetricsService.java package com.example.ldapsync.service; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Service; @Service public class SyncMetricsService { private final Counter usersSynced; private final Counter groupsSynced; private final Counter syncErrors; public SyncMetricsService(MeterRegistry registry) { this.usersSynced = Counter.builder("ldap.sync.users") .description("Number of users synchronized") .register(registry); this.groupsSynced = Counter.builder("ldap.sync.groups") .description("Number of groups synchronized") .register(registry); this.syncErrors = Counter.builder("ldap.sync.errors") .description("Number of synchronization errors") .register(registry); } public void recordUserSync() { usersSynced.increment(); } public void recordGroupSync() { groupsSynced.increment(); } public void recordSyncError() { syncErrors.increment(); } }

Best Practices

  1. Security:
  • Use secure LDAP (LDAPS) in production
  • Store credentials securely (vault/secret management)
  • Implement proper access controls
  1. Performance:
  • Use connection pooling
  • Implement pagination for large directories
  • Schedule sync during off-peak hours
  • Use incremental sync when possible
  1. Reliability:
  • Implement retry mechanisms
  • Handle connection failures gracefully
  • Maintain sync logs for audit purposes
  • Implement conflict resolution strategies
  1. Monitoring:
  • Track sync performance metrics
  • Monitor LDAP connection health
  • Alert on sync failures
  • Log synchronization activities

Conclusion

This comprehensive LDAP user synchronization implementation provides:

  • Bidirectional synchronization between LDAP and application
  • Flexible configuration for different synchronization scenarios
  • Robust error handling and retry mechanisms
  • Comprehensive monitoring and logging
  • Production-ready features including scheduling and metrics

The solution can be extended with:

  • Real-time synchronization using change notifications
  • Advanced conflict resolution strategies
  • Multi-domain LDAP support
  • User provisioning workflows
  • Advanced filtering and mapping capabilities

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper