John Crygier
2012-04-10 f3b625d298bab922c64192c25914e352bd87e59e
Rework LDAP implementation with unboundid.  Also allows for an LDAP server to be started with Gitblit GO (backed by an LDIF file).
3 files added
10 files modified
684 ■■■■ changed files
.classpath 1 ●●●● patch | view | raw | blame | history
NOTICE 10 ●●●●● patch | view | raw | blame | history
distrib/gitblit.properties 52 ●●●● patch | view | raw | blame | history
docs/04_design.mkd 1 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlitServer.java 35 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitblitUserService.java 15 ●●●● patch | view | raw | blame | history
src/com/gitblit/LdapUserService.java 238 ●●●●● patch | view | raw | blame | history
src/com/gitblit/build/Build.java 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 28 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 64 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/LdapUserServiceTest.java 104 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/mock/MemorySettings.java 50 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif 80 ●●●●● patch | view | raw | blame | history
.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>
NOTICE
@@ -214,4 +214,12 @@
   Creative Commons CC-BY License.
   http://glyphicons.com
---------------------------------------------------------------------------
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
distrib/gitblit.properties
@@ -141,13 +141,6 @@
# SINCE 1.0.0
realm.ldap.server = ldap://my.ldap.server
# The LDAP domain to prepend to all usernames during authentication.  If
# unspecified, all logins must prepend the domain to their username.
# e.g. mydomain
#
# SINCE 1.0.0
realm.ldap.domain =
# 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
@@ -182,6 +175,51 @@
# SINCE 1.0.0
realm.ldap.maintainTeams = false
# Root node that all Users sit under in LDAP
#
# This is the 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=people,dc=example,dc=com
# 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.
# The variable ${username} is replaced  by the string entered by the end user
#
# 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 user information will begin from in LDAP
# If blank, it will search ALL of ldap.
#
# SINCE 1.0.0
realm.ldap.groupBase = ou=groups,dc=example,dc=com
# 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.
# The variable ${username} is replaced  by the string entered by the end user.
# Other variables appearing in the pattern, such as ${fooBarAttribute},
# are replaced with the value of the corresponding attribute (in this case, fooBarAttribute)
# as read from the user's account object matched under realm.ldap.accountBase. Attributes such
# as ${dn} or ${uidNumber} may be useful.
#
# 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
#
docs/04_design.mkd
@@ -38,6 +38,7 @@
- [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)
src/com/gitblit/GitBlitServer.java
@@ -29,6 +29,7 @@
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 +51,10 @@
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
@@ -266,6 +271,33 @@
        // 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()) {
                    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, "");
                    InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(rootDN);
                    config.addAdditionalBindCredentials(bindUserName, bindPassword);
                    config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", 389));
                    config.setSchema(null);
                    InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
                    ds.importFromLDIF(true, new LDIFReader(ldifFile));
                    ds.startListening();
                    logger.info("LDAP Server started at ldap://localhost:389");
                }
            }
        } 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);
