From 5f5ceaa47a64a032693f3b9b9cc3f0a2ed157c2e Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 21 Feb 2014 08:36:18 -0500
Subject: [PATCH] Merged ticket #5 patchset 2

---
 src/test/resources/ldap/sampledata.ldif                      |   12 +
 src/test/resources/ldap/adduser.ldif                         |   11 +
 src/test/java/com/gitblit/tests/LdapSyncServiceTest.java     |   72 ++++++
 src/main/distrib/data/gitblit.properties                     |   62 +++--
 src/test/java/com/gitblit/tests/LdapAuthenticationTest.java  |  125 +++++++++--
 src/test/resources/ldap/users.conf                           |   19 +
 src/main/java/com/gitblit/auth/AuthenticationProvider.java   |   12 +
 src/test/java/com/gitblit/tests/GitBlitSuite.java            |    2 
 src/test/config/test-users.conf                              |    5 
 src/main/java/com/gitblit/auth/LdapAuthProvider.java         |  195 +++++++++++------
 src/main/java/com/gitblit/service/LdapSyncService.java       |   69 ++++++
 src/test/resources/ldap/addgroup.ldif                        |    5 
 src/main/java/com/gitblit/manager/AuthenticationManager.java |    7 
 releases.moxie                                               |    5 
 14 files changed, 485 insertions(+), 116 deletions(-)

diff --git a/releases.moxie b/releases.moxie
index 66643cf..6548713 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -33,6 +33,7 @@
 	- Reversed line links in blob view (issue-309)
 	- Dashboard and Activity pages now obey the web.generateActivityGraph setting (issue-310)
 	- Do not log passwords on failed authentication attempts (issue-316)
+	- LDAP synchronization is now scheduled rather than on-demand (issue-336)
 	- Show displayname and username in palettes (issue-364)
 	- Updated default binary and Lucene ignore extensions
 	- Change the WAR baseFolder context parameter to a JNDI env-entry to improve enterprise deployments
@@ -82,6 +83,10 @@
 	- { name: 'git.defaultAccessRestriction', defaultValue: 'PUSH' }
 	- { name: 'git.mirrorPeriod', defaultValue: '30 mins' }
 	- { name: 'realm.authenticationProviders', defaultValue: ' ' }
+	- { name: 'realm.ldap.groupEmptyMemberPattern', defaultValue: '(&(objectClass=group)(!(member=*)))' }
+	- { name: 'realm.ldap.synchronize', defaultValue: 'false' }
+	- { name: 'realm.ldap.syncPeriod', defaultValue: '5 MINUTES' }
+	- { name: 'realm.ldap.removeDeletedUsers', defaultValue: 'true' }
 	- { name: 'web.commitMessageRenderer', defaultValue: 'plain' }
 	- { name: 'web.documents', defaultValue: 'readme home index changelog contributing submitting_patches copying license notice authors' }
 	- { name: 'web.showBranchGraph', defaultValue: 'true' }
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index 482a835..da63832 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -1460,6 +1460,15 @@
 # SINCE 1.0.0
 realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn}))
 
+# Filter criteria for empty LDAP groups
+#
+# Query pattern to use when searching for an empty team. This may be any valid 
+# LDAP query expression, including the standard (&) and (|) operators.
+#
+# default: (&(objectClass=group)(!(member=*)))
+# SINCE 1.4.0
+realm.ldap.groupEmptyMemberPattern = (&(objectClass=group)(!(member=*)))
+
 # LDAP users or groups that should be given administrator privileges.
 #
 # Teams are specified with a leading '@' character.  Groups with spaces in the
@@ -1493,35 +1502,44 @@
 # SINCE 1.0.0
 realm.ldap.email = email
 
