@@ -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> |
@@ -214,4 +214,12 @@ GLYHPICONS | |||
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 | |||
@@ -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 | |||
# |
@@ -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) |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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)://<server>:<port>", e); | |||
} catch (GeneralSecurityException e) { | |||
logger.error("Unable to create SSL Connection", e); | |||
} catch (LDAPException e) { | |||
logger.error("Error Connecting to LDAP", e); | |||
} | |||
return null; | |||
} | |||
/** | |||
* Credentials are defined in the LDAP server and can not be manipulated | |||
* from Gitblit. | |||
@@ -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<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); | |||
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<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"); | |||
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(); | |||
} | |||
} |
@@ -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; |
@@ -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(); | |||
} | |||
} |
@@ -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<String> selectedRepositories = repositories.getSelectedChoices(); |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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 |