@@ -504,6 +536,9 @@
         */
        @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;
    }
}
src/com/gitblit/GitblitUserService.java
@@ -156,9 +156,12 @@
    public boolean updateUserModel(String username, UserModel model) {
        if (supportsCredentialChanges()) {
            if (!supportsTeamMembershipChanges()) {
                //  teams are externally controlled
                //  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);
        }
@@ -166,9 +169,12 @@
            // passwords are not persisted by the backing user service
            model.password = null;
            if (!supportsTeamMembershipChanges()) {
                //  teams are externally controlled
                //  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);
        }
@@ -228,9 +234,12 @@
    @Override
    public boolean updateTeamModel(String teamname, TeamModel model) {
        if (!supportsTeamMembershipChanges()) {
            // teams are externally controlled
            // 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);
    }
src/com/gitblit/LdapUserService.java
@@ -17,23 +17,25 @@
package com.gitblit;
import java.io.File;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ConnectionUtils.BlindSSLSocketFactory;
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.
@@ -43,8 +45,7 @@
public class LdapUserService extends GitblitUserService {
    public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
    private final String CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
    private IStoredSettings settings;
    public LdapUserService() {
@@ -59,6 +60,36 @@
        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;
    }
    
    /**
@@ -98,75 +129,128 @@
    @Override
    public UserModel authenticate(String username, char[] password) {
        String domainUser = getDomainUsername(username);
        DirContext ctx = getDirContext(domainUser, new String(password));
        // TODO do we need a bind here?
        if (ctx != null) {
            String simpleUsername = getSimpleUsername(username);
            UserModel user = getUserModel(simpleUsername);
            if (user == null) {
                // create user object for new authenticated user
                user = new UserModel(simpleUsername.toLowerCase());
            }
            user.password = new String(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);
            if (!supportsTeamMembershipChanges()) {
                // Teams are specified in LDAP server
                // TODO search LDAP for team memberships
                Set<String> foundTeams = new HashSet<String>();
                for (String team : foundTeams) {
                    TeamModel model = getTeamModel(team);
                    if (model == null) {
                        // create the team
                        model = new TeamModel(team.toLowerCase());
                        updateTeamModel(model);
            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(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);
                    }
                    // add team to the user
                    user.teams.add(model);
                    return user;
                }
            }
            try {
                ctx.close();
            } catch (NamingException e) {
                logger.error("Can not close context", e);
            }
            return user;
        }
        }
        return null;        
    }
    protected DirContext getDirContext() {
        String username = settings.getString(Keys.realm.ldap_username, "");
        String password = settings.getString(Keys.realm.ldap_password, "");
        return getDirContext(username, password);
    }
    protected DirContext getDirContext(String username, String password) {
        try {
            String server = settings.getRequiredString(Keys.realm.ldap_server);
            Hashtable<String, String> env = new Hashtable<String, String>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, CONTEXT_FACTORY);
            env.put(Context.PROVIDER_URL, server);
            if (server.startsWith("ldaps:")) {
                env.put("java.naming.ldap.factory.socket", BlindSSLSocketFactory.class.getName());
            }
            // TODO consider making this a setting
            env.put("com.sun.jndi.ldap.read.timeout", "5000");
    private void setAdminAttribute(UserModel user) {
        String adminString = settings.getString(Keys.realm.ldap_admins, "");
        String[] admins = adminString.split(" ");
        user.canAdmin = false;
        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;
        }
    }
            if (!StringUtils.isEmpty(username)) {
                // authenticated login
                env.put(Context.SECURITY_AUTHENTICATION, "simple");
                env.put(Context.SECURITY_PRINCIPAL, getDomainUsername(username));
                env.put(Context.SECURITY_CREDENTIALS, password == null ? "":password.trim());
    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());
            }
            return new InitialDirContext(env);
        } catch (NamingException e) {
            logger.warn(MessageFormat.format("Error connecting to LDAP with credentials. Please check {0}, {1}, and {2}",
                    Keys.realm.ldap_server, Keys.realm.ldap_username, Keys.realm.ldap_password), e);
        }
    }
    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(SearchResultEntry userEntry) {
        UserModel answer = new UserModel(userEntry.getAttributeValue("cn"));
        //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.
@@ -180,21 +264,5 @@
            username = username.substring(lastSlash + 1);
        }
        return username;
    }
    /**
     * Returns a username with a domain prefix as long as the username does not
     * already have a comain prefix.
     *
     * @param username
     * @return a domain username
     */
    protected String getDomainUsername(String username) {
        String domain = settings.getString(Keys.realm.ldap_domain, null);
        String domainUsername = username;
        if (!StringUtils.isEmpty(domain) && (domainUsername.indexOf('\\') == -1)) {
            domainUsername = domain + "\\" + username;
        }
        return domainUsername.trim();
    }
}
src/com/gitblit/build/Build.java
@@ -94,6 +94,7 @@
        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 @@
        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 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;
src/com/gitblit/utils/StringUtils.java
@@ -518,4 +518,32 @@
        }
        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();
    }
}
src/com/gitblit/wicket/pages/EditUserPage.java
@@ -129,40 +129,42 @@
                }
                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();
tests/com/gitblit/tests/LdapUserServiceTest.java
New file
@@ -0,0 +1,104 @@
/*
 * 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");
        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"));
        assertFalse(userTwoModel.canAdmin);
        UserModel userThreeModel = ldapUserService.authenticate("UserThree", "userThreePassword".toCharArray());
        assertNotNull(userThreeModel);
        assertNotNull(userThreeModel.getTeam("git_users"));
        assertNull(userThreeModel.getTeam("git_admins"));
        assertTrue(userThreeModel.canAdmin);
    }
}
tests/com/gitblit/tests/mock/MemorySettings.java
New file
@@ -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;
    }
}
tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif
New file
@@ -0,0 +1,80 @@
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_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
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