-# Defines the cache period to be used when caching LDAP queries. This is currently
-# only used for LDAP user synchronization.
-#
-# Must be of the form '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS' 
-# default: 2 MINUTES
-#
-# RESTART REQUIRED
-realm.ldap.ldapCachePeriod = 2 MINUTES
-
-# Defines whether to synchronize all LDAP users into the backing user service
-#
-# Valid values: true, false
-# If left blank, false is assumed
-realm.ldap.synchronizeUsers.enable = false
-
-# Defines whether to delete non-existent LDAP users from the backing user service
-# during synchronization. depends on  realm.ldap.synchronizeUsers.enable = true
-#
-# Valid values: true, false
-# If left blank, true is assumed
-realm.ldap.synchronizeUsers.removeDeleted = true
-
 # Attribute on the USER record that indicate their username to be used in gitblit
 # when synchronizing users from LDAP
 # if blank, Gitblit will use uid
 # For MS Active Directory this may be sAMAccountName
+#
+# SINCE 1.0.0
 realm.ldap.uid = uid
 
+# Defines whether to synchronize all LDAP users and teams into the user service
+#
+# Valid values: true, false
+# If left blank, false is assumed
+#
+# SINCE 1.4.0
+realm.ldap.synchronize = false
+
+# Defines the period to be used when synchronizing users and teams from ldap.
+#
+# Must be of the form '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS' 
+
+# default: 5 MINUTES
+#
+# RESTART REQUIRED
+# SINCE 1.4.0
+realm.ldap.syncPeriod = 5 MINUTES
+
+# Defines whether to delete non-existent LDAP users from the user service
+# during synchronization. depends on  realm.ldap.synchronize = true
+#
+# Valid values: true, false
+# If left blank, true is assumed
+#
+# SINCE 1.4.0
+realm.ldap.removeDeletedUsers = true
+
 # URL of the Redmine.
+#
+# SINCE 1.2.0
 realm.redmine.url = http://example.com/redmine
 
 #
diff --git a/src/main/java/com/gitblit/auth/AuthenticationProvider.java b/src/main/java/com/gitblit/auth/AuthenticationProvider.java
index f7b75fa..29051df 100644
--- a/src/main/java/com/gitblit/auth/AuthenticationProvider.java
+++ b/src/main/java/com/gitblit/auth/AuthenticationProvider.java
@@ -100,6 +100,8 @@
 
 	public abstract void setup();
 
+	public abstract void stop();
+
 	public abstract UserModel authenticate(String username, char[] password);
 
 	public abstract AccountType getAccountType();
@@ -145,6 +147,11 @@
     	protected UsernamePasswordAuthenticationProvider(String serviceName) {
     		super(serviceName);
     	}
+
+    	@Override
+		public void stop() {
+
+		}
     }
 
     public static class NullProvider extends AuthenticationProvider {
@@ -159,6 +166,11 @@
 		}
 
 		@Override
+		public void stop() {
+
+		}
+
+		@Override
 		public UserModel authenticate(String username, char[] password) {
 			return null;
 		}
diff --git a/src/main/java/com/gitblit/auth/LdapAuthProvider.java b/src/main/java/com/gitblit/auth/LdapAuthProvider.java
index 8fef620..3a688d8 100644
--- a/src/main/java/com/gitblit/auth/LdapAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/LdapAuthProvider.java
@@ -19,12 +19,14 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
+import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
 
 import com.gitblit.Constants;
 import com.gitblit.Constants.AccountType;
@@ -32,6 +34,7 @@
 import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
+import com.gitblit.service.LdapSyncService;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.StringUtils;
 import com.unboundid.ldap.sdk.Attribute;
@@ -57,101 +60,119 @@
  */
 public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
 
-    private AtomicLong lastLdapUserSync = new AtomicLong(0L);
+	private final ScheduledExecutorService scheduledExecutorService;
 
 	public LdapAuthProvider() {
 		super("ldap");
+
+		scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
 	}
 
