Skip to content

Commit d6ddafd

Browse files
authored
Merge pull request #1160 from fzs/sshLdapAuthenticator
LDAP SSH key manager
2 parents 34b98a0 + 1afeccc commit d6ddafd

14 files changed

+2276
-625
lines changed

src/main/distrib/data/defaults.properties

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,6 +1950,22 @@ realm.ldap.email = email
19501950
# SINCE 1.0.0
19511951
realm.ldap.uid = uid
19521952

1953+
# Attribute on the USER record that indicates their public SSH key.
1954+
# Leave blank when public SSH keys shall not be retrieved from LDAP.
1955+
#
1956+
# This setting is only relevant when a public key manager is used that
1957+
# retrieves SSH keys from LDAP (e.g. com.gitblit.transport.ssh.LdapKeyManager).
1958+
#
1959+
# The accepted format of the value is dependent on the public key manager used.
1960+
# Examples:
1961+
# sshPublicKey - Use the attribute 'sshPublicKey' on the user record.
1962+
# altSecurityIdentities:SshKey - Use the attribute 'altSecurityIdentities'
1963+
# on the user record, for which the record value
1964+
# starts with 'SshKey:', followed by the SSH key entry.
1965+
#
1966+
# SINCE 1.9.0
1967+
realm.ldap.sshPublicKey =
1968+
19531969
# Defines whether to synchronize all LDAP users and teams into the user service
19541970
# This requires either anonymous LDAP access or that a specific account is set
19551971
# in realm.ldap.username and realm.ldap.password, that has permission to read

src/main/java/com/gitblit/auth/LdapAuthProvider.java

Lines changed: 10 additions & 274 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616
*/
1717
package com.gitblit.auth;
1818

19-
import java.net.URI;
20-
import java.net.URISyntaxException;
21-
import java.security.GeneralSecurityException;
2219
import java.text.MessageFormat;
2320
import java.util.Arrays;
2421
import java.util.HashMap;
@@ -33,28 +30,20 @@
3330
import com.gitblit.Constants.Role;
3431
import com.gitblit.Keys;
3532
import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
33+
import com.gitblit.ldap.LdapConnection;
3634
import com.gitblit.models.TeamModel;
3735
import com.gitblit.models.UserModel;
3836
import com.gitblit.service.LdapSyncService;
3937
import com.gitblit.utils.ArrayUtils;
4038
import com.gitblit.utils.StringUtils;
4139
import com.unboundid.ldap.sdk.Attribute;
42-
import com.unboundid.ldap.sdk.BindRequest;
4340
import com.unboundid.ldap.sdk.BindResult;
44-
import com.unboundid.ldap.sdk.DereferencePolicy;
45-
import com.unboundid.ldap.sdk.ExtendedResult;
46-
import com.unboundid.ldap.sdk.LDAPConnection;
4741
import com.unboundid.ldap.sdk.LDAPException;
48-
import com.unboundid.ldap.sdk.LDAPSearchException;
4942
import com.unboundid.ldap.sdk.ResultCode;
5043
import com.unboundid.ldap.sdk.SearchRequest;
5144
import com.unboundid.ldap.sdk.SearchResult;
5245
import com.unboundid.ldap.sdk.SearchResultEntry;
5346
import com.unboundid.ldap.sdk.SearchScope;
54-
import com.unboundid.ldap.sdk.SimpleBindRequest;
55-
import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
56-
import com.unboundid.util.ssl.SSLUtil;
57-
import com.unboundid.util.ssl.TrustAllTrustManager;
5847

