From 04a98505a4ab8f48aee22800fcac193d9367d0ae Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Fri, 29 Nov 2013 11:05:51 -0500 Subject: [PATCH] Refactor user services and separate authentication (issue-281) --- src/main/java/com/gitblit/auth/LdapAuthProvider.java | 1038 ++++++++++++++++++++++++++++----------------------------- 1 files changed, 508 insertions(+), 530 deletions(-) diff --git a/src/main/java/com/gitblit/LdapUserService.java b/src/main/java/com/gitblit/auth/LdapAuthProvider.java similarity index 87% rename from src/main/java/com/gitblit/LdapUserService.java rename to src/main/java/com/gitblit/auth/LdapAuthProvider.java index c075afc..7a6b74d 100644 --- a/src/main/java/com/gitblit/LdapUserService.java +++ b/src/main/java/com/gitblit/auth/LdapAuthProvider.java @@ -1,530 +1,508 @@ -/* - * Copyright 2012 John Crygier - * Copyright 2012 gitblit.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.Constants.AccountType; -import com.gitblit.manager.IRuntimeManager; -import com.gitblit.models.TeamModel; -import com.gitblit.models.UserModel; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.StringUtils; -import com.unboundid.ldap.sdk.Attribute; -import com.unboundid.ldap.sdk.DereferencePolicy; -import com.unboundid.ldap.sdk.ExtendedResult; -import com.unboundid.ldap.sdk.LDAPConnection; -import com.unboundid.ldap.sdk.LDAPException; -import com.unboundid.ldap.sdk.LDAPSearchException; -import com.unboundid.ldap.sdk.ResultCode; -import com.unboundid.ldap.sdk.SearchRequest; -import com.unboundid.ldap.sdk.SearchResult; -import com.unboundid.ldap.sdk.SearchResultEntry; -import com.unboundid.ldap.sdk.SearchScope; -import com.unboundid.ldap.sdk.SimpleBindRequest; -import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; -import com.unboundid.util.ssl.SSLUtil; -import com.unboundid.util.ssl.TrustAllTrustManager; - -/** - * Implementation of an LDAP user service. - * - * @author John Crygier - */ -public class LdapUserService extends GitblitUserService { - - public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class); - - private IStoredSettings settings; - private AtomicLong lastLdapUserSync = new AtomicLong(0L); - - public LdapUserService() { - super(); - } - - private long getSynchronizationPeriod() { - final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES"); - try { - final String[] s = cacheDuration.split(" ", 2); - long duration = Long.parseLong(s[0]); - TimeUnit timeUnit = TimeUnit.valueOf(s[1]); - return timeUnit.toMillis(duration); - } catch (RuntimeException ex) { - throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); - } - } - - @Override - public void setup(IRuntimeManager runtimeManager) { - this.settings = runtimeManager.getSettings(); - String file = settings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf"); - File realmFile = runtimeManager.getFileOrFolder(file); - - serviceImpl = createUserService(realmFile); - logger.info("LDAP User Service backed by " + serviceImpl.toString()); - - synchronizeLdapUsers(); - } - - protected synchronized void synchronizeLdapUsers() { - final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false); - if (enabled) { - if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriod())) { - logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server)); - final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true); - LDAPConnection ldapConnection = getLdapConnection(); - if (ldapConnection != null) { - try { - String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); - String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); - String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); - accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); - - SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); - if (result != null && result.getEntryCount() > 0) { - final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>(); - - for (SearchResultEntry loggingInUser : result.getSearchEntries()) { - - final String username = loggingInUser.getAttribute(uidAttribute).getValue(); - logger.debug("LDAP synchronizing: " + username); - - UserModel user = getUserModel(username); - if (user == null) { - user = new UserModel(username); - } - - if (!supportsTeamMembershipChanges()) - getTeamsFromLdap(ldapConnection, username, loggingInUser, user); - - // Get User Attributes - setUserAttributes(user, loggingInUser); - - // store in map - ldapUsers.put(username.toLowerCase(), user); - } - - if (deleteRemovedLdapUsers) { - logger.debug("detecting removed LDAP users..."); - - for (UserModel userModel : super.getAllUsers()) { - if (Constants.EXTERNAL_ACCOUNT.equals(userModel.password)) { - if (! ldapUsers.containsKey(userModel.username)) { - logger.info("deleting removed LDAP user " + userModel.username + " from backing user service"); - super.deleteUser(userModel.username); - } - } - } - } - - super.updateUserModels(ldapUsers.values()); - - if (!supportsTeamMembershipChanges()) { - final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); - for (UserModel user : ldapUsers.values()) { - for (TeamModel userTeam : user.teams) { - userTeams.put(userTeam.name, userTeam); - } - } - updateTeamModels(userTeams.values()); - } - } - lastLdapUserSync.set(System.currentTimeMillis()); - } finally { - ldapConnection.close(); - } - } - } - } - } - - private LDAPConnection getLdapConnection() { - try { - - URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); - String ldapHost = ldapUrl.getHost(); - int ldapPort = ldapUrl.getPort(); - String bindUserName = settings.getString(Keys.realm.ldap.username, ""); - String bindPassword = settings.getString(Keys.realm.ldap.password, ""); - - - LDAPConnection conn; - if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { // SSL - SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); - conn = new LDAPConnection(sslUtil.createSSLSocketFactory()); - } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { // no encryption or StartTLS - conn = new LDAPConnection(); - } else { - logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme()); - return null; - } - - conn.connect(ldapHost, ldapPort); - - if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { - SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); - ExtendedResult extendedResult = conn.processExtendedOperation( - new StartTLSExtendedRequest(sslUtil.createSSLContext())); - if (extendedResult.getResultCode() != ResultCode.SUCCESS) { - throw new LDAPException(extendedResult.getResultCode()); - } - } - - if ( ! StringUtils.isEmpty(bindUserName) || ! StringUtils.isEmpty(bindPassword)) { - conn.bind(new SimpleBindRequest(bindUserName, bindPassword)); - } - - return conn; - - } catch (URISyntaxException e) { - logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e); - } catch (GeneralSecurityException e) { - logger.error("Unable to create SSL Connection", e); - } catch (LDAPException e) { - logger.error("Error Connecting to LDAP", e); - } - - return null; - } - - /** - * Credentials are defined in the LDAP server and can not be manipulated - * from Gitblit. - * - * @return false - * @since 1.0.0 - */ - @Override - public boolean supportsCredentialChanges() { - return false; - } - - /** - * If no displayName pattern is defined then Gitblit can manage the display name. - * - * @return true if Gitblit can manage the user display name - * @since 1.0.0 - */ - @Override - public boolean supportsDisplayNameChanges() { - return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, "")); - } - - /** - * If no email pattern is defined then Gitblit can manage the email address. - * - * @return true if Gitblit can manage the user email address - * @since 1.0.0 - */ - @Override - public boolean supportsEmailAddressChanges() { - return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, "")); - } - - - /** - * 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 - */ - @Override - public boolean supportsTeamMembershipChanges() { - return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false); - } - - @Override - public AccountType getAccountType() { - return AccountType.LDAP; - } - - @Override - public UserModel authenticate(String username, char[] password) { - if (isLocalAccount(username)) { - // local account, bypass LDAP authentication - return super.authenticate(username, password); - } - - String simpleUsername = getSimpleUsername(username); - - LDAPConnection ldapConnection = getLdapConnection(); - if (ldapConnection != null) { - try { - // 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}", escapeLDAPSearchFilter(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("LDAP authenticated: " + username); - - UserModel user = null; - synchronized (this) { - user = getUserModel(simpleUsername); - if (user == null) // create user object for new authenticated user - user = new UserModel(simpleUsername); - - // create a user cookie - if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { - user.cookie = StringUtils.getSHA1(user.username + new String(password)); - } - - if (!supportsTeamMembershipChanges()) - getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user); - - // Get User Attributes - setUserAttributes(user, loggingInUser); - - // Push the ldap looked up values to backing file - super.updateUserModel(user); - if (!supportsTeamMembershipChanges()) { - for (TeamModel userTeam : user.teams) - updateTeamModel(userTeam); - } - } - - return user; - } - } - } finally { - ldapConnection.close(); - } - } - return null; - } - - /** - * Set the admin attribute from team memberships retrieved from LDAP. - * If we are not storing teams in LDAP and/or we have not defined any - * administrator teams, then do not change the admin flag. - * - * @param user - */ - private void setAdminAttribute(UserModel user) { - if (!supportsTeamMembershipChanges()) { - List<String> admins = settings.getStrings(Keys.realm.ldap.admins); - // if we have defined administrative teams, then set admin flag - // otherwise leave admin flag unchanged - if (!ArrayUtils.isEmpty(admins)) { - 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 setUserAttributes(UserModel user, SearchResultEntry userEntry) { - // Is this user an admin? - setAdminAttribute(user); - - // Don't want visibility into the real password, make up a dummy - user.password = Constants.EXTERNAL_ACCOUNT; - user.accountType = getAccountType(); - - // Get full name Attribute - String displayName = settings.getString(Keys.realm.ldap.displayName, ""); - if (!StringUtils.isEmpty(displayName)) { - // Replace embedded ${} with attributes - if (displayName.contains("${")) { - for (Attribute userAttribute : userEntry.getAttributes()) - displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue()); - - user.displayName = displayName; - } else { - Attribute attribute = userEntry.getAttribute(displayName); - if (attribute != null && attribute.hasValue()) { - user.displayName = attribute.getValue(); - } - } - } - - // Get email address Attribute - String email = settings.getString(Keys.realm.ldap.email, ""); - if (!StringUtils.isEmpty(email)) { - if (email.contains("${")) { - for (Attribute userAttribute : userEntry.getAttributes()) - email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue()); - - user.emailAddress = email; - } else { - Attribute attribute = userEntry.getAttribute(email); - if (attribute != null && attribute.hasValue()) { - user.emailAddress = attribute.getValue(); - } - } - } - } - - 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}", escapeLDAPSearchFilter(loggingInUserDN)); - groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); - - // Fill in attributes into groupMemberPattern - for (Attribute userAttribute : loggingInUser.getAttributes()) - groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue())); - - SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn")); - 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")); - // potentially retrieve other attributes here in the future - - return answer; - } - - private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) { - try { - return ldapConnection.search(base, SearchScope.SUB, filter); - } catch (LDAPSearchException e) { - logger.error("Problem Searching LDAP", e); - - return null; - } - } - - private SearchResult doSearch(LDAPConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) { - try { - SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter); - if ( dereferenceAliases ) { - searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING); - } - if (attributes != null) { - searchRequest.setAttributes(attributes); - } - return ldapConnection.search(searchRequest); - - } catch (LDAPSearchException e) { - logger.error("Problem Searching LDAP", e); - - return null; - } catch (LDAPException e) { - logger.error("Problem creating LDAP search", e); - return null; - } - } - - private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) { - try { - // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN - ldapConnection.bind(userDn, password); - return true; - } catch (LDAPException e) { - logger.error("Error authenticating user", e); - return false; - } - } - - @Override - public List<String> getAllUsernames() { - synchronizeLdapUsers(); - return super.getAllUsernames(); - } - - @Override - public List<UserModel> getAllUsers() { - synchronizeLdapUsers(); - return super.getAllUsers(); - } - - /** - * 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; - } - - // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java - public static final String escapeLDAPSearchFilter(String filter) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < filter.length(); i++) { - char curChar = filter.charAt(i); - switch (curChar) { - case '\\': - sb.append("\\5c"); - break; - case '*': - sb.append("\\2a"); - break; - case '(': - sb.append("\\28"); - break; - case ')': - sb.append("\\29"); - break; - case '\u0000': - sb.append("\\00"); - break; - default: - sb.append(curChar); - } - } - return sb.toString(); - } -} +/* + * 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.auth; + +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.gitblit.Constants; +import com.gitblit.Constants.AccountType; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; +import com.unboundid.ldap.sdk.Attribute; +import com.unboundid.ldap.sdk.DereferencePolicy; +import com.unboundid.ldap.sdk.ExtendedResult; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPSearchException; +import com.unboundid.ldap.sdk.ResultCode; +import com.unboundid.ldap.sdk.SearchRequest; +import com.unboundid.ldap.sdk.SearchResult; +import com.unboundid.ldap.sdk.SearchResultEntry; +import com.unboundid.ldap.sdk.SearchScope; +import com.unboundid.ldap.sdk.SimpleBindRequest; +import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; +import com.unboundid.util.ssl.SSLUtil; +import com.unboundid.util.ssl.TrustAllTrustManager; + +/** + * Implementation of an LDAP user service. + * + * @author John Crygier + */ +public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { + + private AtomicLong lastLdapUserSync = new AtomicLong(0L); + + public LdapAuthProvider() { + super("ldap"); + } + + private long getSynchronizationPeriod() { + final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES"); + try { + final String[] s = cacheDuration.split(" ", 2); + long duration = Long.parseLong(s[0]); + TimeUnit timeUnit = TimeUnit.valueOf(s[1]); + return timeUnit.toMillis(duration); + } catch (RuntimeException ex) { + throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); + } + } + + @Override + public void setup() { + synchronizeLdapUsers(); + } + + protected synchronized void synchronizeLdapUsers() { + final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false); + if (enabled) { + if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriod())) { + logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server)); + final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true); + LDAPConnection ldapConnection = getLdapConnection(); + if (ldapConnection != null) { + try { + String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); + String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); + String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); + accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); + + SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); + if (result != null && result.getEntryCount() > 0) { + final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>(); + + for (SearchResultEntry loggingInUser : result.getSearchEntries()) { + + final String username = loggingInUser.getAttribute(uidAttribute).getValue(); + logger.debug("LDAP synchronizing: " + username); + + UserModel user = userManager.getUserModel(username); + if (user == null) { + user = new UserModel(username); + } + + if (!supportsTeamMembershipChanges()) { + getTeamsFromLdap(ldapConnection, username, loggingInUser, user); + } + + // Get User Attributes + setUserAttributes(user, loggingInUser); + + // store in map + ldapUsers.put(username.toLowerCase(), user); + } + + if (deleteRemovedLdapUsers) { + logger.debug("detecting removed LDAP users..."); + + for (UserModel userModel : userManager.getAllUsers()) { + if (Constants.EXTERNAL_ACCOUNT.equals(userModel.password)) { + if (!ldapUsers.containsKey(userModel.username)) { + logger.info("deleting removed LDAP user " + userModel.username + " from user service"); + userManager.deleteUser(userModel.username); + } + } + } + } + + userManager.updateUserModels(ldapUsers.values()); + + if (!supportsTeamMembershipChanges()) { + final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); + for (UserModel user : ldapUsers.values()) { + for (TeamModel userTeam : user.teams) { + userTeams.put(userTeam.name, userTeam); + } + } + userManager.updateTeamModels(userTeams.values()); + } + } + lastLdapUserSync.set(System.currentTimeMillis()); + } finally { + ldapConnection.close(); + } + } + } + } + } + + private LDAPConnection getLdapConnection() { + try { + + URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); + String ldapHost = ldapUrl.getHost(); + int ldapPort = ldapUrl.getPort(); + String bindUserName = settings.getString(Keys.realm.ldap.username, ""); + String bindPassword = settings.getString(Keys.realm.ldap.password, ""); + + + LDAPConnection conn; + if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { + // SSL + SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + conn = new LDAPConnection(sslUtil.createSSLSocketFactory()); + } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { + // no encryption or StartTLS + conn = new LDAPConnection(); + } else { + logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme()); + return null; + } + + conn.connect(ldapHost, ldapPort); + + if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { + SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + ExtendedResult extendedResult = conn.processExtendedOperation( + new StartTLSExtendedRequest(sslUtil.createSSLContext())); + if (extendedResult.getResultCode() != ResultCode.SUCCESS) { + throw new LDAPException(extendedResult.getResultCode()); + } + } + + if (!StringUtils.isEmpty(bindUserName) || !StringUtils.isEmpty(bindPassword)) { + conn.bind(new SimpleBindRequest(bindUserName, bindPassword)); + } + + return conn; + + } catch (URISyntaxException e) { + logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e); + } catch (GeneralSecurityException e) { + logger.error("Unable to create SSL Connection", e); + } catch (LDAPException e) { + logger.error("Error Connecting to LDAP", e); + } + + return null; + } + + /** + * Credentials are defined in the LDAP server and can not be manipulated + * from Gitblit. + * + * @return false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return false; + } + + /** + * If no displayName pattern is defined then Gitblit can manage the display name. + * + * @return true if Gitblit can manage the user display name + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, "")); + } + + /** + * If no email pattern is defined then Gitblit can manage the email address. + * + * @return true if Gitblit can manage the user email address + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, "")); + } + + + /** + * 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 + */ + @Override + public boolean supportsTeamMembershipChanges() { + return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false); + } + + @Override + public AccountType getAccountType() { + return AccountType.LDAP; + } + + @Override + public UserModel authenticate(String username, char[] password) { + String simpleUsername = getSimpleUsername(username); + + LDAPConnection ldapConnection = getLdapConnection(); + if (ldapConnection != null) { + try { + // 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}", escapeLDAPSearchFilter(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("LDAP authenticated: " + username); + + UserModel user = null; + synchronized (this) { + user = userManager.getUserModel(simpleUsername); + if (user == null) // create user object for new authenticated user + user = new UserModel(simpleUsername); + + // create a user cookie + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + new String(password)); + } + + if (!supportsTeamMembershipChanges()) + getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user); + + // Get User Attributes + setUserAttributes(user, loggingInUser); + + // Push the ldap looked up values to backing file + updateUser(user); + + if (!supportsTeamMembershipChanges()) { + for (TeamModel userTeam : user.teams) + updateTeam(userTeam); + } + } + + return user; + } + } + } finally { + ldapConnection.close(); + } + } + return null; + } + + /** + * Set the admin attribute from team memberships retrieved from LDAP. + * If we are not storing teams in LDAP and/or we have not defined any + * administrator teams, then do not change the admin flag. + * + * @param user + */ + private void setAdminAttribute(UserModel user) { + if (!supportsTeamMembershipChanges()) { + List<String> admins = settings.getStrings(Keys.realm.ldap.admins); + // if we have defined administrative teams, then set admin flag + // otherwise leave admin flag unchanged + if (!ArrayUtils.isEmpty(admins)) { + 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 setUserAttributes(UserModel user, SearchResultEntry userEntry) { + // Is this user an admin? + setAdminAttribute(user); + + // Don't want visibility into the real password, make up a dummy + user.password = Constants.EXTERNAL_ACCOUNT; + user.accountType = getAccountType(); + + // Get full name Attribute + String displayName = settings.getString(Keys.realm.ldap.displayName, ""); + if (!StringUtils.isEmpty(displayName)) { + // Replace embedded ${} with attributes + if (displayName.contains("${")) { + for (Attribute userAttribute : userEntry.getAttributes()) + displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue()); + + user.displayName = displayName; + } else { + Attribute attribute = userEntry.getAttribute(displayName); + if (attribute != null && attribute.hasValue()) { + user.displayName = attribute.getValue(); + } + } + } + + // Get email address Attribute + String email = settings.getString(Keys.realm.ldap.email, ""); + if (!StringUtils.isEmpty(email)) { + if (email.contains("${")) { + for (Attribute userAttribute : userEntry.getAttributes()) + email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue()); + + user.emailAddress = email; + } else { + Attribute attribute = userEntry.getAttribute(email); + if (attribute != null && attribute.hasValue()) { + user.emailAddress = attribute.getValue(); + } + } + } + } + + 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}", escapeLDAPSearchFilter(loggingInUserDN)); + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); + + // Fill in attributes into groupMemberPattern + for (Attribute userAttribute : loggingInUser.getAttributes()) { + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue())); + } + + SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn")); + 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 = userManager.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")); + answer.accountType = getAccountType(); + // potentially retrieve other attributes here in the future + + return answer; + } + + private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) { + try { + return ldapConnection.search(base, SearchScope.SUB, filter); + } catch (LDAPSearchException e) { + logger.error("Problem Searching LDAP", e); + + return null; + } + } + + private SearchResult doSearch(LDAPConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) { + try { + SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter); + if (dereferenceAliases) { + searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING); + } + if (attributes != null) { + searchRequest.setAttributes(attributes); + } + return ldapConnection.search(searchRequest); + + } catch (LDAPSearchException e) { + logger.error("Problem Searching LDAP", e); + + return null; + } catch (LDAPException e) { + logger.error("Problem creating LDAP search", e); + return null; + } + } + + private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) { + try { + // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN + ldapConnection.bind(userDn, password); + return true; + } catch (LDAPException e) { + logger.error("Error authenticating user", e); + return false; + } + } + + /** + * Returns a simple username without any domain prefixes. + * + * @param username + * @return a simple username + */ + protected String getSimpleUsername(String username) { + int lastSlash = username.lastIndexOf('\\'); + if (lastSlash > -1) { + username = username.substring(lastSlash + 1); + } + + return username; + } + + // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java + public static final String escapeLDAPSearchFilter(String filter) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < filter.length(); i++) { + char curChar = filter.charAt(i); + switch (curChar) { + case '\\': + sb.append("\\5c"); + break; + case '*': + sb.append("\\2a"); + break; + case '(': + sb.append("\\28"); + break; + case ')': + sb.append("\\29"); + break; + case '\u0000': + sb.append("\\00"); + break; + default: + sb.append(curChar); + } + } + return sb.toString(); + } +} -- Gitblit v1.9.1