Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSH LDAP key manager #1160

Merged
merged 7 commits into from
Dec 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/main/distrib/data/defaults.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1935,6 +1935,22 @@ realm.ldap.email = email
# SINCE 1.0.0
realm.ldap.uid = uid

# Attribute on the USER record that indicates their public SSH key.
# Leave blank when public SSH keys shall not be retrieved from LDAP.
#
# This setting is only relevant when a public key manager is used that
# retrieves SSH keys from LDAP (e.g. com.gitblit.transport.ssh.LdapKeyManager).
#
# The accepted format of the value is dependent on the public key manager used.
# Examples:
# sshPublicKey - Use the attribute 'sshPublicKey' on the user record.
# altSecurityIdentities:SshKey - Use the attribute 'altSecurityIdentities'
# on the user record, for which the record value
# starts with 'SshKey:', followed by the SSH key entry.
#
# SINCE 1.9.0
realm.ldap.sshPublicKey =

# Defines whether to synchronize all LDAP users and teams into the user service
# This requires either anonymous LDAP access or that a specific account is set
# in realm.ldap.username and realm.ldap.password, that has permission to read
Expand Down
284 changes: 10 additions & 274 deletions src/main/java/com/gitblit/auth/LdapAuthProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
*/
package com.gitblit.auth;

import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
Expand All @@ -33,28 +30,20 @@
import com.gitblit.Constants.Role;
import com.gitblit.Keys;
import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
import com.gitblit.ldap.LdapConnection;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.service.LdapSyncService;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.BindResult;
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.
Expand Down Expand Up @@ -109,7 +98,7 @@ public synchronized void sync() {
if (enabled) {
logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true);
LdapConnection ldapConnection = new LdapConnection();
LdapConnection ldapConnection = new LdapConnection(settings);
if (ldapConnection.connect()) {
if (ldapConnection.bind() == null) {
ldapConnection.close();
Expand All @@ -118,9 +107,9 @@ public synchronized void sync() {
}

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}))");
String accountBase = ldapConnection.getAccountBase();
String accountPattern = ldapConnection.getAccountPattern();
accountPattern = StringUtils.replace(accountPattern, "${username}", "*");

SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
Expand Down Expand Up @@ -265,7 +254,7 @@ public AccountType getAccountType() {
public UserModel authenticate(String username, char[] password) {
String simpleUsername = getSimpleUsername(username);

LdapConnection ldapConnection = new LdapConnection();
LdapConnection ldapConnection = new LdapConnection(settings);
if (ldapConnection.connect()) {

// Try to bind either to the "manager" account,
Expand All @@ -286,11 +275,7 @@ public UserModel authenticate(String username, char[] password) {

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);
SearchResult result = ldapConnection.searchUser(simpleUsername);
if (result != null && result.getEntryCount() == 1) {
SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
String loggingInUserDN = loggingInUser.getDN();
Expand Down Expand Up @@ -441,12 +426,12 @@ private void getTeamsFromLdap(LdapConnection ldapConnection, String simpleUserna
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));
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", LdapConnection.escapeLDAPSearchFilter(loggingInUserDN));
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", LdapConnection.escapeLDAPSearchFilter(simpleUsername));

// Fill in attributes into groupMemberPattern
for (Attribute userAttribute : loggingInUser.getAttributes()) {
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", LdapConnection.escapeLDAPSearchFilter(userAttribute.getValue()));
}

SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn"));
Expand Down Expand Up @@ -538,6 +523,7 @@ private SearchResult doSearch(LdapConnection ldapConnection, String base, String




/**
* Returns a simple username without any domain prefixes.
*
Expand All @@ -553,34 +539,6 @@ protected String getSimpleUsername(String username) {
return username;
}

// From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
private 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();
}

private void configureSyncService() {
LdapSyncService ldapSyncService = new LdapSyncService(settings, this);
if (ldapSyncService.isReady()) {
Expand All @@ -593,226 +551,4 @@ private void configureSyncService() {
logger.info("Ldap sync service is disabled.");
}
}



private class LdapConnection {
private LDAPConnection conn;
private SimpleBindRequest currentBindRequest;
private SimpleBindRequest managerBindRequest;
private SimpleBindRequest userBindRequest;


public LdapConnection() {
String bindUserName = settings.getString(Keys.realm.ldap.username, "");
String bindPassword = settings.getString(Keys.realm.ldap.password, "");
if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
this.managerBindRequest = new SimpleBindRequest();
}
this.managerBindRequest = new SimpleBindRequest(bindUserName, bindPassword);
}


boolean connect() {
try {
URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
String ldapHost = ldapUrl.getHost();
int ldapPort = ldapUrl.getPort();

if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
// SSL
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
if (ldapPort == -1) {
ldapPort = 636;
}
} else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
// no encryption or StartTLS
conn = new LDAPConnection();
if (ldapPort == -1) {
ldapPort = 389;
}
} else {
logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
return false;
}

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());
}
}

return true;

} 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 false;
}


void close() {
if (conn != null) {
conn.close();
}
}


SearchResult search(SearchRequest request) {
try {
return conn.search(request);
} catch (LDAPSearchException e) {
logger.error("Problem Searching LDAP [{}]", e.getResultCode());
return e.getSearchResult();
}
}


SearchResult search(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);
}
SearchResult result = search(searchRequest);
return result;

} catch (LDAPException e) {
logger.error("Problem creating LDAP search", e);
return null;
}
}



/**
* Bind using the manager credentials set in realm.ldap.username and ..password
* @return A bind result, or null if binding failed.
*/
BindResult bind() {
BindResult result = null;
try {
result = conn.bind(managerBindRequest);
currentBindRequest = managerBindRequest;
} catch (LDAPException e) {
logger.error("Error authenticating to LDAP with manager account to search the directory.");
logger.error(" Please check your settings for realm.ldap.username and realm.ldap.password.");
logger.debug(" Received exception when binding to LDAP", e);
return null;
}
return result;
}


/**
* Bind using the given credentials, by filling in the username in the given {@code bindPattern} to
* create the DN.
* @return A bind result, or null if binding failed.
*/
BindResult bind(String bindPattern, String simpleUsername, String password) {
BindResult result = null;
try {
String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
SimpleBindRequest request = new SimpleBindRequest(bindUser, password);
result = conn.bind(request);
userBindRequest = request;
currentBindRequest = userBindRequest;
} catch (LDAPException e) {
logger.error("Error authenticating to LDAP with user account to search the directory.");
logger.error(" Please check your settings for realm.ldap.bindpattern.");
logger.debug(" Received exception when binding to LDAP", e);
return null;
}
return result;
}


boolean rebindAsUser() {
if (userBindRequest == null || currentBindRequest == userBindRequest) {
return false;
}
try {
conn.bind(userBindRequest);
currentBindRequest = userBindRequest;
} catch (LDAPException e) {
conn.close();
logger.error("Error rebinding to LDAP with user account.", e);
return false;
}
return true;
}


boolean isAuthenticated(String userDn, String password) {
verifyCurrentBinding();

// If the currently bound DN is already the DN of the logging in user, authentication has already happened
// during the previous bind operation. We accept this and return with the current bind left in place.
// This could also be changed to always retry binding as the logging in user, to make sure that the
// connection binding has not been tampered with in between. So far I see no way how this could happen
// and thus skip the repeated binding.
// This check also makes sure that the DN in realm.ldap.bindpattern actually matches the DN that was found
// when searching the user entry.
String boundDN = currentBindRequest.getBindDN();
if (boundDN != null && boundDN.equals(userDn)) {
return true;
}

// Bind a the logging in user to check for authentication.
// Afterwards, bind as the original bound DN again, to restore the previous authorization.
boolean isAuthenticated = false;
try {
// Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
SimpleBindRequest ubr = new SimpleBindRequest(userDn, password);
conn.bind(ubr);
isAuthenticated = true;
userBindRequest = ubr;
} catch (LDAPException e) {
logger.error("Error authenticating user ({})", userDn, e);
}

try {
conn.bind(currentBindRequest);
} catch (LDAPException e) {
logger.error("Error reinstating original LDAP authorization (code {}). Team information may be inaccurate for this log in.",
e.getResultCode(), e);
}
return isAuthenticated;
}



private boolean verifyCurrentBinding() {
BindRequest lastBind = conn.getLastBindRequest();
if (lastBind == currentBindRequest) {
return true;
}
logger.debug("Unexpected binding in LdapConnection. {} != {}", lastBind, currentBindRequest);

String lastBoundDN = ((SimpleBindRequest)lastBind).getBindDN();
String boundDN = currentBindRequest.getBindDN();
logger.debug("Currently bound as '{}', check authentication for '{}'", lastBoundDN, boundDN);
if (boundDN != null && ! boundDN.equals(lastBoundDN)) {
logger.warn("Unexpected binding DN in LdapConnection. '{}' != '{}'.", lastBoundDN, boundDN);
logger.warn("Updated binding information in LDAP connection.");
currentBindRequest = (SimpleBindRequest)lastBind;
return false;
}
return true;
}
}
}