From d98cdd29178b5623032b7d340658221549114361 Mon Sep 17 00:00:00 2001 From: Nick Couchman Date: Fri, 17 Mar 2017 16:16:04 -0400 Subject: [PATCH] GUACAMOLE-243: Implement LDAP referral handling in Guacamole LDAP extension. --- .../auth/ldap/ConfigurationService.java | 84 +++++++++++++++- .../auth/ldap/LDAPConnectionService.java | 22 +++++ .../auth/ldap/LDAPGuacamoleProperties.java | 41 ++++++++ .../auth/ldap/ReferralAuthHandler.java | 97 +++++++++++++++++++ .../guacamole/auth/ldap/user/UserService.java | 39 +++++--- 5 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java index c7e4819d1..0b6f9e9e9 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java @@ -251,6 +251,24 @@ public class ConfigurationService { ); } + /** + * Returns the boolean value for whether the connection should + * follow referrals or not. By default, it will not. + * + * @return + * The boolean value of whether to follow referrals + * as configured in guacamole.properties + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public boolean getFollowReferrals() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS, + false + ); + } + /** * Returns a set of LDAPSearchConstraints to apply globally * to all LDAP searches. @@ -272,6 +290,23 @@ public class ConfigurationService { return constraints; } + /** + * Returns the maximum number of referral hops to follow. + * + * @return + * The maximum number of referral hops to follow + * as configured in guacamole.properties + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getMaxReferralHops() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS, + 5 + ); + } + /** * Returns the search filter that should be used when querying the * LDAP server for Guacamole users. If no filter is specified, @@ -281,7 +316,6 @@ public class ConfigurationService { * The search filter that should be used when querying the * LDAP server for users that are valid in Guacamole, or * "(objectClass=*)" if not specified. - * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ @@ -292,4 +326,52 @@ public class ConfigurationService { ); } + /** + * Returns the authentication method to use during referral following. + * + * @return + * The authentication method to use during referral following + * as configured in guacamole.properties or as derived from + * other configuration options. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public String getReferralAuthentication() throws GuacamoleException { + String confMethod = environment.getProperty( + LDAPGuacamoleProperties.LDAP_REFERRAL_AUTHENTICATION + ); + + if (confMethod == null) + + if (getSearchBindDN() != null && getSearchBindPassword() != null) + return "bind"; + + else + return "anonymous"; + + else if (confMethod.equals("bind") && (getSearchBindDN() == null || getSearchBindPassword() == null)) + throw new GuacamoleException("Referral is set to bind with credentials, but credentials are not configured."); + + return confMethod; + + } + + /** + * Returns the maximum number of seconds to wait for LDAP operations + * + * @return + * The maximum number of seconds to wait for LDAP operations + * as configured in guacamole.properties + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getOperationTimeout() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_OPERATION_TIMEOUT, + 30 + ); + } + } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java index bf0534c64..c3b2e12fd 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java @@ -21,12 +21,14 @@ package org.apache.guacamole.auth.ldap; import com.google.inject.Inject; import com.novell.ldap.LDAPConnection; +import com.novell.ldap.LDAPConstraints; import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPJSSESecureSocketFactory; import com.novell.ldap.LDAPJSSEStartTLSFactory; import java.io.UnsupportedEncodingException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleUnsupportedException; +import org.apache.guacamole.auth.ldap.ReferralAuthHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -111,6 +113,26 @@ public class LDAPConnectionService { // Obtain appropriately-configured LDAPConnection instance LDAPConnection ldapConnection = createLDAPConnection(); + // Configure LDAP connection constraints + LDAPConstraints ldapConstraints = ldapConnection.getConstraints(); + if (ldapConstraints == null) + ldapConstraints = new LDAPConstraints(); + + // Set whether or not we follow referrals, and max hops + ldapConstraints.setReferralFollowing(confService.getFollowReferrals()); + String refAuthMethod = confService.getReferralAuthentication(); + + if (refAuthMethod != null && refAuthMethod.equals("bind")) + ldapConstraints.setReferralHandler(new ReferralAuthHandler(userDN, password)); + + ldapConstraints.setHopLimit(confService.getMaxReferralHops()); + + // Set timelimit to wait for LDAP operations, converting to ms + ldapConstraints.setTimeLimit(confService.getOperationTimeout() * 1000); + + // Apply the constraints to the connection + ldapConnection.setConstraints(ldapConstraints); + try { // Connect to LDAP server diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java index e13264dd8..7a1dcadf6 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java @@ -19,6 +19,7 @@ package org.apache.guacamole.auth.ldap; +import org.apache.guacamole.properties.BooleanGuacamoleProperty; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; @@ -174,4 +175,44 @@ public class LDAPGuacamoleProperties { }; + /** + * Whether or not we should follow referrals + */ + public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS = new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "ldap-follow-referrals"; } + + }; + + /** + * Maximum number of referral hops to follow + */ + public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS = new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "ldap-max-referral-hops"; } + + }; + + /** + * Authentication method to use to follow referrals + */ + public static final StringGuacamoleProperty LDAP_REFERRAL_AUTHENTICATION = new StringGuacamoleProperty() { + + @Override + public String getName() { return "ldap-referral-authentication"; } + + }; + + /** + * Number of seconds to wait for LDAP operations to complete + */ + public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT = new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "ldap-operation-timeout"; } + + }; + } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java new file mode 100644 index 000000000..21a7644c3 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.guacamole.auth.ldap; + +import com.google.inject.Inject; +import com.novell.ldap.LDAPAuthHandler; +import com.novell.ldap.LDAPAuthProvider; +import com.novell.ldap.LDAPConnection; +import java.io.UnsupportedEncodingException; +import org.apache.guacamole.GuacamoleException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReferralAuthHandler implements LDAPAuthHandler { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(ReferralAuthHandler.class); + + /** + * The LDAPAuthProvider object that will be set and returned to the referral handler. + */ + private final LDAPAuthProvider ldapAuth; + + /** + * Service for retrieving LDAP server configuration information. + */ + @Inject + private ConfigurationService confService; + + + public ReferralAuthHandler() throws GuacamoleException { + String binddn = confService.getSearchBindDN(); + String password = confService.getSearchBindPassword(); + byte[] passwordBytes; + try { + + // Convert password into corresponding byte array + if (password != null) + passwordBytes = password.getBytes("UTF-8"); + else + passwordBytes = null; + + } + catch (UnsupportedEncodingException e) { + logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage()); + logger.debug("Support for UTF-8 (as required by Java spec) not found.", e); + throw new GuacamoleException("Could not set password due to missing support for UTF-8 encoding."); + } + + ldapAuth = new LDAPAuthProvider(binddn, passwordBytes); + + } + + public ReferralAuthHandler(String dn, String password) throws GuacamoleException { + byte[] passwordBytes; + try { + + // Convert password into corresponding byte array + if (password != null) + passwordBytes = password.getBytes("UTF-8"); + else + passwordBytes = null; + + } + catch (UnsupportedEncodingException e) { + logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage()); + logger.debug("Support for UTF-8 (as required by Java spec) not found.", e); + throw new GuacamoleException("Could not set password due to missing UTF-8 support."); + } + ldapAuth = new LDAPAuthProvider(dn, passwordBytes); + } + + @Override + public LDAPAuthProvider getAuthProvider(String host, int port) { + return ldapAuth; + } + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java index 91f1636e5..087365f1b 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java @@ -24,6 +24,7 @@ import com.novell.ldap.LDAPAttribute; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPException; +import com.novell.ldap.LDAPReferralException; import com.novell.ldap.LDAPSearchResults; import java.util.ArrayList; import java.util.HashMap; @@ -107,19 +108,35 @@ public class UserService { // Read all visible users while (results.hasMore()) { - LDAPEntry entry = results.next(); + try { + + LDAPEntry entry = results.next(); + + // Get username from record + LDAPAttribute username = entry.getAttribute(usernameAttribute); + if (username == null) { + logger.warn("Queried user is missing the username attribute \"{}\".", usernameAttribute); + continue; + } + + // Store user using their username as the identifier + String identifier = username.getStringValue(); + if (users.put(identifier, new SimpleUser(identifier)) != null) + logger.warn("Possibly ambiguous user account: \"{}\".", identifier); - // Get username from record - LDAPAttribute username = entry.getAttribute(usernameAttribute); - if (username == null) { - logger.warn("Queried user is missing the username attribute \"{}\".", usernameAttribute); - continue; } - - // Store user using their username as the identifier - String identifier = username.getStringValue(); - if (users.put(identifier, new SimpleUser(identifier)) != null) - logger.warn("Possibly ambiguous user account: \"{}\".", identifier); + catch (LDAPReferralException e) { + if (confService.getFollowReferrals()) { + logger.error("Could not follow referral.", e.getMessage()); + logger.debug("Error encountered trying to follow referral.", e); + throw new GuacamoleException("Could not follow LDAP referral."); + } + else { + logger.warn("Encountered a referral, but not following it.", e.getMessage()); + logger.debug("Got a referral, but not configured to follow it.", e); + continue; + } + } }