5948
/**
6049
* Implementation of an LDAP user service.
@@ -109,7 +98,7 @@ public synchronized void sync() {
10998
if (enabled) {
11099
logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
111100
final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true);
112-
LdapConnection ldapConnection = new LdapConnection();
101+
LdapConnection ldapConnection = new LdapConnection(settings);
113102
if (ldapConnection.connect()) {
114103
if (ldapConnection.bind() == null) {
115104
ldapConnection.close();
@@ -118,9 +107,9 @@ public synchronized void sync() {
118107
}
119108

120109
try {
121-
String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
122110
String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
123-
String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
111+
String accountBase = ldapConnection.getAccountBase();
112+
String accountPattern = ldapConnection.getAccountPattern();
124113
accountPattern = StringUtils.replace(accountPattern, "${username}", "*");
125114

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

268-
LdapConnection ldapConnection = new LdapConnection();
257+
LdapConnection ldapConnection = new LdapConnection(settings);
269258
if (ldapConnection.connect()) {
270259

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

287276
try {
288277
// Find the logging in user's DN
289-
String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
290-
String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
291-
accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
292-
293-
SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
278+
SearchResult result = ldapConnection.searchUser(simpleUsername);
294279
if (result != null && result.getEntryCount() == 1) {
295280
SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
296281
String loggingInUserDN = loggingInUser.getDN();
@@ -441,12 +426,12 @@ private void getTeamsFromLdap(LdapConnection ldapConnection, String simpleUserna
441426
String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
442427
String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
443428

444-
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
445-
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
429+
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", LdapConnection.escapeLDAPSearchFilter(loggingInUserDN));
430+
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", LdapConnection.escapeLDAPSearchFilter(simpleUsername));
446431

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

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

539524

540525

526+
541527
/**
542528
* Returns a simple username without any domain prefixes.
543529
*
@@ -553,34 +539,6 @@ protected String getSimpleUsername(String username) {
553539
return username;
554540
}
555541

556-
// From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
557-
private static final String escapeLDAPSearchFilter(String filter) {
558-
StringBuilder sb = new StringBuilder();
559-
for (int i = 0; i < filter.length(); i++) {
560-
char curChar = filter.charAt(i);
561-
switch (curChar) {
562-
case '\\':
563-
sb.append("\\5c");
564-
break;
565-
case '*':
566-
sb.append("\\2a");
567-
break;
568-
case '(':
569-
sb.append("\\28");
570-
break;
571-
case ')':
572-
sb.append("\\29");
573-
break;
574-
case '\u0000':
575-
sb.append("\\00");
576-
break;
577-
default:
578-
sb.append(curChar);
579-
}
580-
}
581-
return sb.toString();
582-
}
583-
584542
private void configureSyncService() {
585543
LdapSyncService ldapSyncService = new LdapSyncService(settings, this);
586544
if (ldapSyncService.isReady()) {
@@ -593,226 +551,4 @@ private void configureSyncService() {
593551
logger.info("Ldap sync service is disabled.");
594552
}
595553
}
596-
597-
598-
599-
private class LdapConnection {
600-
private LDAPConnection conn;
601-
private SimpleBindRequest currentBindRequest;
602-
private SimpleBindRequest managerBindRequest;
603-
private SimpleBindRequest userBindRequest;
604-
605-
606-
public LdapConnection() {
607-
String bindUserName = settings.getString(Keys.realm.ldap.username, "");
608-
String bindPassword = settings.getString(Keys.realm.ldap.password, "");
609-
if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
610-
this.managerBindRequest = new SimpleBindRequest();
611-
}
612-
this.managerBindRequest = new SimpleBindRequest(bindUserName, bindPassword);
613-
}
614-
615-
616-
boolean connect() {
617-
try {
618-
URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
619-
String ldapHost = ldapUrl.getHost();
620-
int ldapPort = ldapUrl.getPort();
621-
622-
if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
623-
// SSL
624-
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
625-
conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
626-
if (ldapPort == -1) {
627-
ldapPort = 636;
628-
}
629-
} else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
630-
// no encryption or StartTLS
631-
conn = new LDAPConnection();
632-
if (ldapPort == -1) {
633-
ldapPort = 389;
634-
}
635-
} else {
636-
logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
637-
return false;
638-
}
639-
640-
conn.connect(ldapHost, ldapPort);
641-
642-
if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
643-
SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
644-
ExtendedResult extendedResult = conn.processExtendedOperation(
645-
new StartTLSExtendedRequest(sslUtil.createSSLContext()));
646-
if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
647-
throw new LDAPException(extendedResult.getResultCode());
648-
}
649-
}
650-
651-
return true;
652-
653-
} catch (URISyntaxException e) {
654-
logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
655-
} catch (GeneralSecurityException e) {
656-
logger.error("Unable to create SSL Connection", e);
657-
} catch (LDAPException e) {
658-
logger.error("Error Connecting to LDAP", e);
659-
}
660-
661-
return false;
662-
}
663-
664-
665-
void close() {
666-
if (conn != null) {
667-
conn.close();
668-
}
669-
}
670-
671-
672-
SearchResult search(SearchRequest request) {
673-
try {
674-
return conn.search(request);
675-
} catch (LDAPSearchException e) {
676-
logger.error("Problem Searching LDAP [{}]", e.getResultCode());
677-
return e.getSearchResult();
678-
}
679-
}
680-
681-
682-
SearchResult search(String base, boolean dereferenceAliases, String filter, List<String> attributes) {
683-
try {
684-
SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
685-
if (dereferenceAliases) {
686-
searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
687-
}
688-
if (attributes != null) {
689-
searchRequest.setAttributes(attributes);
690-
}
691-
SearchResult result = search(searchRequest);
692-
return result;
693-
694-
} catch (LDAPException e) {
695-
logger.error("Problem creating LDAP search", e);
696-
return null;
697-
}
698-
}
699-
700-
701-
702-
/**
703-
* Bind using the manager credentials set in realm.ldap.username and ..password
704-
* @return A bind result, or null if binding failed.
705-
*/
706-
BindResult bind() {
707-
BindResult result = null;
708-
try {
709-
result = conn.bind(managerBindRequest);
710-
currentBindRequest = managerBindRequest;
711-
} catch (LDAPException e) {
712-
logger.error("Error authenticating to LDAP with manager account to search the directory.");
713-
logger.error(" Please check your settings for realm.ldap.username and realm.ldap.password.");
714-
logger.debug(" Received exception when binding to LDAP", e);
715-
return null;
716-
}
717-
return result;
718-
}
719-
720-
721-
/**
722-
* Bind using the given credentials, by filling in the username in the given {@code bindPattern} to
723-
* create the DN.
724-
* @return A bind result, or null if binding failed.
725-
*/
726-
BindResult bind(String bindPattern, String simpleUsername, String password) {
727-
BindResult result = null;
728-
try {
729-
String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
730-
SimpleBindRequest request = new SimpleBindRequest(bindUser, password);
731-
result = conn.bind(request);
732-
userBindRequest = request;
733-
currentBindRequest = userBindRequest;
734-
} catch (LDAPException e) {
735-
logger.error("Error authenticating to LDAP with user account to search the directory.");
736-
logger.error(" Please check your settings for realm.ldap.bindpattern.");
737-
logger.debug(" Received exception when binding to LDAP", e);
738-
return null;
739-
}
740-
return result;
741-
}
742-
743-
744-
boolean rebindAsUser() {
745-
if (userBindRequest == null || currentBindRequest == userBindRequest) {
746-
return false;
747-
}
748-
try {
749-
conn.bind(userBindRequest);
750-
currentBindRequest = userBindRequest;
751-
} catch (LDAPException e) {
752-
conn.close();
753-
logger.error("Error rebinding to LDAP with user account.", e);
754-
return false;
755-
}
756-
return true;
757-
}
758-
759-
760-
boolean isAuthenticated(String userDn, String password) {
761-
verifyCurrentBinding();
762-
763-
// If the currently bound DN is already the DN of the logging in user, authentication has already happened
764-
// during the previous bind operation. We accept this and return with the current bind left in place.
765-
// This could also be changed to always retry binding as the logging in user, to make sure that the
766-
// connection binding has not been tampered with in between. So far I see no way how this could happen
767-
// and thus skip the repeated binding.
768-
// This check also makes sure that the DN in realm.ldap.bindpattern actually matches the DN that was found
769-
// when searching the user entry.
770-
String boundDN = currentBindRequest.getBindDN();
771-
if (boundDN != null && boundDN.equals(userDn)) {
772-
return true;
773-
}
774-
775-
// Bind a the logging in user to check for authentication.
776-
// Afterwards, bind as the original bound DN again, to restore the previous authorization.
777-
boolean isAuthenticated = false;
778-
try {
779-
// Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
780-
SimpleBindRequest ubr = new SimpleBindRequest(userDn, password);
781-
conn.bind(ubr);
782-
isAuthenticated = true;
783-
userBindRequest = ubr;
784-
} catch (LDAPException e) {
785-
logger.error("Error authenticating user ({})", userDn, e);
786-
}
787-
788-
try {
789-
conn.bind(currentBindRequest);
790-
} catch (LDAPException e) {
791-
logger.error("Error reinstating original LDAP authorization (code {}). Team information may be inaccurate for this log in.",
792-
e.getResultCode(), e);
793-
}
794-
return isAuthenticated;
795-
}
796-
797-
798-
799-
private boolean verifyCurrentBinding() {
800-
BindRequest lastBind = conn.getLastBindRequest();
801-
if (lastBind == currentBindRequest) {
802-
return true;
803-
}
804-
logger.debug("Unexpected binding in LdapConnection. {} != {}", lastBind, currentBindRequest);
805-
806-
String lastBoundDN = ((SimpleBindRequest)lastBind).getBindDN();
807-
String boundDN = currentBindRequest.getBindDN();
808-
logger.debug("Currently bound as '{}', check authentication for '{}'", lastBoundDN, boundDN);
809-
if (boundDN != null && ! boundDN.equals(lastBoundDN)) {
810-
logger.warn("Unexpected binding DN in LdapConnection. '{}' != '{}'.", lastBoundDN, boundDN);
811-
logger.warn("Updated binding information in LDAP connection.");
812-
currentBindRequest = (SimpleBindRequest)lastBind;
813-
return false;
814-
}
815-
return true;
816-
}
817-
}
818554
}

0 commit comments

Comments
 (0)