- 	private long getSynchronizationPeriod() {
-        final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES");
+ 	private long getSynchronizationPeriodInMilliseconds() {
+ 		String period = settings.getString(Keys.realm.ldap.syncPeriod, null);
+ 		if (StringUtils.isEmpty(period)) {
+ 	 		period = settings.getString("realm.ldap.ldapCachePeriod", null);
+ 	 		if (StringUtils.isEmpty(period)) {
+ 	 			period = "5 MINUTES";
+ 	 		} else {
+ 	 			logger.warn("realm.ldap.ldapCachePeriod is obsolete!");
+ 	 			logger.warn(MessageFormat.format("Please set {0}={1} in gitblit.properties!", Keys.realm.ldap.syncPeriod, period));
+ 	 			settings.overrideSetting(Keys.realm.ldap.syncPeriod, period);
+ 	 		}
+ 		}
+
         try {
-            final String[] s = cacheDuration.split(" ", 2);
-            long duration = Long.parseLong(s[0]);
+            final String[] s = period.split(" ", 2);
+            long duration = Math.abs(Long.parseLong(s[0]));
             TimeUnit timeUnit = TimeUnit.valueOf(s[1]);
             return timeUnit.toMillis(duration);
         } catch (RuntimeException ex) {
-            throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'");
+            throw new IllegalArgumentException(Keys.realm.ldap.syncPeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'");
         }
     }
 
 	@Override
 	public void setup() {
-		synchronizeLdapUsers();
+		configureSyncService();
 	}
 
-	protected synchronized void synchronizeLdapUsers() {
-        final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false);
-        if (enabled) {
-            if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriod())) {
-            	logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
-                final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true);
-                LDAPConnection ldapConnection = getLdapConnection();
-                if (ldapConnection != null) {
-                    try {
-                        String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
-                        String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
-                        String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
-                        accountPattern = StringUtils.replace(accountPattern, "${username}", "*");
+	@Override
+	public void stop() {
+		scheduledExecutorService.shutdownNow();
+	}
 
-                        SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
-                        if (result != null && result.getEntryCount() > 0) {
-                            final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>();
+	public synchronized void sync() {
+		final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronize, false);
+		if (enabled) {
+			logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
+			final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true);
+			LDAPConnection ldapConnection = getLdapConnection();
+			if (ldapConnection != null) {
+				try {
+					String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
+					String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
+					String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+					accountPattern = StringUtils.replace(accountPattern, "${username}", "*");
 
-                            for (SearchResultEntry loggingInUser : result.getSearchEntries()) {
+					SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
+					if (result != null && result.getEntryCount() > 0) {
+						final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>();
 
-                                final String username = loggingInUser.getAttribute(uidAttribute).getValue();
-                                logger.debug("LDAP synchronizing: " + username);
+						for (SearchResultEntry loggingInUser : result.getSearchEntries()) {
 
-                                UserModel user = userManager.getUserModel(username);
-                                if (user == null) {
-                                    user = new UserModel(username);
-                                }
+							final String username = loggingInUser.getAttribute(uidAttribute).getValue();
+							logger.debug("LDAP synchronizing: " + username);
 
-                                if (!supportsTeamMembershipChanges()) {
-                                    getTeamsFromLdap(ldapConnection, username, loggingInUser, user);
-                                }
+							UserModel user = userManager.getUserModel(username);
+							if (user == null) {
+								user = new UserModel(username);
+							}
 
-                                // Get User Attributes
-                                setUserAttributes(user, loggingInUser);
+							if (!supportsTeamMembershipChanges()) {
+								getTeamsFromLdap(ldapConnection, username, loggingInUser, user);
+							}
 
-                                // store in map
-                                ldapUsers.put(username.toLowerCase(), user);
-                            }
+							// Get User Attributes
+							setUserAttributes(user, loggingInUser);
 
-                            if (deleteRemovedLdapUsers) {
-                                logger.debug("detecting removed LDAP users...");
+							// store in map
+							ldapUsers.put(username.toLowerCase(), user);
+						}
 
-                                for (UserModel userModel : userManager.getAllUsers()) {
-                                    if (Constants.EXTERNAL_ACCOUNT.equals(userModel.password)) {
-                                        if (!ldapUsers.containsKey(userModel.username)) {
-                                            logger.info("deleting removed LDAP user " + userModel.username + " from user service");
-                                            userManager.deleteUser(userModel.username);
-                                        }
-                                    }
-                                }
-                            }
+						if (deleteRemovedLdapUsers) {
+							logger.debug("detecting removed LDAP users...");
 
-                            userManager.updateUserModels(ldapUsers.values());
+							for (UserModel userModel : userManager.getAllUsers()) {
+								if (AccountType.LDAP == userModel.accountType) {
+									if (!ldapUsers.containsKey(userModel.username)) {
+										logger.info("deleting removed LDAP user " + userModel.username + " from user service");
+										userManager.deleteUser(userModel.username);
+									}
+								}
+							}
+						}
 
-                            if (!supportsTeamMembershipChanges()) {
-                                final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>();
-                                for (UserModel user : ldapUsers.values()) {
-                                    for (TeamModel userTeam : user.teams) {
-                                        userTeams.put(userTeam.name, userTeam);
-                                    }
-                                }
-                                userManager.updateTeamModels(userTeams.values());
-                            }
-                        }
-                        lastLdapUserSync.set(System.currentTimeMillis());
-                    } finally {
-                        ldapConnection.close();
-                    }
-                }
-            }
-        }
-    }
+						userManager.updateUserModels(ldapUsers.values());
+
+						if (!supportsTeamMembershipChanges()) {
+							final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>();
+							for (UserModel user : ldapUsers.values()) {
+								for (TeamModel userTeam : user.teams) {
+									userTeams.put(userTeam.name, userTeam);
+								}
+							}
+							userManager.updateTeamModels(userTeams.values());
+						}
+					}
+					if (!supportsTeamMembershipChanges()) {
+						getEmptyTeamsFromLdap(ldapConnection);
+					}
+				} finally {
+					ldapConnection.close();
+				}
+			}
+		}
+	}
 
 	private LDAPConnection getLdapConnection() {
 		try {
@@ -427,6 +448,29 @@
 		}
 	}
 
