summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.classpath1
-rw-r--r--NOTICE10
-rw-r--r--distrib/gitblit.properties85
-rw-r--r--docs/01_setup.mkd61
-rw-r--r--docs/04_design.mkd1
-rw-r--r--docs/ldapSample.pngbin0 -> 34151 bytes
-rw-r--r--src/com/gitblit/ConfigUserService.java25
-rw-r--r--src/com/gitblit/FileUserService.java27
-rw-r--r--src/com/gitblit/GitBlit.java16
-rw-r--r--src/com/gitblit/GitBlitServer.java42
-rw-r--r--src/com/gitblit/GitblitUserService.java47
-rw-r--r--src/com/gitblit/IStoredSettings.java18
-rw-r--r--src/com/gitblit/IUserService.java16
-rw-r--r--src/com/gitblit/LdapUserService.java268
-rw-r--r--src/com/gitblit/build/Build.java6
-rw-r--r--src/com/gitblit/utils/ConnectionUtils.java89
-rw-r--r--src/com/gitblit/utils/StringUtils.java60
-rw-r--r--src/com/gitblit/wicket/GitBlitWebApp.properties4
-rw-r--r--src/com/gitblit/wicket/pages/BasePage.java6
-rw-r--r--src/com/gitblit/wicket/pages/ChangePasswordPage.java6
-rw-r--r--src/com/gitblit/wicket/pages/EditTeamPage.java5
-rw-r--r--src/com/gitblit/wicket/pages/EditUserPage.java82
-rw-r--r--src/com/gitblit/wicket/panels/UsersPanel.java3
-rw-r--r--tests/com/gitblit/tests/LdapUserServiceTest.java105
-rw-r--r--tests/com/gitblit/tests/StringUtilsTest.java25
-rw-r--r--tests/com/gitblit/tests/mock/MemorySettings.java50
-rw-r--r--tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif88
27 files changed, 1079 insertions, 67 deletions
diff --git a/.classpath b/.classpath
index b0f1f866..f636f49c 100644
--- a/.classpath
+++ b/.classpath
@@ -30,5 +30,6 @@
<classpathentry kind="lib" path="ext/org.eclipse.jgit.http.server-1.3.0.201202151440-r.jar" sourcepath="ext/org.eclipse.jgit.http.server-1.3.0.201202151440-r-sources.jar"/>
<classpathentry kind="lib" path="ext/lucene-highlighter-3.5.0.jar" sourcepath="ext/lucene-highlighter-3.5.0-sources.jar"/>
<classpathentry kind="lib" path="ext/lucene-memory-3.5.0.jar" sourcepath="ext/lucene-memory-3.5.0-sources.jar"/>
+ <classpathentry kind="lib" path="ext/unboundid-ldapsdk-2.3.0.jar" sourcepath="ext/unboundid-ldapsdk-2.3.0-sources.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/NOTICE b/NOTICE
index f394d86f..022ddd78 100644
--- a/NOTICE
+++ b/NOTICE
@@ -214,4 +214,12 @@ GLYHPICONS
Creative Commons CC-BY License.
http://glyphicons.com
- \ No newline at end of file
+
+---------------------------------------------------------------------------
+UnboundID
+---------------------------------------------------------------------------
+ UnboundID, released under the
+ GNU LESSER GENERAL PUBLIC LICENSE. (http://www.unboundid.com/products/ldap-sdk/docs/LICENSE-LGPLv2.1.txt)
+
+ http://www.unboundid.com
+ \ No newline at end of file
diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index 2846496e..527b7268 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -136,6 +136,91 @@ realm.passwordStorage = md5
# SINCE 0.5.0
realm.minPasswordLength = 5
+# URL of the LDAP server.
+#
+# SINCE 1.0.0
+realm.ldap.server = ldap://localhost
+
+# Login username for LDAP searches.
+# The domain prefix may be omitted if it matches the domain specified in
+# *realm.ldap.domain*. If this value is unspecified, anonymous LDAP login will
+# be used.
+#
+# e.g. mydomain\\username
+#
+# SINCE 1.0.0
+realm.ldap.username = cn=Directory Manager
+
+# Login password for LDAP searches.
+#
+# SINCE 1.0.0
+realm.ldap.password = password
+
+# The LdapUserService must be backed by another user service for standard user
+# and team management.
+# default: users.conf
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+realm.ldap.backingUserService = users.conf
+
+# Delegate team membership control to LDAP.
+#
+# If true, team user memberships will be specified by LDAP groups. This will
+# disable team selection in Edit User and user selection in Edit Team.
+#
+# If false, LDAP will only be used for authentication and Gitblit will maintain
+# team memberships with the *realm.ldap.backingUserService*.
+#
+# SINCE 1.0.0
+realm.ldap.maintainTeams = false
+
+# Root node that all Users sit under in LDAP
+#
+# This is the root node that searches for user information will begin from in LDAP
+# If blank, it will search ALL of ldap.
+#
+# SINCE 1.0.0
+realm.ldap.accountBase = OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+# Filter Criteria for Users in LDAP
+#
+# Query pattern to use when searching for a user account. This may be any valid
+# LDAP query expression, including the standard (&) and (|) operators. Variables may
+# be injected via the ${variableName} syntax. Recognized variables are:
+# ${username} - The text entered as the user name
+#
+# SINCE 1.0.0
+realm.ldap.accountPattern = (&(objectClass=person)(sAMAccountName=${username}))
+
+# Root node that all Teams sit under in LDAP
+#
+# This is the node that searches for team information will begin from in LDAP
+# If blank, it will search ALL of ldap.
+#
+# SINCE 1.0.0
+realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+# Filter Criteria for Teams in LDAP
+#
+# Query pattern to use when searching for a team. This may be any valid
+# LDAP query expression, including the standard (&) and (|) operators. Variables may
+# be injected via the ${variableName} syntax. Recognized variables are:
+# ${username} - The text entered as the user name
+# ${dn} - The Distinguished Name of the user logged in
+# All attributes on the User's record are also passed in. For example, if a user has an
+# attribute "fullName" set to "John", "(fn=${fullName})" will be translated to "(fn=John)".
+#
+# SINCE 1.0.0
+realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn}))
+
+# Users and or teams that are Admins, read from LDAP
+#
+# This is a space delimited list. If it starts with @, it indicates a Team Name
+#
+# SINCE 1.0.0
+realm.ldap.admins= @Git_Admins
+
#
# Gitblit Web Settings
#
diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd
index 75b51419..c2e2ef11 100644
--- a/docs/01_setup.mkd
+++ b/docs/01_setup.mkd
@@ -447,4 +447,63 @@ Nothing special to configure, EGit figures out everything.
<pre>https://yourserver/git/your/repository</pre>
- **Command-line Git**
My testing indicates that your username must be embedded in the url. YMMV.
-<pre>https://username@yourserver/git/your/repository</pre> \ No newline at end of file
+<pre>https://username@yourserver/git/your/repository</pre>
+
+## LDAP Support
+*SINCE 1.0.0*
+
+LDAP can be used with Gitblit to read Users and the Teams that they belong to. If configured, LDAP will be queried upon every login to the system, and synchronize that information with the traditional Gitblit backed file (.conf or .properties). This "lazy" reading approach provides for fast reaction times, but will force a user to log in before you can maintain them (or their teams).
+
+### Example Diagram (with attributes)
+![block diagram](ldapSample.png "LDAP Sample")
+
+Please see <gitblit>/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif to see the data in LDAP that reflects the above picture.
+
+### GitBlit Properties (See gitblit.properties for full description)
+The following is are descriptions of the properties that would follow the sample layout of an LDAP (or Active Directory) setup above.
+
+<table border="1" cellpadding="1" cellspacing="1">
+<tr>
+ <td>realm.ldap.server</td><td>ldap://localhost:389</td>
+ <td>Tells Gitblit to connect to the LDAP server on localhost, port 389. URL Must be of form ldap(s)://<server>:<port> with port being optional (389 for ldap, 636 for ldaps).</td>
+</tr>
+<tr>
+ <td>realm.ldap.username</td><td>cn=Directory Manager</td>
+ <td>The credentials that will log into this gitblit server</td>
+</tr>
+<tr>
+ <td>realm.ldap.password</td><td>password</td>
+ <td>The credentials that will log into this gitblit server</td>
+</tr>
+<tr>
+ <td>realm.ldap.backingUserService</td><td>users.conf</td>
+ <td>Where to store all information that is used by Gitblit. All information will be synced here upon user login.</td>
+</tr>
+<tr>
+ <td>realm.ldap.maintainTeams</td><td>true</td>
+ <td>Are users maintained in LDAP (true), or manually in Gitblit (false).</td>
+</tr>
+<tr>
+ <td>realm.ldap.accountBase</td><td>OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain</td>
+ <td>What is the root node for all users in this LDAP system. Searches will be subtree searches starting from this node.</td>
+</tr>
+<tr>
+ <td>realm.ldap.accountPattern</td><td>(&(objectClass=person)(sAMAccountName=${username}))</td><td>The LDAP Search filter that will match a particular user in LDAP. ${username} will be replaced with whatever the user types in as their user name.</td>
+</tr>
+<tr>
+ <td>realm.ldap.groupBase</td><td>OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain</td>
+ <td>What is the root node for all teams in this LDAP system. Searches will be subtree searches starting from this node.</td>
+</tr>
+<tr>
+ <td>realm.ldap.groupMemberPattern</td><td>(&(objectClass=group)(member=${dn}))</td><td>The LDAP Search filter that will match all teams for the logging in user in LDAP. ${username} will be replaced with whatever the user types in as their user name. Anything else in ${} will be replaced by Attributes on the User node.</td>
+</tr>
+<tr>
+ <td>realm.ldap.admins</td><td>@Git_Admins</td><td>A space delimited list of users and teams (if starting with @) that indicate admin status in Gitblit.</td>
+</tr>
+</table>
+
+You may notice that there are no properties to find the password on the User record. This is intentional, and the service utilizes the LDAP login process to verify that the user credentials are correct.
+
+You can also start Gitblit GO with an in-memory (backed by an LDIF file) LDAP server by using the --ldapLdifFile property. It will listen where ever gitblit.settings is pointed to. However, it only supports ldap...not ldaps, so be sure to set that in gitblit.settings. It reads the user / password in gitblit.settings to create the root user login.
+
+Finally, writing back to LDAP is not implemented at this time, so do not worry about corrupting your corporate LDAP. Many orgnizations are likely to go through a different flow to update their LDAP, so it's unlikely that this will become a feature. \ No newline at end of file
diff --git a/docs/04_design.mkd b/docs/04_design.mkd
index a439edf5..64dce43c 100644
--- a/docs/04_design.mkd
+++ b/docs/04_design.mkd
@@ -38,6 +38,7 @@ The following dependencies are automatically downloaded by Gitblit GO (or alread
- [javamail](http://kenai.com/projects/javamail) (CDDL-1.0, BSD, GPL-2.0, GNU-Classpath)
- [Groovy](http://groovy.codehaus.org) (Apache 2.0)
- [Lucene](http://lucene.apache.org) (Apache 2.0)
+- [UnboundID](http://www.unboundid.com) (LGPL 2.1)
### Other Build Dependencies
- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)
diff --git a/docs/ldapSample.png b/docs/ldapSample.png
new file mode 100644
index 00000000..fd8c999a
--- /dev/null
+++ b/docs/ldapSample.png
Binary files differ
diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java
index 8f47f7a0..828ba762 100644
--- a/src/com/gitblit/ConfigUserService.java
+++ b/src/com/gitblit/ConfigUserService.java
@@ -100,6 +100,27 @@ public class ConfigUserService implements IUserService {
}
/**
+ * Does the user service support changes to credentials?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsCredentialChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support changes to team memberships?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return true;
+ }
+
+ /**
* Does the user service support cookie authentication?
*
* @return true or false
@@ -656,7 +677,9 @@ public class ConfigUserService implements IUserService {
// write users
for (UserModel model : users.values()) {
- config.setString(USER, model.username, PASSWORD, model.password);
+ if (!StringUtils.isEmpty(model.password)) {
+ config.setString(USER, model.username, PASSWORD, model.password);
+ }
// user roles
List<String> roles = new ArrayList<String>();
diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java
index 7842c31d..b8d4a40e 100644
--- a/src/com/gitblit/FileUserService.java
+++ b/src/com/gitblit/FileUserService.java
@@ -74,6 +74,27 @@ public class FileUserService extends FileSettings implements IUserService {
}
/**
+ * Does the user service support changes to credentials?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsCredentialChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support changes to team memberships?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return true;
+ }
+
+ /**
* Does the user service support cookie authentication?
*
* @return true or false
@@ -233,7 +254,9 @@ public class FileUserService extends FileSettings implements IUserService {
}
StringBuilder sb = new StringBuilder();
- sb.append(model.password);
+ if (!StringUtils.isEmpty(model.password)) {
+ sb.append(model.password);
+ }
sb.append(',');
for (String role : roles) {
sb.append(role);
@@ -658,6 +681,8 @@ public class FileUserService extends FileSettings implements IUserService {
team.addRepositories(repositories);
team.addUsers(users);
team.addMailingLists(mailingLists);
+ team.preReceiveScripts.addAll(preReceive);
+ team.postReceiveScripts.addAll(postReceive);
teams.put(team.name.toLowerCase(), team);
} else {
// user definition
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index bfe93d09..2b7ba3c1 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -377,6 +377,22 @@ public class GitBlit implements ServletContextListener {
this.userService = userService;
this.userService.setup(settings);
}
+
+ /**
+ *
+ * @return true if the user service supports credential changes
+ */
+ public boolean supportsCredentialChanges() {
+ return userService.supportsCredentialChanges();
+ }
+
+ /**
+ *
+ * @return true if the user service supports team membership changes
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return userService.supportsTeamMembershipChanges();
+ }
/**
* Authenticate a user based on a username and password.
diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java
index ce0d1fbb..1307bc3d 100644
--- a/src/com/gitblit/GitBlitServer.java
+++ b/src/com/gitblit/GitBlitServer.java
@@ -23,12 +23,14 @@ import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
+import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.ProtectionDomain;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
+import java.util.Scanner;
import org.eclipse.jetty.ajp.Ajp13SocketConnector;
import org.eclipse.jetty.server.Connector;
@@ -50,6 +52,10 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.gitblit.utils.StringUtils;
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldif.LDIFReader;
/**
* GitBlitServer is the embedded Jetty server for Gitblit GO. This class starts
@@ -268,6 +274,39 @@ public class GitBlitServer {
// Override settings from the command-line
settings.overrideSetting(Keys.realm.userService, params.userService);
settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder);
+
+ // Start up an in-memory LDAP server, if configured
+ try {
+ if (StringUtils.isEmpty(params.ldapLdifFile) == false) {
+ File ldifFile = new File(params.ldapLdifFile);
+ if (ldifFile != null && ldifFile.exists()) {
+ URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap_server));
+ String firstLine = new Scanner(ldifFile).nextLine();
+ String rootDN = firstLine.substring(4);
+ String bindUserName = settings.getString(Keys.realm.ldap_username, "");
+ String bindPassword = settings.getString(Keys.realm.ldap_password, "");
+
+ // Get the port
+ int port = ldapUrl.getPort();
+ if (port == -1)
+ port = 389;
+
+ InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(rootDN);
+ config.addAdditionalBindCredentials(bindUserName, bindPassword);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", port));
+ config.setSchema(null);
+
+ InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
+ ds.importFromLDIF(true, new LDIFReader(ldifFile));
+ ds.startListening();
+
+ logger.info("LDAP Server started at ldap://localhost:" + port);
+ }
+ }
+ } catch (Exception e) {
+ // Completely optional, just show a warning
+ logger.warn("Unable to start LDAP server", e);
+ }
// Set the server's contexts
server.setHandler(rootContext);
@@ -506,6 +545,9 @@ public class GitBlitServer {
*/
@Parameter(names = { "--settings" }, description = "Path to alternative settings")
public String settingsfile;
+
+ @Parameter(names = { "--ldapLdifFile" }, description = "Path to LDIF file. This will cause an in-memory LDAP server to be started according to gitblit settings")
+ public String ldapLdifFile;
}
} \ No newline at end of file
diff --git a/src/com/gitblit/GitblitUserService.java b/src/com/gitblit/GitblitUserService.java
index 7462af08..ddb3ca77 100644
--- a/src/com/gitblit/GitblitUserService.java
+++ b/src/com/gitblit/GitblitUserService.java
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
+import com.gitblit.utils.DeepCopier;
/**
* This class wraps the default user service and is recommended as the starting
@@ -112,6 +113,16 @@ public class GitblitUserService implements IUserService {
}
@Override
+ public boolean supportsCredentialChanges() {
+ return serviceImpl.supportsCredentialChanges();
+ }
+
+ @Override
+ public boolean supportsTeamMembershipChanges() {
+ return serviceImpl.supportsTeamMembershipChanges();
+ }
+
+ @Override
public boolean supportsCookies() {
return serviceImpl.supportsCookies();
}
@@ -143,9 +154,33 @@ public class GitblitUserService implements IUserService {
@Override
public boolean updateUserModel(String username, UserModel model) {
- return serviceImpl.updateUserModel(username, model);
+ if (supportsCredentialChanges()) {
+ if (!supportsTeamMembershipChanges()) {
+ // teams are externally controlled - copy from original model
+ UserModel existingModel = getUserModel(username);
+
+ model = DeepCopier.copy(model);
+ model.teams.clear();
+ model.teams.addAll(existingModel.teams);
+ }
+ return serviceImpl.updateUserModel(username, model);
+ }
+ if (model.username.equals(username)) {
+ // passwords are not persisted by the backing user service
+ model.password = null;
+ if (!supportsTeamMembershipChanges()) {
+ // teams are externally controlled- copy from original model
+ UserModel existingModel = getUserModel(username);
+
+ model = DeepCopier.copy(model);
+ model.teams.clear();
+ model.teams.addAll(existingModel.teams);
+ }
+ return serviceImpl.updateUserModel(username, model);
+ }
+ logger.error("Users can not be renamed!");
+ return false;
}
-
@Override
public boolean deleteUserModel(UserModel model) {
return serviceImpl.deleteUserModel(model);
@@ -198,6 +233,14 @@ public class GitblitUserService implements IUserService {
@Override
public boolean updateTeamModel(String teamname, TeamModel model) {
+ if (!supportsTeamMembershipChanges()) {
+ // teams are externally controlled - copy from original model
+ TeamModel existingModel = getTeamModel(teamname);
+
+ model = DeepCopier.copy(model);
+ model.users.clear();
+ model.users.addAll(existingModel.users);
+ }
return serviceImpl.updateTeamModel(teamname, model);
}
diff --git a/src/com/gitblit/IStoredSettings.java b/src/com/gitblit/IStoredSettings.java
index 2d8b6055..2f45f09d 100644
--- a/src/com/gitblit/IStoredSettings.java
+++ b/src/com/gitblit/IStoredSettings.java
@@ -157,6 +157,24 @@ public abstract class IStoredSettings {
}
return defaultValue;
}
+
+ /**
+ * Returns the string value for the specified key. If the key does not
+ * exist an exception is thrown.
+ *
+ * @param key
+ * @return key value
+ */
+ public String getRequiredString(String name) {
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ String value = props.getProperty(name);
+ if (value != null) {
+ return value.trim();
+ }
+ }
+ throw new RuntimeException("Property (" + name + ") does not exist");
+ }
/**
* Returns a list of space-separated strings from the specified key.
diff --git a/src/com/gitblit/IUserService.java b/src/com/gitblit/IUserService.java
index a5e04e3e..334bbedd 100644
--- a/src/com/gitblit/IUserService.java
+++ b/src/com/gitblit/IUserService.java
@@ -40,6 +40,22 @@ public interface IUserService {
void setup(IStoredSettings settings);
/**
+ * Does the user service support changes to credentials?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ boolean supportsCredentialChanges();
+
+ /**
+ * Does the user service support changes to team memberships?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ boolean supportsTeamMembershipChanges();
+
+ /**
* Does the user service support cookie authentication?
*
* @return true or false
diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java
new file mode 100644
index 00000000..86b61364
--- /dev/null
+++ b/src/com/gitblit/LdapUserService.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2012 John Crygier
+ * Copyright 2012 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;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.unboundid.ldap.sdk.Attribute;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPSearchException;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+
+/**
+ * Implementation of an LDAP user service.
+ *
+ * @author John Crygier
+ */
+public class LdapUserService extends GitblitUserService {
+
+ public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
+
+ private IStoredSettings settings;
+
+ public LdapUserService() {
+ super();
+ }
+
+ @Override
+ public void setup(IStoredSettings settings) {
+ this.settings = settings;
+ String file = settings.getString(Keys.realm.ldap_backingUserService, "users.conf");
+ File realmFile = GitBlit.getFileOrFolder(file);
+
+ serviceImpl = createUserService(realmFile);
+ logger.info("LDAP User Service backed by " + serviceImpl.toString());
+ }
+
+ private LDAPConnection getLdapConnection() {
+ try {
+ URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap_server));
+ String bindUserName = settings.getString(Keys.realm.ldap_username, "");
+ String bindPassword = settings.getString(Keys.realm.ldap_password, "");
+ int ldapPort = ldapUrl.getPort();
+
+ if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { // SSL
+ if (ldapPort == -1) // Default Port
+ ldapPort = 636;
+
+ SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+ return new LDAPConnection(sslUtil.createSSLSocketFactory(), ldapUrl.getHost(), ldapPort, bindUserName, bindPassword);
+ } else {
+ if (ldapPort == -1) // Default Port
+ ldapPort = 389;
+
+ return new LDAPConnection(ldapUrl.getHost(), ldapPort, bindUserName, bindPassword);
+ }
+ } catch (URISyntaxException e) {
+ logger.error("Bad LDAP URL, should be in the form: ldap(s)://<server>:<port>", e);
+ } catch (GeneralSecurityException e) {
+ logger.error("Unable to create SSL Connection", e);
+ } catch (LDAPException e) {
+ logger.error("Error Connecting to LDAP", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * Credentials are defined in the LDAP server and can not be manipulated
+ * from Gitblit.
+ *
+ * @return false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsCredentialChanges() {
+ return false;
+ }
+
+ /**
+ * If the LDAP server will maintain team memberships then LdapUserService
+ * will not allow team membership changes. In this scenario all team
+ * changes must be made on the LDAP server by the LDAP administrator.
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return !settings.getBoolean(Keys.realm.ldap_maintainTeams, false);
+ }
+
+ /**
+ * Does the user service support cookie authentication?
+ *
+ * @return true or false
+ */
+ @Override
+ public boolean supportsCookies() {
+ // TODO cookies need to be reviewed
+ return false;
+ }
+
+ @Override
+ public UserModel authenticate(String username, char[] password) {
+ String simpleUsername = getSimpleUsername(username);
+
+ LDAPConnection ldapConnection = getLdapConnection();
+ if (ldapConnection != null) {
+ // Find the logging in user's DN
+ String accountBase = settings.getString(Keys.realm.ldap_accountBase, "");
+ String accountPattern = settings.getString(Keys.realm.ldap_accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+ accountPattern = StringUtils.replace(accountPattern, "${username}", simpleUsername);
+
+ SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
+ if (result != null && result.getEntryCount() == 1) {
+ SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
+ String loggingInUserDN = loggingInUser.getDN();
+
+ if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
+ logger.debug("Authenitcated: " + username);
+
+ UserModel user = getUserModel(simpleUsername);
+ if (user == null) // create user object for new authenticated user
+ user = createUserFromLdap(simpleUsername, loggingInUser);
+
+ user.password = "StoredInLDAP";
+
+ if (!supportsTeamMembershipChanges())
+ getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);
+
+ // Get Admin Attributes
+ setAdminAttribute(user);
+
+ // Push the ldap looked up values to backing file
+ super.updateUserModel(user);
+ if (!supportsTeamMembershipChanges()) {
+ for (TeamModel userTeam : user.teams)
+ updateTeamModel(userTeam);
+ }
+
+ return user;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void setAdminAttribute(UserModel user) {
+ user.canAdmin = false;
+ List<String> admins = settings.getStrings(Keys.realm.ldap_admins);
+ for (String admin : admins) {
+ if (admin.startsWith("@")) { // Team
+ if (user.getTeam(admin.substring(1)) != null)
+ user.canAdmin = true;
+ } else
+ if (user.getName().equalsIgnoreCase(admin))
+ user.canAdmin = true;
+ }
+ }
+
+ private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
+ String loggingInUserDN = loggingInUser.getDN();
+
+ user.teams.clear(); // Clear the users team memberships - we're going to get them from LDAP
+ String groupBase = settings.getString(Keys.realm.ldap_groupBase, "");
+ String groupMemberPattern = settings.getString(Keys.realm.ldap_groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
+
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", loggingInUserDN);
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", simpleUsername);
+
+ // Fill in attributes into groupMemberPattern
+ for (Attribute userAttribute : loggingInUser.getAttributes())
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", userAttribute.getValue());
+
+ SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, groupMemberPattern);
+ if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
+ for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
+ SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
+ String teamName = teamEntry.getAttribute("cn").getValue();
+
+ TeamModel teamModel = getTeamModel(teamName);
+ if (teamModel == null)
+ teamModel = createTeamFromLdap(teamEntry);
+
+ user.teams.add(teamModel);
+ teamModel.addUser(user.getName());
+ }
+ }
+ }
+
+ private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
+ TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
+ // If attributes other than team name ever from from LDAP, this is where to get them
+
+ return answer;
+ }
+
+ private UserModel createUserFromLdap(String simpleUserName, SearchResultEntry userEntry) {
+ UserModel answer = new UserModel(simpleUserName);
+ //If attributes other than user name ever from from LDAP, this is where to get them
+
+ return answer;
+ }
+
+ private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) {
+ try {
+ return ldapConnection.search(base, SearchScope.SUB, filter);
+ } catch (LDAPSearchException e) {
+ logger.error("Problem Searching LDAP", e);
+
+ return null;
+ }
+ }
+
+ private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) {
+ try {
+ ldapConnection.bind(userDn, password);
+ return true;
+ } catch (LDAPException e) {
+ logger.error("Error authenitcating user", e);
+ return false;
+ }
+ }
+
+
+ /**
+ * Returns a simple username without any domain prefixes.
+ *
+ * @param username
+ * @return a simple username
+ */
+ protected String getSimpleUsername(String username) {
+ int lastSlash = username.lastIndexOf('\\');
+ if (lastSlash > -1) {
+ username = username.substring(lastSlash + 1);
+ }
+ return username;
+ }
+}
diff --git a/src/com/gitblit/build/Build.java b/src/com/gitblit/build/Build.java
index 96f848b4..cbe09a9c 100644
--- a/src/com/gitblit/build/Build.java
+++ b/src/com/gitblit/build/Build.java
@@ -94,6 +94,7 @@ public class Build {
downloadFromApache(MavenObject.LUCENE, BuildType.RUNTIME);
downloadFromApache(MavenObject.LUCENE_HIGHLIGHTER, BuildType.RUNTIME);
downloadFromApache(MavenObject.LUCENE_MEMORY, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.UNBOUND_ID, BuildType.RUNTIME);
downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.RUNTIME);
@@ -124,6 +125,7 @@ public class Build {
downloadFromApache(MavenObject.LUCENE, BuildType.COMPILETIME);
downloadFromApache(MavenObject.LUCENE_HIGHLIGHTER, BuildType.COMPILETIME);
downloadFromApache(MavenObject.LUCENE_MEMORY, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.UNBOUND_ID, BuildType.COMPILETIME);
downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME);
downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME);
@@ -524,6 +526,10 @@ public class Build {
public static final MavenObject LUCENE_MEMORY = new MavenObject("lucene memory", "org/apache/lucene", "lucene-memory",
"3.5.0", 30000, 23000, 0, "7908e954e8c1b4b2463aa712b34fa4a5612e241d",
"69b19b38d78cc3b27ea5542a14f0ebbb1625ffdd", "");
+
+ public static final MavenObject UNBOUND_ID = new MavenObject("unbound id", "com/unboundid", "unboundid-ldapsdk",
+ "2.3.0", 1383417, 1439721, 0, "6fde8d9fb4ee3e7e3d7e764e3ea57195971e2eb2",
+ "5276d3d29630693dba99ab9f7ea54f4c471d3af1", "");
public final String name;
diff --git a/src/com/gitblit/utils/ConnectionUtils.java b/src/com/gitblit/utils/ConnectionUtils.java
index 9ad62d0e..f0b41118 100644
--- a/src/com/gitblit/utils/ConnectionUtils.java
+++ b/src/com/gitblit/utils/ConnectionUtils.java
@@ -16,16 +16,22 @@
package com.gitblit.utils;
import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
+import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
@@ -87,6 +93,89 @@ public class ConnectionUtils {
}
return conn;
}
+
+ // Copyright (C) 2009 The Android Open Source Project
+ //
+ // 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.
+ public static class BlindSSLSocketFactory extends SSLSocketFactory {
+ private static final BlindSSLSocketFactory INSTANCE;
+
+ static {
+ try {
+ final SSLContext context = SSLContext.getInstance("SSL");
+ final TrustManager[] trustManagers = { new DummyTrustManager() };
+ final SecureRandom rng = new SecureRandom();
+ context.init(null, trustManagers, rng);
+ INSTANCE = new BlindSSLSocketFactory(context.getSocketFactory());
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Cannot create BlindSslSocketFactory", e);
+ }
+ }
+
+ public static SocketFactory getDefault() {
+ return INSTANCE;
+ }
+
+ private final SSLSocketFactory sslFactory;
+
+ private BlindSSLSocketFactory(final SSLSocketFactory sslFactory) {
+ this.sslFactory = sslFactory;
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+ throws IOException {
+ return sslFactory.createSocket(s, host, port, autoClose);
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return sslFactory.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return sslFactory.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return sslFactory.createSocket();
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException,
+ UnknownHostException {
+ return sslFactory.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return sslFactory.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost,
+ int localPort) throws IOException, UnknownHostException {
+ return sslFactory.createSocket(host, port, localHost, localPort);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port,
+ InetAddress localAddress, int localPort) throws IOException {
+ return sslFactory.createSocket(address, port, localAddress, localPort);
+ }
+ }
/**
* DummyTrustManager trusts all certificates.
diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java
index d6441829..2c357241 100644
--- a/src/com/gitblit/utils/StringUtils.java
+++ b/src/com/gitblit/utils/StringUtils.java
@@ -327,20 +327,24 @@ public class StringUtils {
* @return list of strings
*/
public static List<String> getStringsFromValue(String value, String separator) {
- List<String> strings = new ArrayList<String>();
- try {
- String[] chunks = value.split(separator);
- for (String chunk : chunks) {
- chunk = chunk.trim();
- if (chunk.length() > 0) {
- strings.add(chunk);
- }
- }
- } catch (PatternSyntaxException e) {
- throw new RuntimeException(e);
- }
- return strings;
- }
+ List<String> strings = new ArrayList<String>();
+ try {
+ String[] chunks = value.split(separator + "(?=([^\"]*\"[^\"]*\")*[^\"]*$)");
+ for (String chunk : chunks) {
+ chunk = chunk.trim();
+ if (chunk.length() > 0) {
+ if (chunk.charAt(0) == '"' && chunk.charAt(chunk.length() - 1) == '"') {
+ // strip double quotes
+ chunk = chunk.substring(1, chunk.length() - 1).trim();
+ }
+ strings.add(chunk);
+ }
+ }
+ } catch (PatternSyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ return strings;
+ }
/**
* Validates that a name is composed of letters, digits, or limited other
@@ -518,4 +522,32 @@ public class StringUtils {
}
return "";
}
+
+ /**
+ * Replace all occurences of a substring within a string with
+ * another string.
+ *
+ * From Spring StringUtils.
+ *
+ * @param inString String to examine
+ * @param oldPattern String to replace
+ * @param newPattern String to insert
+ * @return a String with the replacements
+ */
+ public static String replace(String inString, String oldPattern, String newPattern) {
+ StringBuilder sb = new StringBuilder();
+ int pos = 0; // our position in the old string
+ int index = inString.indexOf(oldPattern);
+ // the index of an occurrence we've found, or -1
+ int patLen = oldPattern.length();
+ while (index >= 0) {
+ sb.append(inString.substring(pos, index));
+ sb.append(newPattern);
+ pos = index + patLen;
+ index = inString.indexOf(oldPattern, pos);
+ }
+ sb.append(inString.substring(pos));
+ // remember to append any characters to the right of a match
+ return sb.toString();
+ }
} \ No newline at end of file
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index e73addc8..295db8a3 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -270,4 +270,6 @@ gb.noProposals = Sorry, {0} is not accepting proposals at this time.
gb.noFederation = Sorry, {0} is not configured to federate with any Gitblit instances.
gb.proposalFailed = Sorry, {0} did not receive any proposal data!
gb.proposalError = Sorry, {0} reports that an unexpected error occurred!
-gb.failedToSendProposal = Failed to send proposal! \ No newline at end of file
+gb.failedToSendProposal = Failed to send proposal!
+gb.userServiceDoesNotPermitAddUser = {0} does not permit adding a user account!
+gb.userServiceDoesNotPermitPasswordChanges = {0} does not permit password changes! \ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java
index 3852818a..94ed6334 100644
--- a/src/com/gitblit/wicket/pages/BasePage.java
+++ b/src/com/gitblit/wicket/pages/BasePage.java
@@ -254,9 +254,11 @@ public abstract class BasePage extends WebPage {
add(new Label("username", GitBlitWebSession.get().getUser().toString() + ":"));
add(new LinkPanel("loginLink", null, markupProvider.getString("gb.logout"),
LogoutPage.class));
+ boolean editCredentials = GitBlit.self().supportsCredentialChanges();
// quick and dirty hack for showing a separator
- add(new Label("separator", "|"));
- add(new BookmarkablePageLink<Void>("changePasswordLink", ChangePasswordPage.class));
+ add(new Label("separator", "|").setVisible(editCredentials));
+ add(new BookmarkablePageLink<Void>("changePasswordLink",
+ ChangePasswordPage.class).setVisible(editCredentials));
} else {
// login
add(new Label("username").setVisible(false));
diff --git a/src/com/gitblit/wicket/pages/ChangePasswordPage.java b/src/com/gitblit/wicket/pages/ChangePasswordPage.java
index 4fb5d237..cbe732ff 100644
--- a/src/com/gitblit/wicket/pages/ChangePasswordPage.java
+++ b/src/com/gitblit/wicket/pages/ChangePasswordPage.java
@@ -50,6 +50,12 @@ public class ChangePasswordPage extends RootSubPage {
// no authentication enabled
throw new RestartResponseException(getApplication().getHomePage());
}
+
+ if (!GitBlit.self().supportsCredentialChanges()) {
+ error(MessageFormat.format(getString("gb.userServiceDoesNotPermitPasswordChanges"),
+ GitBlit.getString(Keys.realm.userService, "users.conf")), true);
+ }
+
setupPage(getString("gb.changePassword"), GitBlitWebSession.get().getUser().username);
StatelessForm<Void> form = new StatelessForm<Void>("passwordForm") {
diff --git a/src/com/gitblit/wicket/pages/EditTeamPage.java b/src/com/gitblit/wicket/pages/EditTeamPage.java
index 890ea8f7..96bd188f 100644
--- a/src/com/gitblit/wicket/pages/EditTeamPage.java
+++ b/src/com/gitblit/wicket/pages/EditTeamPage.java
@@ -217,9 +217,12 @@ public class EditTeamPage extends RootSubPage {
// do not let the browser pre-populate these fields
form.add(new SimpleAttributeModifier("autocomplete", "off"));
+ // not all user services support manipulating team memberships
+ boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges();
+
// field names reflective match TeamModel fields
form.add(new TextField<String>("name"));
- form.add(users);
+ form.add(users.setEnabled(editMemberships));
mailingLists = new Model<String>(teamModel.mailingLists == null ? ""
: StringUtils.flattenStrings(teamModel.mailingLists, " "));
form.add(new TextField<String>("mailingLists", mailingLists));
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java
index 36f7578d..103d672a 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.java
+++ b/src/com/gitblit/wicket/pages/EditUserPage.java
@@ -54,6 +54,10 @@ public class EditUserPage extends RootSubPage {
public EditUserPage() {
// create constructor
super();
+ if (!GitBlit.self().supportsCredentialChanges()) {
+ error(MessageFormat.format(getString("gb.userServiceDoesNotPermitAddUser"),
+ GitBlit.getString(Keys.realm.userService, "users.conf")), true);
+ }
isCreate = true;
setupPage(new UserModel(""));
}
@@ -125,40 +129,42 @@ public class EditUserPage extends RootSubPage {
}
boolean rename = !StringUtils.isEmpty(oldName)
&& !oldName.equalsIgnoreCase(username);
- if (!userModel.password.equals(confirmPassword.getObject())) {
- error(getString("gb.passwordsDoNotMatch"));
- return;
- }
- String password = userModel.password;
- if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
- && !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
- // This is a plain text password.
- // Check length.
- int minLength = GitBlit.getInteger(Keys.realm.minPasswordLength, 5);
- if (minLength < 4) {
- minLength = 4;
- }
- if (password.trim().length() < minLength) {
- error(MessageFormat.format(getString("gb.passwordTooShort"),
- minLength));
+ if (GitBlit.self().supportsCredentialChanges()) {
+ if (!userModel.password.equals(confirmPassword.getObject())) {
+ error(getString("gb.passwordsDoNotMatch"));
return;
}
-
- // Optionally store the password MD5 digest.
- String type = GitBlit.getString(Keys.realm.passwordStorage, "md5");
- if (type.equalsIgnoreCase("md5")) {
- // store MD5 digest of password
- userModel.password = StringUtils.MD5_TYPE
- + StringUtils.getMD5(userModel.password);
- } else if (type.equalsIgnoreCase("combined-md5")) {
- // store MD5 digest of username+password
- userModel.password = StringUtils.COMBINED_MD5_TYPE
- + StringUtils.getMD5(username + userModel.password);
+ String password = userModel.password;
+ if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
+ && !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ // This is a plain text password.
+ // Check length.
+ int minLength = GitBlit.getInteger(Keys.realm.minPasswordLength, 5);
+ if (minLength < 4) {
+ minLength = 4;
+ }
+ if (password.trim().length() < minLength) {
+ error(MessageFormat.format(getString("gb.passwordTooShort"),
+ minLength));
+ return;
+ }
+
+ // Optionally store the password MD5 digest.
+ String type = GitBlit.getString(Keys.realm.passwordStorage, "md5");
+ if (type.equalsIgnoreCase("md5")) {
+ // store MD5 digest of password
+ userModel.password = StringUtils.MD5_TYPE
+ + StringUtils.getMD5(userModel.password);
+ } else if (type.equalsIgnoreCase("combined-md5")) {
+ // store MD5 digest of username+password
+ userModel.password = StringUtils.COMBINED_MD5_TYPE
+ + StringUtils.getMD5(username + userModel.password);
+ }
+ } else if (rename
+ && password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ error(getString("gb.combinedMd5Rename"));
+ return;
}
- } else if (rename
- && password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
- error(getString("gb.combinedMd5Rename"));
- return;
}
Iterator<String> selectedRepositories = repositories.getSelectedChoices();
@@ -200,20 +206,26 @@ public class EditUserPage extends RootSubPage {
// do not let the browser pre-populate these fields
form.add(new SimpleAttributeModifier("autocomplete", "off"));
+
+ // not all user services support manipulating username and password
+ boolean editCredentials = GitBlit.self().supportsCredentialChanges();
+
+ // not all user services support manipulating team memberships
+ boolean editTeams = GitBlit.self().supportsTeamMembershipChanges();
// field names reflective match UserModel fields
- form.add(new TextField<String>("username"));
+ form.add(new TextField<String>("username").setEnabled(editCredentials));
PasswordTextField passwordField = new PasswordTextField("password");
passwordField.setResetPassword(false);
- form.add(passwordField);
+ form.add(passwordField.setEnabled(editCredentials));
PasswordTextField confirmPasswordField = new PasswordTextField("confirmPassword",
confirmPassword);
confirmPasswordField.setResetPassword(false);
- form.add(confirmPasswordField);
+ form.add(confirmPasswordField.setEnabled(editCredentials));
form.add(new CheckBox("canAdmin"));
form.add(new CheckBox("excludeFromFederation"));
form.add(repositories);
- form.add(teams);
+ form.add(teams.setEnabled(editTeams));
form.add(new Button("save"));
Button cancel = new Button("cancel") {
diff --git a/src/com/gitblit/wicket/panels/UsersPanel.java b/src/com/gitblit/wicket/panels/UsersPanel.java
index ad2ed922..4b0edb34 100644
--- a/src/com/gitblit/wicket/panels/UsersPanel.java
+++ b/src/com/gitblit/wicket/panels/UsersPanel.java
@@ -39,7 +39,8 @@ public class UsersPanel extends BasePanel {
super(wicketId);
Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
- adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class));
+ adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class)
+ .setVisible(GitBlit.self().supportsCredentialChanges()));
add(adminLinks.setVisible(showAdmin));
final List<UserModel> users = GitBlit.self().getAllUsers();
diff --git a/tests/com/gitblit/tests/LdapUserServiceTest.java b/tests/com/gitblit/tests/LdapUserServiceTest.java
new file mode 100644
index 00000000..43af24f2
--- /dev/null
+++ b/tests/com/gitblit/tests/LdapUserServiceTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2012 John Crygier
+ * Copyright 2012 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.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.gitblit.LdapUserService;
+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.LDAPConnection;
+import com.unboundid.ldif.LDIFReader;
+
+/**
+ * An Integration test for LDAP that tests going against an in-memory UnboundID
+ * LDAP server.
+ *
+ * @author jcrygier
+ *
+ */
+public class LdapUserServiceTest {
+
+ private LdapUserService ldapUserService;
+
+ @Before
+ public void createInMemoryLdapServer() throws Exception {
+ InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=MyDomain");
+ config.addAdditionalBindCredentials("cn=Directory Manager", "password");
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", 389));
+ config.setSchema(null);
+
+ InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
+ ds.importFromLDIF(true, new LDIFReader(this.getClass().getResourceAsStream("resources/ldapUserServiceSampleData.ldif")));
+ ds.startListening();
+ }
+
+ @Before
+ public void createLdapUserService() {
+ Map<Object, Object> backingMap = new HashMap<Object, Object>();
+ backingMap.put("realm.ldap.server", "ldap://localhost:389");
+ 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\"");
+
+ MemorySettings ms = new MemorySettings(backingMap);
+
+ ldapUserService = new LdapUserService();
+ ldapUserService.setup(ms);
+ }
+
+ @Test
+ public void testAuthenticate() {
+ UserModel userOneModel = ldapUserService.authenticate("UserOne", "userOnePassword".toCharArray());
+ assertNotNull(userOneModel);
+ assertNotNull(userOneModel.getTeam("git_admins"));
+ assertNotNull(userOneModel.getTeam("git_users"));
+ assertTrue(userOneModel.canAdmin);
+
+ UserModel userOneModelFailedAuth = ldapUserService.authenticate("UserOne", "userTwoPassword".toCharArray());
+ assertNull(userOneModelFailedAuth);
+
+ UserModel userTwoModel = ldapUserService.authenticate("UserTwo", "userTwoPassword".toCharArray());
+ assertNotNull(userTwoModel);
+ assertNotNull(userTwoModel.getTeam("git_users"));
+ assertNull(userTwoModel.getTeam("git_admins"));
+ assertNotNull(userTwoModel.getTeam("git admins"));
+ assertTrue(userTwoModel.canAdmin);
+
+ UserModel userThreeModel = ldapUserService.authenticate("UserThree", "userThreePassword".toCharArray());
+ assertNotNull(userThreeModel);
+ assertNotNull(userThreeModel.getTeam("git_users"));
+ assertNull(userThreeModel.getTeam("git_admins"));
+ assertTrue(userThreeModel.canAdmin);
+ }
+
+}
diff --git a/tests/com/gitblit/tests/StringUtilsTest.java b/tests/com/gitblit/tests/StringUtilsTest.java
index 2e00fa3e..91bfa672 100644
--- a/tests/com/gitblit/tests/StringUtilsTest.java
+++ b/tests/com/gitblit/tests/StringUtilsTest.java
@@ -112,13 +112,24 @@ public class StringUtilsTest {
@Test
public void testStringsFromValue() throws Exception {
- List<String> strings = StringUtils.getStringsFromValue("A B C D");
- assertEquals(4, strings.size());
- assertEquals("A", strings.get(0));
- assertEquals("B", strings.get(1));
- assertEquals("C", strings.get(2));
- assertEquals("D", strings.get(3));
- }
+ List<String> strings = StringUtils.getStringsFromValue("\"A A \" B \"C C\" D \"\" \"E\"");
+ assertEquals(6, strings.size());
+ assertEquals("A A", strings.get(0));
+ assertEquals("B", strings.get(1));
+ assertEquals("C C", strings.get(2));
+ assertEquals("D", strings.get(3));
+ assertEquals("", strings.get(4));
+ assertEquals("E", strings.get(5));
+
+ strings = StringUtils.getStringsFromValue("\"A A \", B, \"C C\", D, \"\", \"E\"", ",");
+ assertEquals(6, strings.size());
+ assertEquals("A A", strings.get(0));
+ assertEquals("B", strings.get(1));
+ assertEquals("C C", strings.get(2));
+ assertEquals("D", strings.get(3));
+ assertEquals("", strings.get(4));
+ assertEquals("E", strings.get(5));
+ }
@Test
public void testStringsFromValue2() throws Exception {
diff --git a/tests/com/gitblit/tests/mock/MemorySettings.java b/tests/com/gitblit/tests/mock/MemorySettings.java
new file mode 100644
index 00000000..8b559354
--- /dev/null
+++ b/tests/com/gitblit/tests/mock/MemorySettings.java
@@ -0,0 +1,50 @@
+ /*
+ * Copyright 2012 John Crygier
+ * Copyright 2012 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.mock;
+
+import java.util.Map;
+import java.util.Properties;
+
+import com.gitblit.IStoredSettings;
+
+public class MemorySettings extends IStoredSettings {
+
+ private Map<Object, Object> backingMap;
+
+ public MemorySettings(Map<Object, Object> backingMap) {
+ super(MemorySettings.class);
+ this.backingMap = backingMap;
+ }
+
+ @Override
+ protected Properties read() {
+ Properties props = new Properties();
+ props.putAll(backingMap);
+
+ return props;
+ }
+
+ public void put(Object key, Object value) {
+ backingMap.put(key, value);
+ }
+
+ @Override
+ public boolean saveSettings(Map<String, String> updatedSettings) {
+ return false;
+ }
+
+}
diff --git a/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif b/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif
new file mode 100644
index 00000000..84ee243e
--- /dev/null
+++ b/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif
@@ -0,0 +1,88 @@
+dn: DC=MyDomain
+dc: MyDomain
+objectClass: top
+objectClass: domain
+
+dn: OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: organizationalUnit
+ou: MyOrganization
+
+dn: OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: organizationalUnit
+ou: UserControl
+
+dn: OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: organizationalUnit
+ou: Groups
+
+dn: CN=Git_Admins,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: group
+cn: Git_Admins
+sAMAccountName: Git_Admins
+member: CN=UserOne,OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+dn: CN=Git Admins,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: group
+cn: Git Admins
+sAMAccountName: Git_Admins_With_Space
+member: CN=UserTwo,OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+dn: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: group
+cn: Git_Users
+sAMAccountName: Git_Users
+member: CN=UserOne,OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+member: CN=UserTwo,OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+member: CN=UserThree,OU=Canada,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+member: CN=UserFour,OU=Canada,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+dn: OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: organizationalUnit
+ou: Users
+
+dn: OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: organizationalUnit
+ou: US
+
+dn: OU=Canada,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: top
+objectClass: organizationalUnit
+ou: Canada
+
+dn: CN=UserOne,OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: user
+objectClass: person
+sAMAccountName: UserOne
+userPassword: userOnePassword
+memberOf: CN=Git_Admins,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+memberOf: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+dn: CN=UserTwo,OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: user
+objectClass: person
+sAMAccountName: UserTwo
+userPassword: userTwoPassword
+memberOf: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+memberOf: CN=Git Admins,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+dn: CN=UserThree,OU=Canada,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: user
+objectClass: person
+sAMAccountName: UserThree
+userPassword: userThreePassword
+memberOf: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+dn: CN=UserFour,OU=Canada,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+objectClass: user
+objectClass: person
+sAMAccountName: UserFour
+userPassword: userFourPassword
+memberOf: CN=Git_Users,OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain \ No newline at end of file