From 6cca8699f98a606ff19e88d40a8a2535fdc340e7 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 6 Apr 2012 18:01:58 -0400 Subject: Skeleton LdapUserService based on John Cryiger's implementation --- src/com/gitblit/ConfigUserService.java | 25 ++- src/com/gitblit/FileUserService.java | 27 ++- src/com/gitblit/GitBlit.java | 16 ++ src/com/gitblit/GitblitUserService.java | 38 +++- src/com/gitblit/IStoredSettings.java | 18 ++ src/com/gitblit/IUserService.java | 16 ++ src/com/gitblit/LdapUserService.java | 200 +++++++++++++++++++++ src/com/gitblit/utils/ConnectionUtils.java | 89 +++++++++ src/com/gitblit/wicket/GitBlitWebApp.properties | 4 +- src/com/gitblit/wicket/pages/BasePage.java | 6 +- .../gitblit/wicket/pages/ChangePasswordPage.java | 6 + src/com/gitblit/wicket/pages/EditTeamPage.java | 5 +- src/com/gitblit/wicket/pages/EditUserPage.java | 18 +- src/com/gitblit/wicket/panels/UsersPanel.java | 3 +- 14 files changed, 458 insertions(+), 13 deletions(-) create mode 100644 src/com/gitblit/LdapUserService.java (limited to 'src') 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 @@ -99,6 +99,27 @@ public class ConfigUserService implements IUserService { public void setup(IStoredSettings settings) { } + /** + * 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? * @@ -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 roles = new ArrayList(); 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 @@ -73,6 +73,27 @@ public class FileUserService extends FileSettings implements IUserService { public void setup(IStoredSettings settings) { } + /** + * 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? * @@ -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 7b557d79..6ed54dc6 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/GitblitUserService.java b/src/com/gitblit/GitblitUserService.java index 7462af08..1514b6b9 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 @@ -111,6 +112,16 @@ public class GitblitUserService implements IUserService { return getClass().getSimpleName(); } + @Override + public boolean supportsCredentialChanges() { + return serviceImpl.supportsCredentialChanges(); + } + + @Override + public boolean supportsTeamMembershipChanges() { + return serviceImpl.supportsTeamMembershipChanges(); + } + @Override public boolean supportsCookies() { return serviceImpl.supportsCookies(); @@ -143,9 +154,27 @@ 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 + model = DeepCopier.copy(model); + model.teams.clear(); + } + 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 + model = DeepCopier.copy(model); + model.teams.clear(); + } + 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 +227,11 @@ public class GitblitUserService implements IUserService { @Override public boolean updateTeamModel(String teamname, TeamModel model) { + if (!supportsTeamMembershipChanges()) { + // teams are externally controlled + model = DeepCopier.copy(model); + model.users.clear(); + } 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 @@ -39,6 +39,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? * diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java new file mode 100644 index 00000000..3ec45e17 --- /dev/null +++ b/src/com/gitblit/LdapUserService.java @@ -0,0 +1,200 @@ +/* + * 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.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 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; + +/** + * Implementation of an LDAP user service. + * + * @author John Crygier + */ +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() { + 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()); + } + + /** + * 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 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); + + if (!supportsTeamMembershipChanges()) { + // Teams are specified in LDAP server + // TODO search LDAP for team memberships + Set foundTeams = new HashSet(); + for (String team : foundTeams) { + TeamModel model = getTeamModel(team); + if (model == null) { + // create the team + model = new TeamModel(team.toLowerCase()); + updateTeamModel(model); + } + // add team to the user + user.teams.add(model); + } + } + + 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 env = new Hashtable(); + 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"); + + 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()); + } + 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); + return null; + } + } + + /** + * 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; + } + + /** + * 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(); + } +} 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/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("changePasswordLink", ChangePasswordPage.class)); + add(new Label("separator", "|").setVisible(editCredentials)); + add(new BookmarkablePageLink("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 form = new StatelessForm("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("name")); - form.add(users); + form.add(users.setEnabled(editMemberships)); mailingLists = new Model(teamModel.mailingLists == null ? "" : StringUtils.flattenStrings(teamModel.mailingLists, " ")); form.add(new TextField("mailingLists", mailingLists)); diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java index 36f7578d..e7b42877 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("")); } @@ -200,20 +204,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("username")); + form.add(new TextField("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("newUser", EditUserPage.class)); + adminLinks.add(new BookmarkablePageLink("newUser", EditUserPage.class) + .setVisible(GitBlit.self().supportsCredentialChanges())); add(adminLinks.setVisible(showAdmin)); final List users = GitBlit.self().getAllUsers(); -- cgit v1.2.3 From f3b625d298bab922c64192c25914e352bd87e59e Mon Sep 17 00:00:00 2001 From: John Crygier Date: Tue, 10 Apr 2012 12:48:06 -0500 Subject: Rework LDAP implementation with unboundid. Also allows for an LDAP server to be started with Gitblit GO (backed by an LDIF file). --- .classpath | 1 + NOTICE | 10 +- distrib/gitblit.properties | 52 ++++- docs/04_design.mkd | 1 + src/com/gitblit/GitBlitServer.java | 35 +++ src/com/gitblit/GitblitUserService.java | 15 +- src/com/gitblit/LdapUserService.java | 240 +++++++++++++-------- src/com/gitblit/build/Build.java | 6 + src/com/gitblit/utils/StringUtils.java | 28 +++ src/com/gitblit/wicket/pages/EditUserPage.java | 64 +++--- tests/com/gitblit/tests/LdapUserServiceTest.java | 104 +++++++++ tests/com/gitblit/tests/mock/MemorySettings.java | 50 +++++ .../tests/resources/ldapUserServiceSampleData.ldif | 80 +++++++ 13 files changed, 558 insertions(+), 128 deletions(-) create mode 100644 tests/com/gitblit/tests/LdapUserServiceTest.java create mode 100644 tests/com/gitblit/tests/mock/MemorySettings.java create mode 100644 tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif (limited to 'src') diff --git a/.classpath b/.classpath index b0f1f866..f636f49c 100644 --- a/.classpath +++ b/.classpath @@ -30,5 +30,6 @@ + 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 acceb888..147f2a1e 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -141,13 +141,6 @@ realm.minPasswordLength = 5 # 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 @@ realm.ldap.backingUserService = users.conf # 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 # 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/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java index 3f996fcc..e8ee89ce 100644 --- a/src/com/gitblit/GitBlitServer.java +++ b/src/com/gitblit/GitBlitServer.java @@ -29,6 +29,7 @@ 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 +51,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 @@ -266,6 +271,33 @@ 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()) { + 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 @@ 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 1514b6b9..ddb3ca77 100644 --- a/src/com/gitblit/GitblitUserService.java +++ b/src/com/gitblit/GitblitUserService.java @@ -156,9 +156,12 @@ public class GitblitUserService implements IUserService { 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 @@ public class GitblitUserService implements IUserService { // 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 @@ public class GitblitUserService implements IUserService { @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); } diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java index 3ec45e17..c67d17b1 100644 --- a/src/com/gitblit/LdapUserService.java +++ b/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 @@ import com.gitblit.utils.StringUtils; 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() { @@ -61,6 +62,36 @@ public class LdapUserService extends GitblitUserService { 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)://:", 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. @@ -98,76 +129,129 @@ public class LdapUserService extends GitblitUserService { @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); - - if (!supportsTeamMembershipChanges()) { - // Teams are specified in LDAP server - // TODO search LDAP for team memberships - Set foundTeams = new HashSet(); - for (String team : foundTeams) { - TeamModel model = getTeamModel(team); - if (model == null) { - // create the team - model = new TeamModel(team.toLowerCase()); - updateTeamModel(model); + 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(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; - } + } + + 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; + } + } + + 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; + } - protected DirContext getDirContext() { - String username = settings.getString(Keys.realm.ldap_username, ""); - String password = settings.getString(Keys.realm.ldap_password, ""); - return getDirContext(username, password); + 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; } - protected DirContext getDirContext(String username, String password) { + private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) { try { - String server = settings.getRequiredString(Keys.realm.ldap_server); - Hashtable env = new Hashtable(); - 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"); - - 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()); - } - 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); + 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. * @@ -181,20 +265,4 @@ public class LdapUserService extends GitblitUserService { } 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(); - } } 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/StringUtils.java b/src/com/gitblit/utils/StringUtils.java index d6441829..0460ac9f 100644 --- a/src/com/gitblit/utils/StringUtils.java +++ b/src/com/gitblit/utils/StringUtils.java @@ -518,4 +518,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/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java index e7b42877..103d672a 100644 --- a/src/com/gitblit/wicket/pages/EditUserPage.java +++ b/src/com/gitblit/wicket/pages/EditUserPage.java @@ -129,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 selectedRepositories = repositories.getSelectedChoices(); diff --git a/tests/com/gitblit/tests/LdapUserServiceTest.java b/tests/com/gitblit/tests/LdapUserServiceTest.java new file mode 100644 index 00000000..777d86cf --- /dev/null +++ b/tests/com/gitblit/tests/LdapUserServiceTest.java @@ -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 backingMap = new HashMap(); + 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); + } + +} 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 backingMap; + + public MemorySettings(Map 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 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..22d68699 --- /dev/null +++ b/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif @@ -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 \ No newline at end of file -- cgit v1.2.3 From 6f0d843f855e179c66be107d1ab14f81254d6017 Mon Sep 17 00:00:00 2001 From: John Crygier Date: Tue, 10 Apr 2012 15:54:57 -0500 Subject: Allow for different port for LDAP in-memory server. Update some documentation. --- distrib/gitblit.properties | 31 ++++++++++++++++--------------- docs/01_setup.mkd | 2 +- src/com/gitblit/GitBlitServer.java | 11 +++++++++-- 3 files changed, 26 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index 147f2a1e..527b7268 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -139,7 +139,7 @@ realm.minPasswordLength = 5 # URL of the LDAP server. # # SINCE 1.0.0 -realm.ldap.server = ldap://my.ldap.server +realm.ldap.server = ldap://localhost # Login username for LDAP searches. # The domain prefix may be omitted if it matches the domain specified in @@ -149,12 +149,12 @@ realm.ldap.server = ldap://my.ldap.server # e.g. mydomain\\username # # SINCE 1.0.0 -realm.ldap.username = +realm.ldap.username = cn=Directory Manager # Login password for LDAP searches. # # SINCE 1.0.0 -realm.ldap.password = +realm.ldap.password = password # The LdapUserService must be backed by another user service for standard user # and team management. @@ -177,38 +177,39 @@ 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 +# 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=people,dc=example,dc=com +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. -# The variable ${username} is replaced by the string entered by the end user +# 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 user information will begin from 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,dc=example,dc=com +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. -# 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. +# 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})) diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd index a7b4cdaa..c2e2ef11 100644 --- a/docs/01_setup.mkd +++ b/docs/01_setup.mkd @@ -504,6 +504,6 @@ The following is are descriptions of the properties that would follow the sample 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 always start at ldap://localhost:389, so be sure to set that in gitblit.settings. It reads the user / password in gitblit.settings to create the root user login. +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/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java index e8ee89ce..f4be8e9f 100644 --- a/src/com/gitblit/GitBlitServer.java +++ b/src/com/gitblit/GitBlitServer.java @@ -23,6 +23,7 @@ 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; @@ -277,21 +278,27 @@ public class GitBlitServer { 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", 389)); + 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:389"); + logger.info("LDAP Server started at ldap://localhost:" + port); } } } catch (Exception e) { -- cgit v1.2.3 From f898a655a912ae76585339dfa04f018df552fb8a Mon Sep 17 00:00:00 2001 From: John Crygier Date: Wed, 18 Apr 2012 15:48:00 -0500 Subject: Change username to come from simpleUserName (from login screen) instead of CN. --- src/com/gitblit/LdapUserService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java index c67d17b1..9fcef9d6 100644 --- a/src/com/gitblit/LdapUserService.java +++ b/src/com/gitblit/LdapUserService.java @@ -148,7 +148,7 @@ public class LdapUserService extends GitblitUserService { UserModel user = getUserModel(simpleUsername); if (user == null) // create user object for new authenticated user - user = createUserFromLdap(loggingInUser); + user = createUserFromLdap(simpleUsername, loggingInUser); user.password = "StoredInLDAP"; @@ -224,8 +224,8 @@ public class LdapUserService extends GitblitUserService { return answer; } - private UserModel createUserFromLdap(SearchResultEntry userEntry) { - UserModel answer = new UserModel(userEntry.getAttributeValue("cn")); + 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; -- cgit v1.2.3 From 3d699cb14d6c891338567c562f7fe8d81e318b1c Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 24 Apr 2012 07:48:39 -0500 Subject: Allow for admin teams with spaces in their name. --- src/com/gitblit/LdapUserService.java | 22 +++++++-------- src/com/gitblit/utils/StringUtils.java | 32 ++++++++++++---------- tests/com/gitblit/tests/LdapUserServiceTest.java | 5 ++-- tests/com/gitblit/tests/StringUtilsTest.java | 25 ++++++++++++----- .../tests/resources/ldapUserServiceSampleData.ldif | 8 ++++++ 5 files changed, 58 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java index 9fcef9d6..86b61364 100644 --- a/src/com/gitblit/LdapUserService.java +++ b/src/com/gitblit/LdapUserService.java @@ -20,6 +20,7 @@ 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; @@ -174,17 +175,16 @@ public class LdapUserService extends GitblitUserService { } 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; - } + user.canAdmin = false; + List 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) { diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java index 0460ac9f..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 getStringsFromValue(String value, String separator) { - List strings = new ArrayList(); - 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 strings = new ArrayList(); + 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 diff --git a/tests/com/gitblit/tests/LdapUserServiceTest.java b/tests/com/gitblit/tests/LdapUserServiceTest.java index 777d86cf..43af24f2 100644 --- a/tests/com/gitblit/tests/LdapUserServiceTest.java +++ b/tests/com/gitblit/tests/LdapUserServiceTest.java @@ -69,7 +69,7 @@ public class LdapUserServiceTest { 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"); + backingMap.put("realm.ldap.admins", "UserThree @Git_Admins \"@Git Admins\""); MemorySettings ms = new MemorySettings(backingMap); @@ -92,7 +92,8 @@ public class LdapUserServiceTest { assertNotNull(userTwoModel); assertNotNull(userTwoModel.getTeam("git_users")); assertNull(userTwoModel.getTeam("git_admins")); - assertFalse(userTwoModel.canAdmin); + assertNotNull(userTwoModel.getTeam("git admins")); + assertTrue(userTwoModel.canAdmin); UserModel userThreeModel = ldapUserService.authenticate("UserThree", "userThreePassword".toCharArray()); assertNotNull(userThreeModel); 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 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 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/resources/ldapUserServiceSampleData.ldif b/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif index 22d68699..84ee243e 100644 --- a/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif +++ b/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif @@ -25,6 +25,13 @@ 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 @@ -64,6 +71,7 @@ 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 -- cgit v1.2.3