+	private void getEmptyTeamsFromLdap(LDAPConnection ldapConnection) {
+		logger.info("Start fetching empty teams from ldap.");
+		String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
+		String groupMemberPattern = settings.getString(Keys.realm.ldap.groupEmptyMemberPattern, "(&(objectClass=group)(!(member=*)))");
+
+		SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, null);
+		if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
+			for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
+				SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
+				if (!teamEntry.hasAttribute("member")) {
+					String teamName = teamEntry.getAttribute("cn").getValue();
+
+					TeamModel teamModel = userManager.getTeamModel(teamName);
+					if (teamModel == null) {
+						teamModel = createTeamFromLdap(teamEntry);
+						userManager.updateTeamModel(teamModel);
+					}
+				}
+			}
+		}
+		logger.info("Finished fetching empty teams from ldap.");
+	}
+
 	private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
 		TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
 		answer.accountType = getAccountType();
@@ -519,4 +563,17 @@
 		}
 		return sb.toString();
 	}
+
+	private void configureSyncService() {
+		LdapSyncService ldapSyncService = new LdapSyncService(settings, this);
+		if (ldapSyncService.isReady()) {
+			long ldapSyncPeriod = getSynchronizationPeriodInMilliseconds();
+			int delay = 1;
+			logger.info("Ldap sync service will update users and groups every {} minutes.", ldapSyncPeriod);
+			scheduledExecutorService.scheduleAtFixedRate(ldapSyncService, delay, ldapSyncPeriod,  TimeUnit.MILLISECONDS);
+		} else {
+			logger.info("Ldap sync service is disabled.");
+		}
+	}
+
 }
diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java
index cd4a258..4897514 100644
--- a/src/main/java/com/gitblit/manager/AuthenticationManager.java
+++ b/src/main/java/com/gitblit/manager/AuthenticationManager.java
@@ -150,6 +150,13 @@
 
 	@Override
 	public AuthenticationManager stop() {
+		for (AuthenticationProvider provider : authenticationProviders) {
+			try {
+				provider.stop();
+			} catch (Exception e) {
+				logger.error("Failed to stop " + provider.getClass().getSimpleName(), e);
+			}
+		}
 		return this;
 	}
 
diff --git a/src/main/java/com/gitblit/service/LdapSyncService.java b/src/main/java/com/gitblit/service/LdapSyncService.java
new file mode 100644
index 0000000..7ae19aa
--- /dev/null
+++ b/src/main/java/com/gitblit/service/LdapSyncService.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2014 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.service;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.auth.LdapAuthProvider;
+
+/**
+ * @author Alfred Schmid
+ *
+ */
+public final class LdapSyncService implements Runnable {
+
+	private final Logger logger = LoggerFactory.getLogger(LdapSyncService.class);
+
+	private final IStoredSettings settings;
+
+	private final LdapAuthProvider ldapAuthProvider;
+
+	private final AtomicBoolean running = new AtomicBoolean(false);
+
+	public LdapSyncService(IStoredSettings settings, LdapAuthProvider ldapAuthProvider) {
+		this.settings = settings;
+		this.ldapAuthProvider = ldapAuthProvider;
+	}
+
+	/**
+	 *
+	 * @see java.lang.Runnable#run()
+	 */
+	@Override
+	public void run() {
+		logger.info("Starting user and group sync with ldap service");
+		if (!running.getAndSet(true)) {
+			try {
+				ldapAuthProvider.sync();
+			} catch (Exception e) {
+				logger.error("Failed to synchronize with ldap", e);
+			} finally {
+				running.getAndSet(false);
+			}
+		}
+		logger.info("Finished user and group sync with ldap service");
+	}
+
+	public boolean isReady() {
+		return settings.getBoolean(Keys.realm.ldap.synchronize, false);
+	}
+
+}
diff --git a/src/test/config/test-users.conf b/src/test/config/test-users.conf
index 59b6df4..1d01f84 100644
--- a/src/test/config/test-users.conf
+++ b/src/test/config/test-users.conf
@@ -4,6 +4,11 @@
 	accountType = LOCAL
 	role = "#admin"
 	role = "#notfederated"
+[user "sampleuser"]
+	password = sampleuser
+	cookie = 6e07ed42149fc166206319faffdfba2e2ec82e43
+	accountType = LOCAL
+	role = "#none"
 [team "admins"]
 	role = "#none"
 	accountType = LOCAL
diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index 7b38c18..9fe7312 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -63,7 +63,7 @@
 		GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, GitDaemonTest.class,
 		GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class,
 		FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdAuthenticationTest.class,
-		ModelUtilsTest.class, JnaUtilsTest.class })
+		ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class })
 public class GitBlitSuite {
 
 	public static final File BASEFOLDER = new File("data");
diff --git a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
index 3cd2dc7..e4dc2db 100644
--- a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
+++ b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
@@ -16,23 +16,33 @@
  */
 package com.gitblit.tests;
 
+import java.io.File;
 import java.io.FileInputStream;
 import java.util.HashMap;
 import java.util.Map;
 
+import org.apache.commons.io.FileUtils;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
+import com.gitblit.Constants.AccountType;
 import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
 import com.gitblit.auth.LdapAuthProvider;
+import com.gitblit.manager.IUserManager;
 import com.gitblit.manager.RuntimeManager;
 import com.gitblit.manager.UserManager;
+import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.tests.mock.MemorySettings;
 import com.unboundid.ldap.listener.InMemoryDirectoryServer;
 import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
 import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchScope;
 import com.unboundid.ldif.LDIFReader;
 
 /**
@@ -43,12 +53,22 @@
  *
  */
 public class LdapAuthenticationTest extends GitblitUnitTest {
+    @Rule
+    public TemporaryFolder folder = new TemporaryFolder();
 
 	private static final String RESOURCE_DIR = "src/test/resources/ldap/";
 
-	private LdapAuthProvider ldap;
+    private File usersConf;
+
+    private LdapAuthProvider ldap;
 
 	static int ldapPort = 1389;
+
+	private static InMemoryDirectoryServer ds;
+
+	private IUserManager userManager;
+
+	private MemorySettings settings;
 
 	@BeforeClass
 	public static void createInMemoryLdapServer() throws Exception {
@@ -57,40 +77,45 @@
 		config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", ldapPort));
 		config.setSchema(null);
 
-		InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
-		ds.importFromLDIF(true, new LDIFReader(new FileInputStream(RESOURCE_DIR + "sampledata.ldif")));
+		ds = new InMemoryDirectoryServer(config);
 		ds.startListening();
 	}
 
 	@Before
-	public void newLdapAuthentication() {
-		ldap = newLdapAuthentication(getSettings());
+	public void init() throws Exception {
+		ds.clear();
+		ds.importFromLDIF(true, new LDIFReader(new FileInputStream(RESOURCE_DIR + "sampledata.ldif")));
+		usersConf = folder.newFile("users.conf");
+		FileUtils.copyFile(new File(RESOURCE_DIR + "users.conf"), usersConf);
+		settings = getSettings();
+		ldap = newLdapAuthentication(settings);
 	}
 
-	public LdapAuthProvider newLdapAuthentication(IStoredSettings settings) {
+	private LdapAuthProvider newLdapAuthentication(IStoredSettings settings) {
 		RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
-		UserManager users = new UserManager(runtime).start();
+		userManager = new UserManager(runtime).start();
 		LdapAuthProvider ldap = new LdapAuthProvider();
-		ldap.setup(runtime, users);
+		ldap.setup(runtime, userManager);
 		return ldap;
 	}
 
 	private MemorySettings getSettings() {
 		Map<String, Object> backingMap = new HashMap<String, Object>();
-		backingMap.put("realm.userService", RESOURCE_DIR + "users.conf");
-		backingMap.put("realm.ldap.server", "ldap://localhost:" + ldapPort);
-		backingMap.put("realm.ldap.domain", "");
-		backingMap.put("realm.ldap.username", "cn=Directory Manager");
-		backingMap.put("realm.ldap.password", "password");
-		backingMap.put("realm.ldap.backingUserService", "users.conf");
-		backingMap.put("realm.ldap.maintainTeams", "true");
-		backingMap.put("realm.ldap.accountBase", "OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain");
-		backingMap.put("realm.ldap.accountPattern", "(&(objectClass=person)(sAMAccountName=${username}))");
-		backingMap.put("realm.ldap.groupBase", "OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain");
-		backingMap.put("realm.ldap.groupPattern", "(&(objectClass=group)(member=${dn}))");
-		backingMap.put("realm.ldap.admins", "UserThree @Git_Admins \"@Git Admins\"");
-		backingMap.put("realm.ldap.displayName", "displayName");
-		backingMap.put("realm.ldap.email", "email");
+		backingMap.put(Keys.realm.userService, usersConf.getAbsolutePath());
+		backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + ldapPort);
+//		backingMap.put(Keys.realm.ldap.domain, "");
+		backingMap.put(Keys.realm.ldap.username, "cn=Directory Manager");
+		backingMap.put(Keys.realm.ldap.password, "password");
+//		backingMap.put(Keys.realm.ldap.backingUserService, "users.conf");
+		backingMap.put(Keys.realm.ldap.maintainTeams, "true");
+		backingMap.put(Keys.realm.ldap.accountBase, "OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain");
+		backingMap.put(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+		backingMap.put(Keys.realm.ldap.groupBase, "OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain");
+		backingMap.put(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
+		backingMap.put(Keys.realm.ldap.admins, "UserThree @Git_Admins \"@Git Admins\"");
+		backingMap.put(Keys.realm.ldap.displayName, "displayName");
+		backingMap.put(Keys.realm.ldap.email, "email");
+		backingMap.put(Keys.realm.ldap.uid, "sAMAccountName");
 
 		MemorySettings ms = new MemorySettings(backingMap);
 		return ms;
@@ -162,4 +187,60 @@
 		assertNull(userOneModel);
 	}
 
+	@Test
+	public void checkIfUsersConfContainsAllUsersFromSampleDataLdif() throws Exception {
+		SearchResult searchResult = ds.search("OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain", SearchScope.SUB, "objectClass=person");
+		assertEquals("Number of ldap users in gitblit user model", searchResult.getEntryCount(), countLdapUsersInUserManager());
+	}
+
+	@Test
+	public void addingUserInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
+		ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
+		ldap.sync();
+		assertEquals("Number of ldap users in gitblit user model", 5, countLdapUsersInUserManager());
+	}
+
+	@Test
+	public void addingUserInLdapShouldUpdateGitBlitUsersAndGroups() throws Exception {
+		settings.put(Keys.realm.ldap.synchronize, "true");
+		ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
+		ldap.sync();
+		assertEquals("Number of ldap users in gitblit user model", 6, countLdapUsersInUserManager());
+	}
+
+	@Test
+	public void addingGroupsInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
+		ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
+		ldap.sync();
+		assertEquals("Number of ldap groups in gitblit team model", 0, countLdapTeamsInUserManager());
+	}
+
+	@Test
+	public void addingGroupsInLdapShouldUpdateGitBlitUsersAndGroups() throws Exception {
+		settings.put(Keys.realm.ldap.synchronize, "true");
+		ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
+		ldap.sync();
+		assertEquals("Number of ldap groups in gitblit team model", 1, countLdapTeamsInUserManager());
+	}
+
+	private int countLdapUsersInUserManager() {
+		int ldapAccountCount = 0;
+		for (UserModel userModel : userManager.getAllUsers()) {
+			if (AccountType.LDAP.equals(userModel.accountType)) {
+				ldapAccountCount++;
+			}
+		}
+		return ldapAccountCount;
+	}
+
+	private int countLdapTeamsInUserManager() {
+		int ldapAccountCount = 0;
+		for (TeamModel teamModel : userManager.getAllTeams()) {
+			if (AccountType.LDAP.equals(teamModel.accountType)) {
+				ldapAccountCount++;
+			}
+		}
+		return ldapAccountCount;
+	}
+
 }
diff --git a/src/test/java/com/gitblit/tests/LdapSyncServiceTest.java b/src/test/java/com/gitblit/tests/LdapSyncServiceTest.java
new file mode 100644
index 0000000..eda0903
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/LdapSyncServiceTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2014 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.gitblit.Keys;
+import com.gitblit.service.LdapSyncService;
+import com.gitblit.tests.mock.MemorySettings;
+
+/**
+ * A behavior driven test for the LdapSyncService with in-memory Settings.
+ *
+ * @author Alfred Schmid
+ *
+ */
+public class LdapSyncServiceTest {
+
+	private MemorySettings settings;
+
+	@Before
+	public void init() throws Exception {
+		settings = getSettings();
+	}
+
+	@Test
+	public void defaultOfUnAvailableLdapSynchronizeKeyIsLdapServiceNotReady() {
+		LdapSyncService ldapSyncService = new LdapSyncService(settings, null);
+		assertFalse("When key " + Keys.realm.ldap.synchronize + " is not configured ldap sync is not ready." , ldapSyncService.isReady());
+	}
+
+	@Test
+	public void whenLdapSynchronizeKeyIsFalseLdapServiceNotReady() {
+		LdapSyncService ldapSyncService = new LdapSyncService(settings, null);
+		settings.put(Keys.realm.ldap.synchronize, "false");
+		assertFalse("When key " + Keys.realm.ldap.synchronize + " is configured with value false ldap sync is not ready." , ldapSyncService.isReady());
+	}
+
+	@Test
+	public void whenLdapSynchronizeKeyIsTrueLdapServiceIsReady() {
+		LdapSyncService ldapSyncService = new LdapSyncService(settings, null);
+		settings.put(Keys.realm.ldap.synchronize, "true");
+		assertTrue("When key " + Keys.realm.ldap.synchronize + " is configured with value true ldap sync is not ready." , ldapSyncService.isReady());
+	}
+
+	private MemorySettings getSettings() {
+		Map<String, Object> backingMap = new HashMap<String, Object>();
+		MemorySettings ms = new MemorySettings(backingMap);
+		return ms;
+	}
+
+}
diff --git a/src/test/resources/ldap/addgroup.ldif b/src/test/resources/ldap/addgroup.ldif
new file mode 100644
index 0000000..665df31
--- /dev/null
+++ b/src/test/resources/ldap/addgroup.ldif
@@ -0,0 +1,5 @@
+dn: CN=Git_Group_Without_Members,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: group
+cn: Git_Group_Without_Members
+sAMAccountName: Git_Group_Without_Members
diff --git a/src/test/resources/ldap/adduser.ldif b/src/test/resources/ldap/adduser.ldif
new file mode 100644
index 0000000..b007a94
--- /dev/null
+++ b/src/test/resources/ldap/adduser.ldif
@@ -0,0 +1,11 @@
+dn: CN=UserSix,OU=Canada,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: user
+objectClass: person
+sAMAccountName: UserSix
+userPassword: userSixPassword
+displayName: User Six
+givenName: User
+surname: Six
+personalTitle: Miss
+email: usersix@gitblit.com
+memberOf: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
\ No newline at end of file
diff --git a/src/test/resources/ldap/sampledata.ldif b/src/test/resources/ldap/sampledata.ldif
index df79333..6254f19 100644
--- a/src/test/resources/ldap/sampledata.ldif
+++ b/src/test/resources/ldap/sampledata.ldif
@@ -105,4 +105,16 @@
 surname: Four
 personalTitle: Miss
 email: userfour@gitblit.com
+memberOf: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+dn: CN=UserFive,OU=Canada,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: user
+objectClass: person
+sAMAccountName: UserFive
+userPassword: userFivePassword
+displayName: User Five
+givenName: User
+surname: Five
+personalTitle: Miss
+email: userfive@gitblit.com
 memberOf: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
\ No newline at end of file
diff --git a/src/test/resources/ldap/users.conf b/src/test/resources/ldap/users.conf
index b4b4a6e..7d1e319 100644
--- a/src/test/resources/ldap/users.conf
+++ b/src/test/resources/ldap/users.conf
@@ -7,10 +7,17 @@
 [user "userthree"]
 	password = "#externalAccount"
 	cookie = d7d3894fc517612aa6c595555b6e1ab8e147e597
-	displayName = User Three
+	displayName = Mrs. User Three
 	emailAddress = userthree@gitblit.com
 	accountType = LDAP
 	role = "#admin"
+[user "userfive"]
+	password = "#externalAccount"
+	cookie = 220bafef069b8b399b2597644015b6b0f4667982
+	displayName = Miss. User Five
+	emailAddress = userfive@gitblit.com
+	accountType = LDAP
+	role = "#none"
 [user "userone"]
 	password = "#externalAccount"
 	cookie = c97cd38e50858cd0b389ec61b18fb9a89b4da54c
@@ -21,7 +28,7 @@
 [user "usertwo"]
 	password = "#externalAccount"
 	cookie = 498ca9bd2841d39050fa45d1d737b9f9f767858d
-	displayName = User Two
+	displayName = Mr. User Two
 	emailAddress = usertwo@gitblit.com
 	accountType = LDAP
 	role = "#admin"
@@ -37,6 +44,13 @@
 	cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
 	accountType = LOCAL
 	role = "#create"
+[user "userfour"]
+	password = "#externalAccount"
+	cookie = d4fca47ebc70f0495947f8b6158ac6edb546e2fe
+	displayName = Miss. User Four
+	emailAddress = userfour@gitblit.com
+	accountType = LDAP
+	role = "#none"
 [team "Git_Admins"]
 	role = "#none"
 	accountType = LOCAL
@@ -47,6 +61,7 @@
 	user = userone
 	user = usertwo
 	user = userthree
+	user = userfour
 [team "Git Admins"]
 	role = "#none"
 	accountType = LOCAL

--
Gitblit v1.9.1