From 07b443257f901fcee1068d4be48f001ec402e990 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 20 Oct 2021 18:45:06 -0700 Subject: [PATCH 01/14] GUACAMOLE-957: Refactor LDAP configuration such that it may be user-specific. --- .../ldap/AuthenticationProviderService.java | 60 ++- .../auth/ldap/LDAPConnectionService.java | 62 +-- .../auth/ldap/ObjectQueryService.java | 40 +- .../auth/ldap/conf/ConfigurationService.java | 379 +----------------- .../conf/EnvironmentLDAPConfiguration.java | 209 ++++++++++ .../auth/ldap/conf/LDAPConfiguration.java | 303 ++++++++++++++ .../ldap/connection/ConnectionService.java | 36 +- .../auth/ldap/group/UserGroupService.java | 75 ++-- .../auth/ldap/user/LDAPAuthenticatedUser.java | 28 +- .../auth/ldap/user/LDAPUserContext.java | 11 +- .../guacamole/auth/ldap/user/UserService.java | 50 ++- 11 files changed, 738 insertions(+), 515 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java index 833674c1b..856a536e0 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java @@ -35,6 +35,7 @@ import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.ldap.conf.ConfigurationService; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser; import org.apache.guacamole.auth.ldap.user.LDAPUserContext; @@ -105,6 +106,9 @@ public class AuthenticationProviderService { * or queried from the LDAP server, depending on how LDAP authentication * has been configured. * + * @param config + * The configuration of the LDAP server being queried. + * * @param username * The username of the user whose corresponding DN should be returned. * @@ -115,18 +119,17 @@ public class AuthenticationProviderService { * If required properties are missing, and thus the user DN cannot be * determined. */ - private Dn getUserBindDN(String username) throws GuacamoleException { + private Dn getUserBindDN(LDAPConfiguration config, String username) + throws GuacamoleException { // If a search DN is provided, search the LDAP directory for the DN // corresponding to the given username - String searchBindLogon = confService.getSearchBindDN(); + String searchBindLogon = config.getSearchBindDN(); if (searchBindLogon != null) { // Create an LDAP connection using the search account - LdapNetworkConnection searchConnection = ldapService.bindAs( - searchBindLogon, - confService.getSearchBindPassword() - ); + LdapNetworkConnection searchConnection = ldapService.bindAs(config, + searchBindLogon, config.getSearchBindPassword()); // Warn of failure to find if (searchConnection == null) { @@ -138,7 +141,7 @@ public class AuthenticationProviderService { try { // Retrieve all DNs associated with the given username - List userDNs = userService.getUserDNs(searchConnection, username); + List userDNs = userService.getUserDNs(config, searchConnection, username); if (userDNs.isEmpty()) return null; @@ -161,7 +164,7 @@ public class AuthenticationProviderService { } // Otherwise, derive user DN from base DN - return userService.deriveUserDN(username); + return userService.deriveUserDN(config, username); } @@ -196,8 +199,15 @@ public class AuthenticationProviderService { "Anonymous bind is not currently allowed by the LDAP" + " authentication provider.", CredentialsInfo.USERNAME_PASSWORD); } - - Dn bindDn = getUserBindDN(username); + + // Get relevant LDAP configuration for user + LDAPConfiguration config = confService.getLDAPConfiguration(username); + if (config == null) { + throw new GuacamoleInvalidCredentialsException("User \"" + username + "\" " + + "does not map to any defined LDAP configuration.", CredentialsInfo.USERNAME_PASSWORD); + } + + Dn bindDn = getUserBindDN(config, username); if (bindDn == null || bindDn.isEmpty()) { throw new GuacamoleInvalidCredentialsException("Unable to determine" + " DN of user " + username, CredentialsInfo.USERNAME_PASSWORD); @@ -205,7 +215,7 @@ public class AuthenticationProviderService { // Attempt bind LdapNetworkConnection ldapConnection = - ldapService.bindAs(bindDn.getName(), password); + ldapService.bindAs(config, bindDn.getName(), password); if (ldapConnection == null) throw new GuacamoleInvalidCredentialsException("Invalid login.", CredentialsInfo.USERNAME_PASSWORD); @@ -214,13 +224,14 @@ public class AuthenticationProviderService { // Retrieve group membership of the user that just authenticated Set effectiveGroups = - userGroupService.getParentUserGroupIdentifiers(ldapConnection, - bindDn); + userGroupService.getParentUserGroupIdentifiers(config, + ldapConnection, bindDn); // Return AuthenticatedUser if bind succeeds LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(credentials, getAttributeTokens(ldapConnection, - bindDn), effectiveGroups, bindDn); + authenticatedUser.init(config, credentials, + getAttributeTokens(config, ldapConnection, bindDn), + effectiveGroups, bindDn); return authenticatedUser; @@ -240,6 +251,9 @@ public class AuthenticationProviderService { * guacamole.properties. If no attributes are specified or none are * found on the LDAP user object, an empty map is returned. * + * @param config + * The configuration of the LDAP server being queried. + * * @param ldapConnection * LDAP connection to use to read the attributes of the user. * @@ -255,11 +269,11 @@ public class AuthenticationProviderService { * @throws GuacamoleException * If an error occurs retrieving the user DN or the attributes. */ - private Map getAttributeTokens(LdapNetworkConnection ldapConnection, - Dn userDn) throws GuacamoleException { + private Map getAttributeTokens(LDAPConfiguration config, + LdapNetworkConnection ldapConnection, Dn userDn) throws GuacamoleException { // Get attributes from configuration information - List attrList = confService.getAttributes(); + List attrList = config.getAttributes(); // If there are no attributes there is no reason to search LDAP if (attrList.isEmpty()) @@ -316,9 +330,11 @@ public class AuthenticationProviderService { Credentials credentials = authenticatedUser.getCredentials(); if (authenticatedUser instanceof LDAPAuthenticatedUser) { - Dn bindDn = ((LDAPAuthenticatedUser) authenticatedUser).getBindDn(); - LdapNetworkConnection ldapConnection = - ldapService.bindAs(bindDn.getName(), credentials.getPassword()); + LDAPAuthenticatedUser ldapAuthenticatedUser = (LDAPAuthenticatedUser) authenticatedUser; + LDAPConfiguration config = ldapAuthenticatedUser.getLDAPConfiguration(); + Dn bindDn = ldapAuthenticatedUser.getBindDn(); + + LdapNetworkConnection ldapConnection = ldapService.bindAs(config, bindDn.getName(), credentials.getPassword()); if (ldapConnection == null) { logger.debug("LDAP bind succeeded for \"{}\" during " + "authentication but failed during data retrieval.", @@ -331,7 +347,7 @@ public class AuthenticationProviderService { // Build user context by querying LDAP LDAPUserContext userContext = userContextProvider.get(); - userContext.init(authenticatedUser, ldapConnection); + userContext.init(ldapAuthenticatedUser, ldapConnection); return userContext; } 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 d93ae23a8..f9d2a0513 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 @@ -19,7 +19,6 @@ package org.apache.guacamole.auth.ldap; -import com.google.inject.Inject; import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException; import org.apache.directory.api.ldap.model.exception.LdapException; import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException; @@ -34,8 +33,8 @@ import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleUnsupportedException; -import org.apache.guacamole.auth.ldap.conf.ConfigurationService; import org.apache.guacamole.auth.ldap.conf.EncryptionMethod; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,12 +48,6 @@ public class LDAPConnectionService { */ private static final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class); - /** - * Service for retrieving LDAP server configuration information. - */ - @Inject - private ConfigurationService confService; - /** * Creates a new instance of LdapNetworkConnection, configured as required * to use the given encryption method to communicate with the LDAP server @@ -130,6 +123,9 @@ public class LDAPConnectionService { * requested, and will not be connected until it is used in an LDAP * operation (such as a bind). * + * @param config + * The configuration of the LDAP server being queried. + * * @return * A new LdapNetworkConnection instance which has already been * configured to use the encryption method, hostname, and port @@ -139,12 +135,12 @@ public class LDAPConnectionService { * If an error occurs while parsing guacamole.properties, or if the * requested encryption method is actually not implemented (a bug). */ - private LdapNetworkConnection createLDAPConnection() + private LdapNetworkConnection createLDAPConnection(LDAPConfiguration config) throws GuacamoleException { return createLDAPConnection( - confService.getServerHostname(), - confService.getServerPort(), - confService.getEncryptionMethod()); + config.getServerHostname(), + config.getServerPort(), + config.getEncryptionMethod()); } /** @@ -156,6 +152,9 @@ public class LDAPConnectionService { * requested, and will not be connected until it is used in an LDAP * operation (such as a bind). * + * @param config + * The configuration of the LDAP server being queried. + * * @param url * The LDAP URL containing the details which should be used to connect * to the LDAP server. @@ -170,8 +169,8 @@ public class LDAPConnectionService { * method indicated by the URL is known but not actually implemented (a * bug). */ - private LdapNetworkConnection createLDAPConnection(String url) - throws GuacamoleException { + private LdapNetworkConnection createLDAPConnection(LDAPConfiguration config, + String url) throws GuacamoleException { // Parse provided LDAP URL LdapUrl ldapUrl; @@ -197,7 +196,7 @@ public class LDAPConnectionService { // Use STARTTLS for otherwise unencrypted ldap:// URLs if the main // LDAP connection requires STARTTLS - else if (confService.getEncryptionMethod() == EncryptionMethod.STARTTLS) { + else if (config.getEncryptionMethod() == EncryptionMethod.STARTTLS) { logger.debug("Using STARTTLS for LDAP URL \"{}\" as the main LDAP " + "connection described in guacamole.properties is " + "configured to use STARTTLS.", url); @@ -329,6 +328,9 @@ public class LDAPConnectionService { * hostname, port, and encryption method of the LDAP server are determined * from guacamole.properties. * + * @param config + * The configuration of the LDAP server being queried. + * * @param bindUser * The DN or UPN of the user to bind as, or null to bind anonymously. * @@ -344,15 +346,18 @@ public class LDAPConnectionService { * If an error occurs while parsing guacamole.properties, or if the * configured encryption method is actually not implemented (a bug). */ - public LdapNetworkConnection bindAs(String bindUser, String password) - throws GuacamoleException { - return bindAs(createLDAPConnection(), bindUser, password); + public LdapNetworkConnection bindAs(LDAPConfiguration config, + String bindUser, String password) throws GuacamoleException { + return bindAs(createLDAPConnection(config), bindUser, password); } /** * Binds to the LDAP server indicated by the given LDAP URL using the * credentials that were used to bind an existing LdapNetworkConnection. * + * @param config + * The configuration of the LDAP server being queried. + * * @param url * The LDAP URL containing the details which should be used to connect * to the LDAP server. @@ -370,16 +375,19 @@ public class LDAPConnectionService { * method indicated by the URL is known but not actually implemented (a * bug). */ - public LdapNetworkConnection bindAs(String url, + public LdapNetworkConnection bindAs(LDAPConfiguration config, String url, LdapNetworkConnection useCredentialsFrom) throws GuacamoleException { - return bindAs(createLDAPConnection(url), useCredentialsFrom); + return bindAs(createLDAPConnection(config, url), useCredentialsFrom); } /** * Generate a SearchRequest object using the given Base DN and filter * and retrieving other properties from the LDAP configuration service. - * + * + * @param config + * The configuration of the LDAP server being queried. + * * @param baseDn * The LDAP Base DN at which to search the search. * @@ -392,19 +400,19 @@ public class LDAPConnectionService { * @throws GuacamoleException * If an error occurs retrieving any of the configuration values. */ - public SearchRequest getSearchRequest(Dn baseDn, ExprNode filter) - throws GuacamoleException { + public SearchRequest getSearchRequest(LDAPConfiguration config, Dn baseDn, + ExprNode filter) throws GuacamoleException { SearchRequest searchRequest = new SearchRequestImpl(); searchRequest.setBase(baseDn); - searchRequest.setDerefAliases(confService.getDereferenceAliases()); + searchRequest.setDerefAliases(config.getDereferenceAliases()); searchRequest.setScope(SearchScope.SUBTREE); searchRequest.setFilter(filter); - searchRequest.setSizeLimit(confService.getMaxResults()); - searchRequest.setTimeLimit(confService.getOperationTimeout()); + searchRequest.setSizeLimit(config.getMaxResults()); + searchRequest.setTimeLimit(config.getOperationTimeout()); searchRequest.setTypesOnly(false); - if (confService.getFollowReferrals()) + if (config.getFollowReferrals()) searchRequest.followReferrals(); return searchRequest; diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java index e54b54f7c..02814ac9c 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java @@ -44,7 +44,7 @@ import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; -import org.apache.guacamole.auth.ldap.conf.ConfigurationService; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties; import org.apache.guacamole.net.auth.Identifiable; import org.slf4j.Logger; @@ -70,12 +70,6 @@ public class ObjectQueryService { @Inject private LDAPConnectionService ldapService; - /** - * Service for retrieving LDAP server configuration information. - */ - @Inject - private ConfigurationService confService; - /** * Returns the identifier of the object represented by the given LDAP * entry. Multiple attributes may be declared as containing the identifier @@ -184,6 +178,9 @@ public class ObjectQueryService { * list of all results. Only objects beneath the given base DN are * included in the search. * + * @param config + * The configuration of the LDAP server being queried. + * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. @@ -212,12 +209,13 @@ public class ObjectQueryService { * information required to execute the query cannot be read from * guacamole.properties. */ - public List search(LdapNetworkConnection ldapConnection, - Dn baseDN, ExprNode query, int searchHop, - Collection attributes) throws GuacamoleException { + public List search(LDAPConfiguration config, + LdapNetworkConnection ldapConnection, Dn baseDN, ExprNode query, + int searchHop, Collection attributes) + throws GuacamoleException { // Refuse to follow referrals if limit has been reached - int maxHops = confService.getMaxReferralHops(); + int maxHops = config.getMaxReferralHops(); if (searchHop >= maxHops) { logger.debug("Refusing to follow further referrals as the maximum " + "number of referral hops ({}) has been reached. LDAP " @@ -230,8 +228,7 @@ public class ObjectQueryService { logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query); // Search within subtree of given base DN - SearchRequest request = ldapService.getSearchRequest(baseDN, query); - + SearchRequest request = ldapService.getSearchRequest(config, baseDN, query); if (attributes != null) request.addAttributes(attributes.toArray(new String[0])); @@ -257,10 +254,10 @@ public class ObjectQueryService { // Connect to referred LDAP server to retrieve further results, ensuring the network // connection is always closed when it will no longer be used - try (LdapNetworkConnection referralConnection = ldapService.bindAs(url, ldapConnection)) { + try (LdapNetworkConnection referralConnection = ldapService.bindAs(config, url, ldapConnection)) { if (referralConnection != null) { logger.debug("Following referral to \"{}\"...", url); - entries.addAll(search(referralConnection, baseDN, query, searchHop + 1, attributes)); + entries.addAll(search(config, referralConnection, baseDN, query, searchHop + 1, attributes)); } else logger.debug("Could not bind with LDAP " @@ -304,6 +301,9 @@ public class ObjectQueryService { * given connection, returning a list of all results. Only objects beneath * the given base DN are included in the search. * + * @param config + * The configuration of the LDAP server being queried. + * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. @@ -339,12 +339,12 @@ public class ObjectQueryService { * information required to execute the query cannot be read from * guacamole.properties. */ - public List search(LdapNetworkConnection ldapConnection, Dn baseDN, - ExprNode filter, Collection filterAttributes, String filterValue, - Collection attributes) - throws GuacamoleException { + public List search(LDAPConfiguration config, + LdapNetworkConnection ldapConnection, Dn baseDN, ExprNode filter, + Collection filterAttributes, String filterValue, + Collection attributes) throws GuacamoleException { ExprNode query = generateQuery(filter, filterAttributes, filterValue); - return search(ldapConnection, baseDN, query, 0, attributes); + return search(config, ldapConnection, baseDN, query, 0, attributes); } /** diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index 2071dfa02..80d98cd22 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -20,12 +20,6 @@ package org.apache.guacamole.auth.ldap.conf; import com.google.inject.Inject; -import java.util.Collections; -import java.util.List; -import org.apache.directory.api.ldap.model.filter.ExprNode; -import org.apache.directory.api.ldap.model.filter.PresenceNode; -import org.apache.directory.api.ldap.model.message.AliasDerefMode; -import org.apache.directory.api.ldap.model.name.Dn; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; @@ -41,375 +35,24 @@ public class ConfigurationService { private Environment environment; /** - * Returns the hostname of the LDAP server as configured with - * guacamole.properties. By default, this will be "localhost". - * - * @return - * The hostname of the LDAP server, as configured with - * guacamole.properties. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public String getServerHostname() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_HOSTNAME, - "localhost" - ); - } - - /** - * Returns the port of the LDAP server configured with - * guacamole.properties. The default value depends on which encryption - * method is being used. For unencrypted LDAP and STARTTLS, this will be - * 389. For LDAPS (LDAP over SSL) this will be 636. - * - * @return - * The port of the LDAP server, as configured with - * guacamole.properties. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public int getServerPort() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_PORT, - getEncryptionMethod().DEFAULT_PORT - ); - } - - /** - * Returns all username attributes which should be used to query and bind - * users using the LDAP directory. By default, this will be "uid" - a - * common attribute used for this purpose. - * - * @return - * The username attributes which should be used to query and bind users - * using the LDAP directory. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public List getUsernameAttributes() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE, - Collections.singletonList("uid") - ); - } - - /** - * Returns the base DN under which all Guacamole users will be stored - * within the LDAP directory. - * - * @return - * The base DN under which all Guacamole users will be stored within - * the LDAP directory. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed, or if the user base DN - * property is not specified. - */ - public Dn getUserBaseDN() throws GuacamoleException { - return environment.getRequiredProperty( - LDAPGuacamoleProperties.LDAP_USER_BASE_DN - ); - } - - /** - * Returns the base DN under which all Guacamole configurations - * (connections) will be stored within the LDAP directory. If Guacamole - * configurations will not be stored within LDAP, null is returned. - * - * @return - * The base DN under which all Guacamole configurations will be stored - * within the LDAP directory, or null if no Guacamole configurations - * will be stored within the LDAP directory. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public Dn getConfigurationBaseDN() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN - ); - } - - /** - * Returns all attributes which should be used to determine the unique - * identifier of each user group. By default, this will be "cn". - * - * @return - * The attributes which should be used to determine the unique - * identifier of each group. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public List getGroupNameAttributes() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_GROUP_NAME_ATTRIBUTE, - Collections.singletonList("cn") - ); - } - - /** - * Returns the base DN under which all Guacamole role based access control - * (RBAC) groups will be stored within the LDAP directory. If RBAC will not - * be used, null is returned. - * - * @return - * The base DN under which all Guacamole RBAC groups will be stored - * within the LDAP directory, or null if RBAC will not be used. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public Dn getGroupBaseDN() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN - ); - } - - /** - * Returns the login that should be used when searching for the DNs of users - * attempting to authenticate. If no such search should be performed, null + * Returns the configuration information for the LDAP server related to the + * user having the given username. If no such LDAP server is defined, null * is returned. * - * @return - * The DN that should be used when searching for the DNs of users - * attempting to authenticate, or null if no such search should be - * performed. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public String getSearchBindDN() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN - ); - } - - /** - * Returns the password that should be used when binding to the LDAP server - * using the DN returned by getSearchBindDN(). If no password should be - * used, null is returned. + * @param username + * The username of the user whose corresponding LDAP server + * configuration should be retrieved. * * @return - * The password that should be used when binding to the LDAP server - * using the DN returned by getSearchBindDN(), or null if no password - * should be used. + * The configuration of the LDAP server related to the user having the + * given username, or null if no such LDAP server is defined. * * @throws GuacamoleException - * If guacamole.properties cannot be parsed. + * If the configuration information of the LDAP server related to the + * user having the given username cannot be retrieved due to an error. */ - public String getSearchBindPassword() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD - ); - } - - /** - * Returns the encryption method that should be used when connecting to the - * LDAP server. By default, no encryption is used. - * - * @return - * The encryption method that should be used when connecting to the - * LDAP server. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public EncryptionMethod getEncryptionMethod() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, - EncryptionMethod.NONE - ); - } - - /** - * Returns maximum number of results a LDAP query can return, - * as configured with guacamole.properties. - * By default, this will be 1000. - * - * @return - * The maximum number of results a LDAP query can return, - * as configured with guacamole.properties. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public int getMaxResults() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS, - 1000 - ); - } - - /** - * Returns whether or not LDAP aliases will be dereferenced, - * as configured with guacamole.properties. The default - * behavior if not explicitly defined is to never - * dereference them. - * - * @return - * The behavior for handling dereferencing of aliases - * as configured in guacamole.properties. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public AliasDerefMode getDereferenceAliases() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES, - AliasDerefMode.NEVER_DEREF_ALIASES - ); - } - - /** - * 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 the maximum number of referral hops to follow. By default - * a maximum of 5 hops is allowed. - * - * @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, - * a default of "(objectClass=user)" is returned. - * - * @return - * The search filter that should be used when querying the - * LDAP server for users that are valid in Guacamole, or - * "(objectClass=user)" if not specified. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public ExprNode getUserSearchFilter() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER, - new PresenceNode("objectClass") - ); - } - - /** - * Returns the search filter that should be used when querying the - * LDAP server for Guacamole groups. If no filter is specified, - * a default of "(objectClass=*)" is used. - * - * @return - * The search filter that should be used when querying the - * LDAP server for groups that are valid in Guacamole, or - * "(objectClass=*)" if not specified. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public ExprNode getGroupSearchFilter() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER, - new PresenceNode("objectClass") - ); - } - - /** - * 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 - ); - } - - /** - * Returns names for custom LDAP user attributes. By default no - * attributes will be returned. - * - * @return - * Custom LDAP user attributes as configured in guacamole.properties. - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public List getAttributes() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_USER_ATTRIBUTES, - Collections.emptyList() - ); - } - - /** - * Returns the name of the LDAP attribute used to enumerate - * members in a group, or "member" by default. - * - * @return - * The name of the LDAP attribute to use to enumerate - * members in a group. - * - * @throws GuacamoleException - * If guacamole.properties connect be parsed. - */ - public String getMemberAttribute() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE, - "member" - ); - } - - /** - * Returns whether the LDAP attribute used to enumerate members in a group - * specifies UID or DN. - * - * @return - * The type of data contained in the LDAP attribute used to enumerate - * members in a group, as configured in guacamole.properties - * - * @throws GuacamoleException - * If guacamole.properties cannot be parsed. - */ - public MemberAttributeType getMemberAttributeType() - throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE, - MemberAttributeType.DN - ); + public LDAPConfiguration getLDAPConfiguration(String username) throws GuacamoleException { + return new EnvironmentLDAPConfiguration(environment); } } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java new file mode 100644 index 000000000..4dca7cde2 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java @@ -0,0 +1,209 @@ +/* + * 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.conf; + +import java.util.Collections; +import java.util.List; +import org.apache.directory.api.ldap.model.filter.ExprNode; +import org.apache.directory.api.ldap.model.filter.PresenceNode; +import org.apache.directory.api.ldap.model.message.AliasDerefMode; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; + +/** + * LDAPConfiguration implementation that reads its configuration details from + * guacamole.properties. + */ +public class EnvironmentLDAPConfiguration implements LDAPConfiguration { + + /** + * The Guacamole server environment. + */ + private final Environment environment; + + /** + * Creates a new EnvironmentLDAPConfiguration that reads its configuration + * details from guacamole.properties, as exposed by the given Environment. + * + * @param environment + * The Guacamole server environment. + */ + public EnvironmentLDAPConfiguration(Environment environment) { + this.environment = environment; + } + + @Override + public String getServerHostname() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_HOSTNAME, + "localhost" + ); + } + + @Override + public int getServerPort() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_PORT, + getEncryptionMethod().DEFAULT_PORT + ); + } + + @Override + public List getUsernameAttributes() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE, + Collections.singletonList("uid") + ); + } + + @Override + public Dn getUserBaseDN() throws GuacamoleException { + return environment.getRequiredProperty( + LDAPGuacamoleProperties.LDAP_USER_BASE_DN + ); + } + + @Override + public Dn getConfigurationBaseDN() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN + ); + } + + @Override + public List getGroupNameAttributes() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_GROUP_NAME_ATTRIBUTE, + Collections.singletonList("cn") + ); + } + + @Override + public Dn getGroupBaseDN() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN + ); + } + + @Override + public String getSearchBindDN() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN + ); + } + + @Override + public String getSearchBindPassword() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD + ); + } + + @Override + public EncryptionMethod getEncryptionMethod() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, + EncryptionMethod.NONE + ); + } + + @Override + public int getMaxResults() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS, + 1000 + ); + } + + @Override + public AliasDerefMode getDereferenceAliases() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES, + AliasDerefMode.NEVER_DEREF_ALIASES + ); + } + + @Override + public boolean getFollowReferrals() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS, + false + ); + } + + @Override + public int getMaxReferralHops() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS, + 5 + ); + } + + @Override + public ExprNode getUserSearchFilter() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER, + new PresenceNode("objectClass") + ); + } + + @Override + public ExprNode getGroupSearchFilter() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER, + new PresenceNode("objectClass") + ); + } + + @Override + public int getOperationTimeout() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_OPERATION_TIMEOUT, + 30 + ); + } + + @Override + public List getAttributes() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_USER_ATTRIBUTES, + Collections.emptyList() + ); + } + + @Override + public String getMemberAttribute() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE, + "member" + ); + } + + @Override + public MemberAttributeType getMemberAttributeType() + throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE, + MemberAttributeType.DN + ); + } + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java new file mode 100644 index 000000000..5c3315741 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java @@ -0,0 +1,303 @@ +/* + * 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.conf; + +import java.util.List; +import org.apache.directory.api.ldap.model.filter.ExprNode; +import org.apache.directory.api.ldap.model.message.AliasDerefMode; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.guacamole.GuacamoleException; + +/** + * Configuration information defining how a particular LDAP server should be + * queried. + */ +public interface LDAPConfiguration { + + /** + * Returns the hostname or IP address of the LDAP server. By default, this + * will be "localhost". + * + * @return + * The hostname or IP address of the LDAP server. + * + * @throws GuacamoleException + * If the hostname or IP address of the LDAP server cannot be + * retrieved. + */ + String getServerHostname() throws GuacamoleException; + + /** + * Returns the port of the LDAP server. The default value depends on which + * encryption method is being used. For unencrypted LDAP and STARTTLS, this + * will be 389. For LDAPS (LDAP over SSL) this will be 636. + * + * @return + * The port of the LDAP server. + * + * @throws GuacamoleException + * If the port of the LDAP server cannot be retrieved. + */ + int getServerPort() throws GuacamoleException; + + /** + * Returns all username attributes which should be used to query and bind + * users using the LDAP directory. By default, this will be "uid" - a + * common attribute used for this purpose. + * + * @return + * The username attributes which should be used to query and bind users + * using the LDAP directory. + * + * @throws GuacamoleException + * If the username attributes cannot be retrieved. + */ + List getUsernameAttributes() throws GuacamoleException; + + /** + * Returns the base DN under which all Guacamole users will be stored + * within the LDAP directory. + * + * @return + * The base DN under which all Guacamole users will be stored within + * the LDAP directory. + * + * @throws GuacamoleException + * If the user base DN cannot be retrieved. + */ + Dn getUserBaseDN() throws GuacamoleException; + + /** + * Returns the base DN under which all Guacamole configurations + * (connections) will be stored within the LDAP directory. If Guacamole + * configurations will not be stored within LDAP, null is returned. + * + * @return + * The base DN under which all Guacamole configurations will be stored + * within the LDAP directory, or null if no Guacamole configurations + * will be stored within the LDAP directory. + * + * @throws GuacamoleException + * If the configuration base DN cannot be retrieved. + */ + Dn getConfigurationBaseDN() throws GuacamoleException; + + /** + * Returns all attributes which should be used to determine the unique + * identifier of each user group. By default, this will be "cn". + * + * @return + * The attributes which should be used to determine the unique + * identifier of each group. + * + * @throws GuacamoleException + * If the group name attributes cannot be retrieved. + */ + List getGroupNameAttributes() throws GuacamoleException; + + /** + * Returns the base DN under which all Guacamole role based access control + * (RBAC) groups will be stored within the LDAP directory. If RBAC will not + * be used, null is returned. + * + * @return + * The base DN under which all Guacamole RBAC groups will be stored + * within the LDAP directory, or null if RBAC will not be used. + * + * @throws GuacamoleException + * If the group base DN cannot be retrieved. + */ + Dn getGroupBaseDN() throws GuacamoleException; + + /** + * Returns the login that should be used when searching for the DNs of users + * attempting to authenticate. If no such search should be performed, null + * is returned. + * + * @return + * The DN that should be used when searching for the DNs of users + * attempting to authenticate, or null if no such search should be + * performed. + * + * @throws GuacamoleException + * If the search bind DN cannot be retrieved. + */ + String getSearchBindDN() throws GuacamoleException; + + /** + * Returns the password that should be used when binding to the LDAP server + * using the DN returned by getSearchBindDN(). If no password should be + * used, null is returned. + * + * @return + * The password that should be used when binding to the LDAP server + * using the DN returned by getSearchBindDN(), or null if no password + * should be used. + * + * @throws GuacamoleException + * If the search bind password cannot be retrieved. + */ + String getSearchBindPassword() throws GuacamoleException; + + /** + * Returns the encryption method that should be used when connecting to the + * LDAP server. By default, no encryption is used. + * + * @return + * The encryption method that should be used when connecting to the + * LDAP server. + * + * @throws GuacamoleException + * If the encryption method cannot be retrieved. + */ + EncryptionMethod getEncryptionMethod() throws GuacamoleException; + + /** + * Returns maximum number of results a LDAP query can return. By default, + * this will be 1000. + * + * @return + * The maximum number of results a LDAP query can return. + * + * @throws GuacamoleException + * If the maximum number of results cannot be retrieved. + */ + int getMaxResults() throws GuacamoleException; + + /** + * Returns whether or not LDAP aliases will be dereferenced. By default, + * aliases are never dereferenced. + * + * @return + * The LDAP alias dereferencing mode. + * + * @throws GuacamoleException + * If the LDAP alias dereferencing mode cannot be retrieved. + */ + AliasDerefMode getDereferenceAliases() throws GuacamoleException; + + /** + * Returns whether referrals should be automatically followed. By default, + * referrals are not followed. + * + * @return + * Whether referrals should be followed. + * + * @throws GuacamoleException + * If the configuration information determining whether LDAP referrals + * should be followed cannot be retrieved. + */ + boolean getFollowReferrals() throws GuacamoleException; + + /** + * Returns the maximum number of referral hops to follow. By default + * a maximum of 5 hops is allowed. + * + * @return + * The maximum number of referral hops to follow. + * + * @throws GuacamoleException + * If the maximum number of referral hops cannot be retrieved. + */ + int getMaxReferralHops() throws GuacamoleException; + + /** + * Returns the search filter that should be used when querying the + * LDAP server for Guacamole users. If no filter is specified, + * a default of "(objectClass=user)" is returned. + * + * @return + * The search filter that should be used when querying the + * LDAP server for users that are valid in Guacamole, or + * "(objectClass=user)" if not specified. + * + * @throws GuacamoleException + * If the user search filter cannot be retrieved. + */ + ExprNode getUserSearchFilter() throws GuacamoleException; + + /** + * Returns the search filter that should be used when querying the + * LDAP server for Guacamole groups. If no filter is specified, + * a default of "(objectClass=*)" is used. + * + * @return + * The search filter that should be used when querying the + * LDAP server for groups that are valid in Guacamole, or + * "(objectClass=*)" if not specified. + * + * @throws GuacamoleException + * If the group search filter cannot be retrieved. + */ + ExprNode getGroupSearchFilter() throws GuacamoleException; + + /** + * Returns the maximum number of seconds to wait for LDAP operations. + * + * @return + * The maximum number of seconds to wait for LDAP operations. + * + * @throws GuacamoleException + * If the LDAP operation timeout cannot be retrieved. + */ + int getOperationTimeout() throws GuacamoleException; + + /** + * Returns names for custom LDAP user attributes that should be made + * available as parameter tokens. By default, no additional LDAP attributes + * will be exposed as parameter tokens. + * + * @return + * A list of all LDAP user attributes that should be made available as + * parameter tokens. + * + * @throws GuacamoleException + * If the names of custom LDAP user attributes cannot be retrieved. + */ + List getAttributes() throws GuacamoleException; + + /** + * Returns the name of the LDAP attribute used to enumerate members in a + * group. By default, this will be "member". + * + * @return + * The name of the LDAP attribute to use to enumerate + * members in a group. + * + * @throws GuacamoleException + * If the group member attribute cannot be retrieved. + */ + String getMemberAttribute() throws GuacamoleException; + + /** + * Returns whether the LDAP attribute used to enumerate members in a group + * specifies a UID or DN. + * + * @return + * The type of data contained in the LDAP attribute used to enumerate + * members in a group. + * + * @throws GuacamoleException + * If the type of attribute used to enumerate group members cannot be + * retrieved. + */ + MemberAttributeType getMemberAttributeType() throws GuacamoleException; + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java index 84bef7f92..bff49858c 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java @@ -38,13 +38,12 @@ import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.ldap.client.api.LdapConnectionConfig; import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider; -import org.apache.guacamole.auth.ldap.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.ldap.ObjectQueryService; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser; -import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.TokenInjectingConnection; import org.apache.guacamole.net.auth.simple.SimpleConnection; @@ -63,12 +62,6 @@ public class ConnectionService { */ private static final Logger logger = LoggerFactory.getLogger(ConnectionService.class); - /** - * Service for retrieving LDAP server configuration information. - */ - @Inject - private ConfigurationService confService; - /** * Service for executing LDAP queries. */ @@ -143,11 +136,13 @@ public class ConnectionService { * @throws GuacamoleException * If an error occurs preventing retrieval of connections. */ - public Map getConnections(AuthenticatedUser user, + public Map getConnections(LDAPAuthenticatedUser user, LdapNetworkConnection ldapConnection) throws GuacamoleException { + LDAPConfiguration ldapConfig = user.getLDAPConfiguration(); + // Do not return any connections if base DN is not specified - Dn configurationBaseDN = confService.getConfigurationBaseDN(); + Dn configurationBaseDN = ldapConfig.getConfigurationBaseDN(); if (configurationBaseDN == null) return Collections.emptyMap(); @@ -164,13 +159,13 @@ public class ConnectionService { // Get the search filter for finding connections accessible by the // current user - ExprNode connectionSearchFilter = getConnectionSearchFilter(userDN, ldapConnection); + ExprNode connectionSearchFilter = getConnectionSearchFilter(user, userDN, ldapConnection); // Find all Guacamole connections for the given user by // looking for direct membership in the guacConfigGroup // and possibly any groups the user is a member of that are // referred to in the seeAlso attribute of the guacConfigGroup. - List results = queryService.search(ldapConnection, + List results = queryService.search(ldapConfig, ldapConnection, configurationBaseDN, connectionSearchFilter, 0, GUAC_CONFIG_LDAP_ATTRIBUTES); // Return a map of all readable connections @@ -261,8 +256,7 @@ public class ConnectionService { // Inject LDAP-specific tokens only if LDAP handled user // authentication if (user instanceof LDAPAuthenticatedUser) - connection = new TokenInjectingConnection(connection, - ((LDAPAuthenticatedUser) user).getTokens()); + connection = new TokenInjectingConnection(connection, user.getTokens()); return connection; @@ -279,6 +273,10 @@ public class ConnectionService { * Returns an LDAP search filter which queries all connections accessible * by the user having the given DN. * + * @param user + * The AuthenticatedUser object associated with the user who is + * currently authenticated with Guacamole. + * * @param userDN * DN of the user to search for associated guacConfigGroup connections. * @@ -296,10 +294,12 @@ public class ConnectionService { * @throws GuacamoleException * If an error occurs retrieving the group base DN. */ - private ExprNode getConnectionSearchFilter(Dn userDN, - LdapNetworkConnection ldapConnection) + private ExprNode getConnectionSearchFilter(LDAPAuthenticatedUser user, + Dn userDN, LdapNetworkConnection ldapConnection) throws LdapException, GuacamoleException { + LDAPConfiguration config = user.getLDAPConfiguration(); + AndNode searchFilter = new AndNode(); // Add the prefix to the search filter, prefix filter searches for guacConfigGroups with the userDN as the member attribute value @@ -307,12 +307,12 @@ public class ConnectionService { // Apply group filters OrNode groupFilter = new OrNode(); - groupFilter.addNode(new EqualityNode(confService.getMemberAttribute(), + groupFilter.addNode(new EqualityNode(config.getMemberAttribute(), userDN.toString())); // Additionally filter by group membership if the current user is a // member of any user groups - List userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN); + List userGroups = userGroupService.getParentUserGroupEntries(config, ldapConnection, userDN); if (!userGroups.isEmpty()) { userGroups.forEach(entry -> groupFilter.addNode(new EqualityNode(LDAP_ATTRIBUTE_NAME_GROUPS,entry.getDn().toString())) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java index 67ffbc3fb..a3dc9c1e8 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java @@ -34,10 +34,11 @@ import org.apache.directory.api.ldap.model.filter.ExprNode; import org.apache.directory.api.ldap.model.filter.NotNode; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.ldap.client.api.LdapNetworkConnection; -import org.apache.guacamole.auth.ldap.conf.ConfigurationService; import org.apache.guacamole.auth.ldap.conf.MemberAttributeType; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.ldap.ObjectQueryService; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; +import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser; import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.simple.SimpleUserGroup; import org.slf4j.Logger; @@ -54,12 +55,6 @@ public class UserGroupService { */ private static final Logger logger = LoggerFactory.getLogger(UserGroupService.class); - /** - * Service for retrieving LDAP server configuration information. - */ - @Inject - private ConfigurationService confService; - /** * Service for executing LDAP queries. */ @@ -73,22 +68,25 @@ public class UserGroupService { * defined (may always return zero results), it should only be explicitly * excluded if it is expected to have been defined. * + * @param config + * The configuration of the LDAP server being queried. + * * @return * The base search filter which should be used to retrieve user groups. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ - private ExprNode getGroupSearchFilter() throws GuacamoleException { + private ExprNode getGroupSearchFilter(LDAPConfiguration config) throws GuacamoleException { // Use filter defined by "ldap-group-search-filter" as basis for all // retrieval of user groups - ExprNode groupFilter = confService.getGroupSearchFilter(); + ExprNode groupFilter = config.getGroupSearchFilter(); // Explicitly exclude guacConfigGroup object class only if it should // be assumed to be defined (query may fail due to no such object // class existing otherwise) - if (confService.getConfigurationBaseDN() != null) { + if (config.getConfigurationBaseDN() != null) { groupFilter = new AndNode( groupFilter, new NotNode(new EqualityNode("objectClass", "guacConfigGroup")) @@ -103,6 +101,10 @@ public class UserGroupService { * Returns all Guacamole user groups accessible to the user currently bound * under the given LDAP connection. * + * @param user + * The AuthenticatedUser object associated with the user who is + * currently authenticated with Guacamole. + * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. @@ -115,25 +117,28 @@ public class UserGroupService { * @throws GuacamoleException * If an error occurs preventing retrieval of user groups. */ - public Map getUserGroups(LdapNetworkConnection ldapConnection) - throws GuacamoleException { + public Map getUserGroups(LDAPAuthenticatedUser user, + LdapNetworkConnection ldapConnection) throws GuacamoleException { + LDAPConfiguration config = user.getLDAPConfiguration(); + // Do not return any user groups if base DN is not specified - Dn groupBaseDN = confService.getGroupBaseDN(); + Dn groupBaseDN = config.getGroupBaseDN(); if (groupBaseDN == null) return Collections.emptyMap(); // Gather all attributes relevant for a group - String memberAttribute = confService.getMemberAttribute(); - Collection groupAttributes = new HashSet<>(confService.getGroupNameAttributes()); + String memberAttribute = config.getMemberAttribute(); + Collection groupAttributes = new HashSet<>(config.getGroupNameAttributes()); groupAttributes.add(memberAttribute); // Retrieve all visible user groups which are not guacConfigGroups - Collection attributes = confService.getGroupNameAttributes(); + Collection attributes = config.getGroupNameAttributes(); List results = queryService.search( + config, ldapConnection, groupBaseDN, - getGroupSearchFilter(), + getGroupSearchFilter(config), attributes, null, groupAttributes @@ -167,6 +172,9 @@ public class UserGroupService { * user is a member of. Only user groups which are readable by the current * user will be retrieved. * + * @param config + * The configuration of the LDAP server being queried. + * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. @@ -181,24 +189,26 @@ public class UserGroupService { * @throws GuacamoleException * If an error occurs preventing retrieval of user groups. */ - public List getParentUserGroupEntries(LdapNetworkConnection ldapConnection, - Dn userDN) throws GuacamoleException { + public List getParentUserGroupEntries(LDAPConfiguration config, + LdapNetworkConnection ldapConnection, Dn userDN) + throws GuacamoleException { // Do not return any user groups if base DN is not specified - Dn groupBaseDN = confService.getGroupBaseDN(); + Dn groupBaseDN = config.getGroupBaseDN(); if (groupBaseDN == null) return Collections.emptyList(); // memberAttribute specified in properties could contain DN or username - MemberAttributeType memberAttributeType = confService.getMemberAttributeType(); + MemberAttributeType memberAttributeType = config.getMemberAttributeType(); String userIDorDN = userDN.toString(); - Collection userAttributes = confService.getUsernameAttributes(); + Collection userAttributes = config.getUsernameAttributes(); if (memberAttributeType == MemberAttributeType.UID) { // Retrieve user objects with userDN List userEntries = queryService.search( + config, ldapConnection, userDN, - confService.getUserSearchFilter(), + config.getUserSearchFilter(), 0, userAttributes); // ... there can surely only be one @@ -222,16 +232,17 @@ public class UserGroupService { } // Gather all attributes relevant for a group - String memberAttribute = confService.getMemberAttribute(); - Collection groupAttributes = new HashSet<>(confService.getGroupNameAttributes()); + String memberAttribute = config.getMemberAttribute(); + Collection groupAttributes = new HashSet<>(config.getGroupNameAttributes()); groupAttributes.add(memberAttribute); // Get all groups the user is a member of starting at the groupBaseDN, // excluding guacConfigGroups return queryService.search( + config, ldapConnection, groupBaseDN, - getGroupSearchFilter(), + getGroupSearchFilter(config), Collections.singleton(memberAttribute), userIDorDN, groupAttributes @@ -244,6 +255,9 @@ public class UserGroupService { * member of. Only identifiers of user groups which are readable by the * current user will be retrieved. * + * @param config + * The configuration of the LDAP server being queried. + * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. @@ -258,11 +272,12 @@ public class UserGroupService { * @throws GuacamoleException * If an error occurs preventing retrieval of user groups. */ - public Set getParentUserGroupIdentifiers(LdapNetworkConnection ldapConnection, - Dn userDN) throws GuacamoleException { + public Set getParentUserGroupIdentifiers(LDAPConfiguration config, + LdapNetworkConnection ldapConnection, Dn userDN) + throws GuacamoleException { - Collection attributes = confService.getGroupNameAttributes(); - List userGroups = getParentUserGroupEntries(ldapConnection, userDN); + Collection attributes = config.getGroupNameAttributes(); + List userGroups = getParentUserGroupEntries(config, ldapConnection, userDN); Set identifiers = new HashSet<>(userGroups.size()); userGroups.forEach(entry -> { diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java index 44296432f..97eab6cae 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.Map; import java.util.Set; import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.apache.guacamole.net.auth.AbstractAuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; @@ -63,10 +64,20 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { */ private Dn bindDn; + /** + * The configuration of the LDAP server that should be used for all queries + * related to this AuthenticatedUser. + */ + private LDAPConfiguration config; + /** * Initializes this AuthenticatedUser with the given credentials, * connection parameter tokens. and set of effective user groups. * + * @param config + * The configuration of the LDAP server that should be used for all + * queries related to this AuthenticatedUser. + * * @param credentials * The credentials provided when this user was authenticated. * @@ -81,8 +92,9 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { * @param bindDn * The LDAP DN used to bind this user. */ - public void init(Credentials credentials, Map tokens, - Set effectiveGroups, Dn bindDn) { + public void init(LDAPConfiguration config, Credentials credentials, + Map tokens, Set effectiveGroups, Dn bindDn) { + this.config = config; this.credentials = credentials; this.tokens = Collections.unmodifiableMap(tokens); this.effectiveGroups = effectiveGroups; @@ -114,6 +126,18 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { return bindDn; } + /** + * Returns the configuration of the LDAP server that should be used for all + * queries related to this AuthenticatedUser. + * + * @return + * The configuration of the LDAP server related to this + * AuthenticatedUser. + */ + public LDAPConfiguration getLDAPConfiguration() { + return config; + } + @Override public AuthenticationProvider getAuthenticationProvider() { return authProvider; diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java index b5c789e1e..cb2afd516 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java @@ -27,7 +27,6 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider; import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.net.auth.AbstractUserContext; -import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; @@ -106,8 +105,8 @@ public class LDAPUserContext extends AbstractUserContext { * * @param user * The AuthenticatedUser representing the user that authenticated. This - * user may have been authenticated by a different authentication - * provider (not LDAP). + * user will always have been authenticated via LDAP, as LDAP data is + * not provided to non-LDAP users. * * @param ldapConnection * The connection to the LDAP server to use when querying accessible @@ -117,17 +116,17 @@ public class LDAPUserContext extends AbstractUserContext { * If associated data stored within the LDAP directory cannot be * queried due to an error. */ - public void init(AuthenticatedUser user, LdapNetworkConnection ldapConnection) + public void init(LDAPAuthenticatedUser user, LdapNetworkConnection ldapConnection) throws GuacamoleException { // Query all accessible users userDirectory = new SimpleDirectory<>( - userService.getUsers(ldapConnection) + userService.getUsers(user, ldapConnection) ); // Query all accessible user groups userGroupDirectory = new SimpleDirectory<>( - userGroupService.getUserGroups(ldapConnection) + userGroupService.getUserGroups(user, ldapConnection) ); // Query all accessible connections 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 1ffc270be..69df58a79 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 @@ -32,11 +32,11 @@ import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueEx import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.api.ldap.model.name.Rdn; import org.apache.directory.ldap.client.api.LdapNetworkConnection; -import org.apache.guacamole.auth.ldap.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties; import org.apache.guacamole.auth.ldap.ObjectQueryService; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.simple.SimpleUser; import org.slf4j.Logger; @@ -53,12 +53,6 @@ public class UserService { */ private static final Logger logger = LoggerFactory.getLogger(UserService.class); - /** - * Service for retrieving LDAP server configuration information. - */ - @Inject - private ConfigurationService confService; - /** * Service for executing LDAP queries. */ @@ -69,6 +63,10 @@ public class UserService { * Returns all Guacamole users accessible to the user currently bound under * the given LDAP connection. * + * @param user + * The AuthenticatedUser object associated with the user who is + * currently authenticated with Guacamole. + * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. @@ -81,16 +79,18 @@ public class UserService { * @throws GuacamoleException * If an error occurs preventing retrieval of users. */ - public Map getUsers(LdapNetworkConnection ldapConnection) - throws GuacamoleException { + public Map getUsers(LDAPAuthenticatedUser user, + LdapNetworkConnection ldapConnection) throws GuacamoleException { + LDAPConfiguration config = user.getLDAPConfiguration(); + // Retrieve all visible user objects - Collection usernameAttrs = confService.getUsernameAttributes(); + Collection usernameAttrs = config.getUsernameAttributes(); Collection attributes = new HashSet<>(usernameAttrs); - attributes.addAll(confService.getAttributes()); - List results = queryService.search(ldapConnection, - confService.getUserBaseDN(), - confService.getUserSearchFilter(), + attributes.addAll(config.getAttributes()); + List results = queryService.search(config, ldapConnection, + config.getUserBaseDN(), + config.getUserSearchFilter(), usernameAttrs, null, attributes); @@ -124,6 +124,9 @@ public class UserService { * is not enforced across the username attribute, it is possible that this * will return multiple DNs. * + * @param config + * The configuration of the LDAP server being queried. + * * @param ldapConnection * The connection to the LDAP server to use when querying user DNs. * @@ -139,14 +142,14 @@ public class UserService { * If an error occurs while querying the user DNs, or if the username * attribute property cannot be parsed within guacamole.properties. */ - public List getUserDNs(LdapNetworkConnection ldapConnection, + public List getUserDNs(LDAPConfiguration config, LdapNetworkConnection ldapConnection, String username) throws GuacamoleException { // Retrieve user objects having a matching username - List results = queryService.search(ldapConnection, - confService.getUserBaseDN(), - confService.getUserSearchFilter(), - confService.getUsernameAttributes(), + List results = queryService.search(config, ldapConnection, + config.getUserBaseDN(), + config.getUserSearchFilter(), + config.getUsernameAttributes(), username, Collections.singletonList("dn")); @@ -164,6 +167,9 @@ public class UserService { * or queried from the LDAP server, depending on how LDAP authentication * has been configured. * + * @param config + * The configuration of the LDAP server being queried. + * * @param username * The username of the user whose corresponding DN should be returned. * @@ -174,11 +180,11 @@ public class UserService { * If required properties are missing, and thus the user DN cannot be * determined. */ - public Dn deriveUserDN(String username) + public Dn deriveUserDN(LDAPConfiguration config, String username) throws GuacamoleException { // Pull username attributes from properties - List usernameAttributes = confService.getUsernameAttributes(); + List usernameAttributes = config.getUsernameAttributes(); // We need exactly one base DN to derive the user DN if (usernameAttributes.size() != 1) { @@ -193,7 +199,7 @@ public class UserService { // Derive user DN from base DN try { return new Dn(new Rdn(usernameAttributes.get(0), username), - confService.getUserBaseDN()); + config.getUserBaseDN()); } catch (LdapInvalidAttributeValueException | LdapInvalidDnException e) { throw new GuacamoleServerException("Error trying to derive user DN.", e); From 278bfa17ae03650d2842d3d5ffde66675d22fcf9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 20 Oct 2021 21:29:41 -0700 Subject: [PATCH 02/14] GUACAMOLE-957: Allow each user to be associated with multiple LDAP servers. --- .../ldap/AuthenticationProviderService.java | 132 +++++++---- .../auth/ldap/ConnectedLDAPConfiguration.java | 210 ++++++++++++++++++ .../auth/ldap/conf/ConfigurationService.java | 21 +- .../ldap/connection/ConnectionService.java | 49 ++-- .../auth/ldap/group/UserGroupService.java | 37 +-- .../auth/ldap/user/LDAPAuthenticatedUser.java | 24 +- .../auth/ldap/user/LDAPUserContext.java | 17 +- .../guacamole/auth/ldap/user/UserService.java | 29 ++- 8 files changed, 361 insertions(+), 158 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java index 856a536e0..42afdf510 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java @@ -168,6 +168,74 @@ public class AuthenticationProviderService { } + /** + * Returns a new ConnectedLDAPConfiguration that is connected to an LDAP + * server associated with the user having the given username and bound + * using the provided password. All LDAP servers associated with the given + * user are tried until the connection and authentication attempt succeeds. + * If no LDAP servers are available, or no LDAP servers are associated with + * the given user, null is returned. + * + * @param username + * The username or DN of the user to bind as. + * + * @param password + * The password of the user to bind as. + * + * @return + * A new ConnectedLDAPConfiguration which is bound to an LDAP server + * using the provided credentials, or null if no LDAP servers are + * available for the given user or connecting/authenticating has + * failed. + * + * @throws GuacamoleException + * If configuration information for the user's LDAP server(s) cannot + * be retrieved. + */ + private ConnectedLDAPConfiguration getLDAPConfiguration(String username, + String password) throws GuacamoleException { + + // Get relevant LDAP configurations for user + Collection configs = confService.getLDAPConfigurations(username); + if (configs.isEmpty()) { + logger.info("User \"{}\" does not map to any defined LDAP configurations.", username); + return null; + } + + // Try each possible LDAP configuration until the TCP connection and + // authentication are successful + for (LDAPConfiguration config : configs) { + + // Derive DN of user within this LDAP server + Dn bindDn = getUserBindDN(config, username); + if (bindDn == null || bindDn.isEmpty()) { + logger.info("Unable to determine DN of user \"{}\" using LDAP " + + "server \"{}\". Proceeding with next server...", + username, config.getServerHostname()); + continue; + } + + // Attempt bind (authentication) + LdapNetworkConnection ldapConnection = ldapService.bindAs(config, bindDn.getName(), password); + if (ldapConnection == null) { + logger.info("Unable to bind as user \"{}\" against LDAP " + + "server \"{}\". Proceeding with next server...", + username, config.getServerHostname()); + continue; + } + + // Connection and bind were successful + logger.info("User \"{}\" was successfully authenticated by LDAP server \"{}\".", username, config.getServerHostname()); + return new ConnectedLDAPConfiguration(config, bindDn, ldapConnection); + + } + + // No LDAP connection/authentication attempt succeeded + logger.info("User \"{}\" did not successfully authenticate against any LDAP server.", username); + return null; + + } + /** * Returns an AuthenticatedUser representing the user authenticated by the * given credentials. Also adds custom LDAP attributes to the @@ -200,46 +268,29 @@ public class AuthenticationProviderService { + " authentication provider.", CredentialsInfo.USERNAME_PASSWORD); } - // Get relevant LDAP configuration for user - LDAPConfiguration config = confService.getLDAPConfiguration(username); - if (config == null) { - throw new GuacamoleInvalidCredentialsException("User \"" + username + "\" " - + "does not map to any defined LDAP configuration.", CredentialsInfo.USERNAME_PASSWORD); - } - - Dn bindDn = getUserBindDN(config, username); - if (bindDn == null || bindDn.isEmpty()) { - throw new GuacamoleInvalidCredentialsException("Unable to determine" - + " DN of user " + username, CredentialsInfo.USERNAME_PASSWORD); - } - - // Attempt bind - LdapNetworkConnection ldapConnection = - ldapService.bindAs(config, bindDn.getName(), password); - if (ldapConnection == null) + ConnectedLDAPConfiguration config = getLDAPConfiguration(username, password); + if (config == null) throw new GuacamoleInvalidCredentialsException("Invalid login.", CredentialsInfo.USERNAME_PASSWORD); try { - + // Retrieve group membership of the user that just authenticated Set effectiveGroups = - userGroupService.getParentUserGroupIdentifiers(config, - ldapConnection, bindDn); + userGroupService.getParentUserGroupIdentifiers(config, config.getBindDN()); // Return AuthenticatedUser if bind succeeds LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); authenticatedUser.init(config, credentials, - getAttributeTokens(config, ldapConnection, bindDn), - effectiveGroups, bindDn); + getAttributeTokens(config), effectiveGroups); return authenticatedUser; } - // Always disconnect - finally { - ldapConnection.close(); + catch (GuacamoleException | RuntimeException | Error e) { + config.close(); + throw e; } } @@ -254,12 +305,6 @@ public class AuthenticationProviderService { * @param config * The configuration of the LDAP server being queried. * - * @param ldapConnection - * LDAP connection to use to read the attributes of the user. - * - * @param username - * The username of the user whose attributes are to be queried. - * * @return * A map of parameter tokens generated from attributes on the user * currently bound under the given LDAP connection, as a map of token @@ -269,8 +314,8 @@ public class AuthenticationProviderService { * @throws GuacamoleException * If an error occurs retrieving the user DN or the attributes. */ - private Map getAttributeTokens(LDAPConfiguration config, - LdapNetworkConnection ldapConnection, Dn userDn) throws GuacamoleException { + private Map getAttributeTokens(ConnectedLDAPConfiguration config) + throws GuacamoleException { // Get attributes from configuration information List attrList = config.getAttributes(); @@ -286,7 +331,7 @@ public class AuthenticationProviderService { try { // Get LDAP attributes by querying LDAP - Entry userEntry = ldapConnection.lookup(userDn, attrArray); + Entry userEntry = config.getLDAPConnection().lookup(config.getBindDN(), attrArray); if (userEntry == null) return Collections.emptyMap(); @@ -326,37 +371,26 @@ public class AuthenticationProviderService { public LDAPUserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { - // Bind using credentials associated with AuthenticatedUser - Credentials credentials = authenticatedUser.getCredentials(); if (authenticatedUser instanceof LDAPAuthenticatedUser) { LDAPAuthenticatedUser ldapAuthenticatedUser = (LDAPAuthenticatedUser) authenticatedUser; - LDAPConfiguration config = ldapAuthenticatedUser.getLDAPConfiguration(); - Dn bindDn = ldapAuthenticatedUser.getBindDn(); - - LdapNetworkConnection ldapConnection = ldapService.bindAs(config, bindDn.getName(), credentials.getPassword()); - if (ldapConnection == null) { - logger.debug("LDAP bind succeeded for \"{}\" during " - + "authentication but failed during data retrieval.", - authenticatedUser.getIdentifier()); - throw new GuacamoleInvalidCredentialsException("Invalid login.", - CredentialsInfo.USERNAME_PASSWORD); - } + ConnectedLDAPConfiguration config = ldapAuthenticatedUser.getLDAPConfiguration(); try { // Build user context by querying LDAP LDAPUserContext userContext = userContextProvider.get(); - userContext.init(ldapAuthenticatedUser, ldapConnection); + userContext.init(ldapAuthenticatedUser); return userContext; } // Always disconnect finally { - ldapConnection.close(); + config.close(); } } + return null; } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java new file mode 100644 index 000000000..f4a875e6e --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java @@ -0,0 +1,210 @@ +/* + * 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 java.util.List; +import org.apache.directory.api.ldap.model.filter.ExprNode; +import org.apache.directory.api.ldap.model.message.AliasDerefMode; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.directory.ldap.client.api.LdapNetworkConnection; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.ldap.conf.EncryptionMethod; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; +import org.apache.guacamole.auth.ldap.conf.MemberAttributeType; + +/** + * LDAPConfiguration implementation that is associated with an + * LdapNetworkConnection to the configured LDAP server. + */ +public class ConnectedLDAPConfiguration implements LDAPConfiguration, AutoCloseable { + + /** + * The wrapped LDAPConfiguration. + */ + private final LDAPConfiguration config; + + /** + * The connection to the LDAP server represented by this configuration. + */ + private final LdapNetworkConnection connection; + + /** + * The LDAP DN that was used to bind with the LDAP server to produce + * {@link #connection}. + */ + private final Dn bindDn; + + /** + * Creates a new ConnectedLDAPConfiguration that associates the given + * LdapNetworkConnection with the given LDAPConfiguration. All functions + * inherited from the LDAPConfiguration interface are delegated to the + * given LDAPConfiguration. It is the responsibility of the caller to + * ensure the provided LdapNetworkConnection is closed after it is no + * longer needed. + * + * @param config + * The LDAPConfiguration to wrap. + * + * @param bindDn + * The LDAP DN that was used to bind with the LDAP server to produce + * the given LdapNetworkConnection. + * + * @param connection + * The connection to the LDAP server represented by the given + * configuration. + */ + public ConnectedLDAPConfiguration(LDAPConfiguration config, Dn bindDn, LdapNetworkConnection connection) { + this.config = config; + this.bindDn = bindDn; + this.connection = connection; + } + + /** + * Returns the LdapNetworkConnection for the connection to the LDAP server + * represented by this configuration. The lifecycle of this connection is + * managed externally. The connection is not guaranteed to still be + * connected. + * + * @return + * The LdapNetworkConnection for the connection to the LDAP server + * represented by this configuration. + */ + public LdapNetworkConnection getLDAPConnection() { + return connection; + } + + /** + * Returns the LDAP DN that was used to bind with the LDAP server to + * produce the LdapNetworkConnection associated with this + * ConnectedLDAPConfiguration. + * + * @return + * The LDAP DN that was used to bind with the LDAP server. + */ + public Dn getBindDN() { + return bindDn; + } + + @Override + public void close() { + connection.close(); + } + + @Override + public String getServerHostname() throws GuacamoleException { + return config.getServerHostname(); + } + + @Override + public int getServerPort() throws GuacamoleException { + return config.getServerPort(); + } + + @Override + public List getUsernameAttributes() throws GuacamoleException { + return config.getUsernameAttributes(); + } + + @Override + public Dn getUserBaseDN() throws GuacamoleException { + return config.getUserBaseDN(); + } + + @Override + public Dn getConfigurationBaseDN() throws GuacamoleException { + return config.getConfigurationBaseDN(); + } + + @Override + public List getGroupNameAttributes() throws GuacamoleException { + return config.getGroupNameAttributes(); + } + + @Override + public Dn getGroupBaseDN() throws GuacamoleException { + return config.getGroupBaseDN(); + } + + @Override + public String getSearchBindDN() throws GuacamoleException { + return config.getSearchBindDN(); + } + + @Override + public String getSearchBindPassword() throws GuacamoleException { + return config.getSearchBindPassword(); + } + + @Override + public EncryptionMethod getEncryptionMethod() throws GuacamoleException { + return config.getEncryptionMethod(); + } + + @Override + public int getMaxResults() throws GuacamoleException { + return config.getMaxResults(); + } + + @Override + public AliasDerefMode getDereferenceAliases() throws GuacamoleException { + return config.getDereferenceAliases(); + } + + @Override + public boolean getFollowReferrals() throws GuacamoleException { + return config.getFollowReferrals(); + } + + @Override + public int getMaxReferralHops() throws GuacamoleException { + return config.getMaxReferralHops(); + } + + @Override + public ExprNode getUserSearchFilter() throws GuacamoleException { + return config.getUserSearchFilter(); + } + + @Override + public ExprNode getGroupSearchFilter() throws GuacamoleException { + return config.getGroupSearchFilter(); + } + + @Override + public int getOperationTimeout() throws GuacamoleException { + return config.getOperationTimeout(); + } + + @Override + public List getAttributes() throws GuacamoleException { + return config.getAttributes(); + } + + @Override + public String getMemberAttribute() throws GuacamoleException { + return config.getMemberAttribute(); + } + + @Override + public MemberAttributeType getMemberAttributeType() throws GuacamoleException { + return config.getMemberAttributeType(); + } + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index 80d98cd22..d84ac47ea 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -20,11 +20,13 @@ package org.apache.guacamole.auth.ldap.conf; import com.google.inject.Inject; +import java.util.Collection; +import java.util.Collections; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; /** - * Service for retrieving configuration information regarding the LDAP server. + * Service for retrieving configuration information regarding LDAP servers. */ public class ConfigurationService { @@ -35,24 +37,25 @@ public class ConfigurationService { private Environment environment; /** - * Returns the configuration information for the LDAP server related to the - * user having the given username. If no such LDAP server is defined, null - * is returned. + * Returns the configuration information for the LDAP servers related to + * the user having the given username, if any. If multiple servers are + * returned, each should be tried in order until a successful LDAP + * connection is established. * * @param username * The username of the user whose corresponding LDAP server * configuration should be retrieved. * * @return - * The configuration of the LDAP server related to the user having the - * given username, or null if no such LDAP server is defined. + * The configurations of the LDAP servers related to the user having + * the given username. * * @throws GuacamoleException - * If the configuration information of the LDAP server related to the + * If the configuration information of the LDAP servers related to the * user having the given username cannot be retrieved due to an error. */ - public LDAPConfiguration getLDAPConfiguration(String username) throws GuacamoleException { - return new EnvironmentLDAPConfiguration(environment); + public Collection getLDAPConfigurations(String username) throws GuacamoleException { + return Collections.singletonList(new EnvironmentLDAPConfiguration(environment)); } } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java index bff49858c..9da1547ba 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java @@ -35,13 +35,11 @@ import org.apache.directory.api.ldap.model.filter.EqualityNode; import org.apache.directory.api.ldap.model.filter.ExprNode; import org.apache.directory.api.ldap.model.filter.OrNode; import org.apache.directory.api.ldap.model.name.Dn; -import org.apache.directory.ldap.client.api.LdapConnectionConfig; -import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration; import org.apache.guacamole.auth.ldap.ObjectQueryService; -import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser; import org.apache.guacamole.net.auth.Connection; @@ -117,17 +115,12 @@ public class ConnectionService { /** - * Returns all Guacamole connections accessible to the user currently bound - * under the given LDAP connection. + * Returns all Guacamole connections accessible to the given user. * * @param user * The AuthenticatedUser object associated with the user who is * currently authenticated with Guacamole. * - * @param ldapConnection - * The current connection to the LDAP server, associated with the - * current user. - * * @return * All connections accessible to the user currently bound under the * given LDAP connection, as a map of connection identifier to @@ -136,11 +129,11 @@ public class ConnectionService { * @throws GuacamoleException * If an error occurs preventing retrieval of connections. */ - public Map getConnections(LDAPAuthenticatedUser user, - LdapNetworkConnection ldapConnection) throws GuacamoleException { + public Map getConnections(LDAPAuthenticatedUser user) + throws GuacamoleException { + + ConnectedLDAPConfiguration ldapConfig = user.getLDAPConfiguration(); - LDAPConfiguration ldapConfig = user.getLDAPConfiguration(); - // Do not return any connections if base DN is not specified Dn configurationBaseDN = ldapConfig.getConfigurationBaseDN(); if (configurationBaseDN == null) @@ -148,24 +141,15 @@ public class ConnectionService { try { - // Pull the current user DN from the LDAP connection - LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig(); - Dn userDN = new Dn(ldapConnectionConfig.getName()); - - // getConnections() will only be called after a connection has been - // authenticated (via non-anonymous bind), thus userDN cannot - // possibly be null - assert(userDN != null); - // Get the search filter for finding connections accessible by the // current user - ExprNode connectionSearchFilter = getConnectionSearchFilter(user, userDN, ldapConnection); + ExprNode connectionSearchFilter = getConnectionSearchFilter(user); // Find all Guacamole connections for the given user by // looking for direct membership in the guacConfigGroup // and possibly any groups the user is a member of that are // referred to in the seeAlso attribute of the guacConfigGroup. - List results = queryService.search(ldapConfig, ldapConnection, + List results = queryService.search(ldapConfig, ldapConfig.getLDAPConnection(), configurationBaseDN, connectionSearchFilter, 0, GUAC_CONFIG_LDAP_ATTRIBUTES); // Return a map of all readable connections @@ -271,19 +255,12 @@ public class ConnectionService { /** * Returns an LDAP search filter which queries all connections accessible - * by the user having the given DN. + * by the given user. * * @param user * The AuthenticatedUser object associated with the user who is * currently authenticated with Guacamole. * - * @param userDN - * DN of the user to search for associated guacConfigGroup connections. - * - * @param ldapConnection - * LDAP connection to use if additional information must be queried to - * produce the filter, such as groups driving RBAC. - * * @return * An LDAP search filter which queries all guacConfigGroup objects * accessible by the user having the given DN. @@ -294,11 +271,11 @@ public class ConnectionService { * @throws GuacamoleException * If an error occurs retrieving the group base DN. */ - private ExprNode getConnectionSearchFilter(LDAPAuthenticatedUser user, - Dn userDN, LdapNetworkConnection ldapConnection) + private ExprNode getConnectionSearchFilter(LDAPAuthenticatedUser user) throws LdapException, GuacamoleException { - LDAPConfiguration config = user.getLDAPConfiguration(); + ConnectedLDAPConfiguration config = user.getLDAPConfiguration(); + Dn userDN = config.getBindDN(); AndNode searchFilter = new AndNode(); @@ -312,7 +289,7 @@ public class ConnectionService { // Additionally filter by group membership if the current user is a // member of any user groups - List userGroups = userGroupService.getParentUserGroupEntries(config, ldapConnection, userDN); + List userGroups = userGroupService.getParentUserGroupEntries(config, userDN); if (!userGroups.isEmpty()) { userGroups.forEach(entry -> groupFilter.addNode(new EqualityNode(LDAP_ATTRIBUTE_NAME_GROUPS,entry.getDn().toString())) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java index a3dc9c1e8..b38fc9198 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java @@ -33,9 +33,9 @@ import org.apache.directory.api.ldap.model.filter.EqualityNode; import org.apache.directory.api.ldap.model.filter.ExprNode; import org.apache.directory.api.ldap.model.filter.NotNode; import org.apache.directory.api.ldap.model.name.Dn; -import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.auth.ldap.conf.MemberAttributeType; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration; import org.apache.guacamole.auth.ldap.ObjectQueryService; import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser; @@ -98,17 +98,12 @@ public class UserGroupService { } /** - * Returns all Guacamole user groups accessible to the user currently bound - * under the given LDAP connection. + * Returns all Guacamole user groups accessible to the given user. * * @param user * The AuthenticatedUser object associated with the user who is * currently authenticated with Guacamole. * - * @param ldapConnection - * The current connection to the LDAP server, associated with the - * current user. - * * @return * All user groups accessible to the user currently bound under the * given LDAP connection, as a map of user group identifier to @@ -117,10 +112,10 @@ public class UserGroupService { * @throws GuacamoleException * If an error occurs preventing retrieval of user groups. */ - public Map getUserGroups(LDAPAuthenticatedUser user, - LdapNetworkConnection ldapConnection) throws GuacamoleException { + public Map getUserGroups(LDAPAuthenticatedUser user) + throws GuacamoleException { - LDAPConfiguration config = user.getLDAPConfiguration(); + ConnectedLDAPConfiguration config = user.getLDAPConfiguration(); // Do not return any user groups if base DN is not specified Dn groupBaseDN = config.getGroupBaseDN(); @@ -136,7 +131,7 @@ public class UserGroupService { Collection attributes = config.getGroupNameAttributes(); List results = queryService.search( config, - ldapConnection, + config.getLDAPConnection(), groupBaseDN, getGroupSearchFilter(config), attributes, @@ -175,10 +170,6 @@ public class UserGroupService { * @param config * The configuration of the LDAP server being queried. * - * @param ldapConnection - * The current connection to the LDAP server, associated with the - * current user. - * * @param userDN * The DN of the user whose group membership should be retrieved. * @@ -189,8 +180,7 @@ public class UserGroupService { * @throws GuacamoleException * If an error occurs preventing retrieval of user groups. */ - public List getParentUserGroupEntries(LDAPConfiguration config, - LdapNetworkConnection ldapConnection, Dn userDN) + public List getParentUserGroupEntries(ConnectedLDAPConfiguration config, Dn userDN) throws GuacamoleException { // Do not return any user groups if base DN is not specified @@ -206,7 +196,7 @@ public class UserGroupService { // Retrieve user objects with userDN List userEntries = queryService.search( config, - ldapConnection, + config.getLDAPConnection(), userDN, config.getUserSearchFilter(), 0, @@ -240,7 +230,7 @@ public class UserGroupService { // excluding guacConfigGroups return queryService.search( config, - ldapConnection, + config.getLDAPConnection(), groupBaseDN, getGroupSearchFilter(config), Collections.singleton(memberAttribute), @@ -258,10 +248,6 @@ public class UserGroupService { * @param config * The configuration of the LDAP server being queried. * - * @param ldapConnection - * The current connection to the LDAP server, associated with the - * current user. - * * @param userDN * The DN of the user whose group membership should be retrieved. * @@ -272,12 +258,11 @@ public class UserGroupService { * @throws GuacamoleException * If an error occurs preventing retrieval of user groups. */ - public Set getParentUserGroupIdentifiers(LDAPConfiguration config, - LdapNetworkConnection ldapConnection, Dn userDN) + public Set getParentUserGroupIdentifiers(ConnectedLDAPConfiguration config, Dn userDN) throws GuacamoleException { Collection attributes = config.getGroupNameAttributes(); - List userGroups = getParentUserGroupEntries(config, ldapConnection, userDN); + List userGroups = getParentUserGroupEntries(config, userDN); Set identifiers = new HashSet<>(userGroups.size()); userGroups.forEach(entry -> { diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java index 97eab6cae..6889e61a7 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java @@ -24,7 +24,7 @@ import java.util.Collections; import java.util.Map; import java.util.Set; import org.apache.directory.api.ldap.model.name.Dn; -import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; +import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration; import org.apache.guacamole.net.auth.AbstractAuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; @@ -68,11 +68,11 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { * The configuration of the LDAP server that should be used for all queries * related to this AuthenticatedUser. */ - private LDAPConfiguration config; + private ConnectedLDAPConfiguration config; /** * Initializes this AuthenticatedUser with the given credentials, - * connection parameter tokens. and set of effective user groups. + * connection parameter tokens, and set of effective user groups. * * @param config * The configuration of the LDAP server that should be used for all @@ -88,17 +88,14 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { * @param effectiveGroups * The unique identifiers of all user groups which affect the * permissions available to this user. - * - * @param bindDn - * The LDAP DN used to bind this user. */ - public void init(LDAPConfiguration config, Credentials credentials, - Map tokens, Set effectiveGroups, Dn bindDn) { + public void init(ConnectedLDAPConfiguration config, Credentials credentials, + Map tokens, Set effectiveGroups) { this.config = config; this.credentials = credentials; this.tokens = Collections.unmodifiableMap(tokens); this.effectiveGroups = effectiveGroups; - this.bindDn = bindDn; + this.bindDn = config.getBindDN(); setIdentifier(credentials.getUsername()); } @@ -115,7 +112,7 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { public Map getTokens() { return tokens; } - + /** * Returns the LDAP DN used to bind this user. * @@ -134,7 +131,7 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { * The configuration of the LDAP server related to this * AuthenticatedUser. */ - public LDAPConfiguration getLDAPConfiguration() { + public ConnectedLDAPConfiguration getLDAPConfiguration() { return config; } @@ -153,4 +150,9 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { return effectiveGroups; } + @Override + public void invalidate() { + config.close(); + } + } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java index cb2afd516..79b1af718 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java @@ -21,7 +21,6 @@ package org.apache.guacamole.auth.ldap.user; import com.google.inject.Inject; import java.util.Collections; -import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.auth.ldap.connection.ConnectionService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider; @@ -100,38 +99,32 @@ public class LDAPUserContext extends AbstractUserContext { private ConnectionGroup rootGroup; /** - * Initializes this UserContext using the provided AuthenticatedUser and - * LdapNetworkConnection. + * Initializes this UserContext using the provided AuthenticatedUser. * * @param user * The AuthenticatedUser representing the user that authenticated. This * user will always have been authenticated via LDAP, as LDAP data is * not provided to non-LDAP users. * - * @param ldapConnection - * The connection to the LDAP server to use when querying accessible - * Guacamole users and connections. - * * @throws GuacamoleException * If associated data stored within the LDAP directory cannot be * queried due to an error. */ - public void init(LDAPAuthenticatedUser user, LdapNetworkConnection ldapConnection) - throws GuacamoleException { + public void init(LDAPAuthenticatedUser user) throws GuacamoleException { // Query all accessible users userDirectory = new SimpleDirectory<>( - userService.getUsers(user, ldapConnection) + userService.getUsers(user) ); // Query all accessible user groups userGroupDirectory = new SimpleDirectory<>( - userGroupService.getUserGroups(user, ldapConnection) + userGroupService.getUserGroups(user) ); // Query all accessible connections connectionDirectory = new SimpleDirectory<>( - connectionService.getConnections(user, ldapConnection) + connectionService.getConnections(user) ); // Root group contains only connections 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 69df58a79..fa9fe1522 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 @@ -34,6 +34,7 @@ import org.apache.directory.api.ldap.model.name.Rdn; import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration; import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties; import org.apache.guacamole.auth.ldap.ObjectQueryService; import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; @@ -60,17 +61,12 @@ public class UserService { private ObjectQueryService queryService; /** - * Returns all Guacamole users accessible to the user currently bound under - * the given LDAP connection. + * Returns all Guacamole users accessible to the given user. * * @param user * The AuthenticatedUser object associated with the user who is * currently authenticated with Guacamole. * - * @param ldapConnection - * The current connection to the LDAP server, associated with the - * current user. - * * @return * All users accessible to the user currently bound under the given * LDAP connection, as a map of connection identifier to corresponding @@ -79,21 +75,24 @@ public class UserService { * @throws GuacamoleException * If an error occurs preventing retrieval of users. */ - public Map getUsers(LDAPAuthenticatedUser user, - LdapNetworkConnection ldapConnection) throws GuacamoleException { + public Map getUsers(LDAPAuthenticatedUser user) + throws GuacamoleException { - LDAPConfiguration config = user.getLDAPConfiguration(); + ConnectedLDAPConfiguration config = user.getLDAPConfiguration(); // Retrieve all visible user objects Collection usernameAttrs = config.getUsernameAttributes(); Collection attributes = new HashSet<>(usernameAttrs); attributes.addAll(config.getAttributes()); - List results = queryService.search(config, ldapConnection, - config.getUserBaseDN(), - config.getUserSearchFilter(), - usernameAttrs, - null, - attributes); + List results = queryService.search( + config, + config.getLDAPConnection(), + config.getUserBaseDN(), + config.getUserSearchFilter(), + usernameAttrs, + null, + attributes + ); // Convert retrieved users to map of identifier to Guacamole user object return queryService.asMap(results, entry -> { From 49a4a6c7a0c505e82f947b29e2f28a9556b99a68 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 20 Oct 2021 23:35:13 -0700 Subject: [PATCH 03/14] GUACAMOLE-957: Support reading multiple LDAP server configurations from "ldap-servers.yml". --- .../jackson-2.12.2/dep-coordinates.txt | 1 + doc/licenses/snakeyaml-1.27/README | 8 + .../snakeyaml-1.27/dep-coordinates.txt | 1 + extensions/guacamole-auth-ldap/pom.xml | 10 + .../ldap/AuthenticationProviderService.java | 2 +- .../auth/ldap/conf/ConfigurationService.java | 43 ++- .../ldap/conf/JacksonLDAPConfiguration.java | 315 ++++++++++++++++++ pom.xml | 5 + 8 files changed, 383 insertions(+), 2 deletions(-) create mode 100644 doc/licenses/snakeyaml-1.27/README create mode 100644 doc/licenses/snakeyaml-1.27/dep-coordinates.txt create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java diff --git a/doc/licenses/jackson-2.12.2/dep-coordinates.txt b/doc/licenses/jackson-2.12.2/dep-coordinates.txt index 6dcc7917e..3f73c2e2c 100644 --- a/doc/licenses/jackson-2.12.2/dep-coordinates.txt +++ b/doc/licenses/jackson-2.12.2/dep-coordinates.txt @@ -1,4 +1,5 @@ com.fasterxml.jackson.core:jackson-databind:jar:2.12.2 com.fasterxml.jackson.core:jackson-core:jar:2.12.2 com.fasterxml.jackson.core:jackson-annotations:jar:2.12.2 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:jar:2.12.2 com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.12.2 diff --git a/doc/licenses/snakeyaml-1.27/README b/doc/licenses/snakeyaml-1.27/README new file mode 100644 index 000000000..3fcd837d6 --- /dev/null +++ b/doc/licenses/snakeyaml-1.27/README @@ -0,0 +1,8 @@ +SnakeYAML (https://bitbucket.org/asomov/snakeyaml/) +--------------------------------------------------- + + Version: 1.27 + From: 'Andrey Somov' (https://bitbucket.org/asomov/) + License(s): + Apache v2.0 + diff --git a/doc/licenses/snakeyaml-1.27/dep-coordinates.txt b/doc/licenses/snakeyaml-1.27/dep-coordinates.txt new file mode 100644 index 000000000..d7cbad91a --- /dev/null +++ b/doc/licenses/snakeyaml-1.27/dep-coordinates.txt @@ -0,0 +1 @@ +org.yaml:snakeyaml:jar:1.27 diff --git a/extensions/guacamole-auth-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml index fa9d8a215..5aa8f967d 100644 --- a/extensions/guacamole-auth-ldap/pom.xml +++ b/extensions/guacamole-auth-ldap/pom.xml @@ -60,6 +60,16 @@ guice + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java index 42afdf510..f56f9e592 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java @@ -196,7 +196,7 @@ public class AuthenticationProviderService { String password) throws GuacamoleException { // Get relevant LDAP configurations for user - Collection configs = confService.getLDAPConfigurations(username); + Collection configs = confService.getLDAPConfigurations(username); if (configs.isEmpty()) { logger.info("User \"{}\" does not map to any defined LDAP configurations.", username); return null; diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index d84ac47ea..e8bafcaa6 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -19,17 +19,41 @@ package org.apache.guacamole.auth.ldap.conf; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.inject.Inject; +import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.Collections; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Service for retrieving configuration information regarding LDAP servers. */ public class ConfigurationService { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(ConfigurationService.class); + + /** + * ObjectMapper for deserializing YAML. + */ + private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + /** + * The name of the file within GUACAMOLE_HOME that defines each available + * LDAP server (if not using guacamole.properties). + */ + private static final String LDAP_SERVERS_YML = "ldap-servers.yml"; + /** * The Guacamole server environment. */ @@ -54,8 +78,25 @@ public class ConfigurationService { * If the configuration information of the LDAP servers related to the * user having the given username cannot be retrieved due to an error. */ - public Collection getLDAPConfigurations(String username) throws GuacamoleException { + public Collection getLDAPConfigurations(String username) throws GuacamoleException { + + // Read configuration from YAML, if available + File ldapServers = new File(environment.getGuacamoleHome(), LDAP_SERVERS_YML); + if (ldapServers.exists()) { + try { + logger.debug("Reading LDAP configuration from \"{}\"...", ldapServers); + return mapper.readValue(ldapServers, new TypeReference>() {}); + } + catch (IOException e) { + logger.error("\"{}\" could not be read/parsed: {}", ldapServers, e.getMessage()); + throw new GuacamoleServerException("Cannot read LDAP configuration from \"" + ldapServers + "\"", e); + } + } + + // Use guacamole.properties if not using YAML + logger.debug("Reading LDAP configuration from guacamole.properties..."); return Collections.singletonList(new EnvironmentLDAPConfiguration(environment)); + } } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java new file mode 100644 index 000000000..1676d8ac3 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -0,0 +1,315 @@ +/* + * 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.conf; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.List; +import org.apache.directory.api.ldap.model.filter.ExprNode; +import org.apache.directory.api.ldap.model.filter.PresenceNode; +import org.apache.directory.api.ldap.model.message.AliasDerefMode; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; + +/** + * LDAPConfiguration implementation that is annotated for deserialization by + * Jackson. + */ +public class JacksonLDAPConfiguration implements LDAPConfiguration { + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_HOSTNAME}. If + * not set within the YAML, this will be null. + */ + @JsonProperty("hostname") + private String hostname; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_PORT}. If not + * set within the YAML, this will be null. + */ + @JsonProperty("port") + private Integer port; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USERNAME_ATTRIBUTES}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("username-attribute") + private List usernameAttributes; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USER_BASE_DN}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("user-base-dn") + private String userBaseDn; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_CONFIG_BASE_DN}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("config-base-dn") + private String configBaseDn; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_GROUP_BASE_DN}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("group-base-dn") + private String groupBaseDn; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_GROUP_NAME_ATTRIBUTES}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("group-name-attribute") + private List groupNameAttributes; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_SEARCH_BIND_DN}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("search-bind-dn") + private String searchBindDn; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_SEARCH_BIND_PASSWORD}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("search-bind-password") + private String searchBindPassword; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_ENCRYPTION_METHOD}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("encryption-method") + private String encryptionMethod; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MAX_SEARCH_RESULTS}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("max-search-results") + private Integer maxSearchResults; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_DEREFERENCE_ALIASES}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("dereference-aliases") + private String dereferenceAliases; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_FOLLOW_REFERRALS}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("follow-referrals") + private Boolean followReferrals; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MAX_REFERRAL_HOPS}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("max-referral-hops") + private Integer maxReferralHops; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USER_SEARCH_FILTER}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("user-search-filter") + private String userSearchFilter; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_GROUP_SEARCH_FILTER}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("group-search-filter") + private String groupSearchFilter; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_OPERATION_TIMEOUT}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("operation-timeout") + private Integer operationTimeout; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USER_ATTRIBUTES}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("user-attributes") + private List userAttributes; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MEMBER_ATTRIBUTE}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("member-attribute") + private String memberAttribute; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MEMBER_ATTRIBUTE_TYPE}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("member-attribute-type") + private String memberAttributeType; + + @Override + public String getServerHostname() { + return hostname != null ? hostname : "localhost"; + } + + @Override + public int getServerPort() throws GuacamoleException { + return port != null ? port : getEncryptionMethod().DEFAULT_PORT; + } + + @Override + public List getUsernameAttributes() { + return usernameAttributes != null ? usernameAttributes : Collections.singletonList("uid"); + } + + @Override + public Dn getUserBaseDN() throws GuacamoleException { + + Dn parsedDn = LDAPGuacamoleProperties.LDAP_USER_BASE_DN.parseValue(userBaseDn); + if (parsedDn == null) + throw new GuacamoleServerException("The \"user-base-dn\" property is required for all LDAP servers."); + + return parsedDn; + + } + + @Override + public Dn getConfigurationBaseDN() throws GuacamoleException { + return LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN.parseValue(configBaseDn); + } + + @Override + public List getGroupNameAttributes() throws GuacamoleException { + return groupNameAttributes != null ? groupNameAttributes : Collections.singletonList("cn"); + } + + @Override + public Dn getGroupBaseDN() throws GuacamoleException { + return LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN.parseValue(groupBaseDn); + } + + @Override + public String getSearchBindDN() throws GuacamoleException { + return searchBindDn; + } + + @Override + public String getSearchBindPassword() throws GuacamoleException { + return searchBindPassword; + } + + @Override + public EncryptionMethod getEncryptionMethod() throws GuacamoleException { + + EncryptionMethod parsedMethod = LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD.parseValue(encryptionMethod); + if (parsedMethod == null) + return EncryptionMethod.NONE; + + return parsedMethod; + + } + + @Override + public int getMaxResults() throws GuacamoleException { + return maxSearchResults != null ? maxSearchResults : 1000; + } + + @Override + public AliasDerefMode getDereferenceAliases() throws GuacamoleException { + + AliasDerefMode parsedMode = LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES.parseValue(dereferenceAliases); + if (parsedMode == null) + return AliasDerefMode.NEVER_DEREF_ALIASES; + + return parsedMode; + + } + + @Override + public boolean getFollowReferrals() throws GuacamoleException { + return followReferrals != null ? followReferrals : false; + } + + @Override + public int getMaxReferralHops() throws GuacamoleException { + return maxReferralHops != null ? maxReferralHops : 5; + } + + @Override + public ExprNode getUserSearchFilter() throws GuacamoleException { + + ExprNode parsedFilter = LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER.parseValue(userSearchFilter); + if (parsedFilter == null) + return new PresenceNode("objectClass"); + + return parsedFilter; + + } + + @Override + public ExprNode getGroupSearchFilter() throws GuacamoleException { + + ExprNode parsedFilter = LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER.parseValue(groupSearchFilter); + if (parsedFilter == null) + return new PresenceNode("objectClass"); + + return parsedFilter; + + } + + @Override + public int getOperationTimeout() throws GuacamoleException { + return operationTimeout != null ? operationTimeout : 30; + } + + @Override + public List getAttributes() throws GuacamoleException { + return userAttributes != null ? userAttributes : Collections.emptyList(); + } + + @Override + public String getMemberAttribute() throws GuacamoleException { + return memberAttribute != null ? memberAttribute : "member"; + } + + @Override + public MemberAttributeType getMemberAttributeType() + throws GuacamoleException { + + MemberAttributeType parsedType = LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE.parseValue(memberAttributeType); + if (parsedType == null) + return MemberAttributeType.DN; + + return parsedType; + + } + +} diff --git a/pom.xml b/pom.xml index 0480d5bf1..f133e2c18 100644 --- a/pom.xml +++ b/pom.xml @@ -377,6 +377,11 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + com.fasterxml.jackson.module jackson-module-jaxb-annotations From 91a057cad96eda25518ac60a363c7614a58f0f63 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 00:30:47 -0700 Subject: [PATCH 04/14] GUACAMOLE-957: Choose LDAP configurations based on usernames. --- .../ldap/AuthenticationProviderService.java | 18 ++++++++++---- .../auth/ldap/ConnectedLDAPConfiguration.java | 5 ++++ .../auth/ldap/conf/ConfigurationService.java | 20 ++++++---------- .../conf/EnvironmentLDAPConfiguration.java | 5 ++++ .../ldap/conf/JacksonLDAPConfiguration.java | 24 ++++++++++++++++++- .../auth/ldap/conf/LDAPConfiguration.java | 17 +++++++++++++ 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java index f56f9e592..486c524bd 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java @@ -195,10 +195,10 @@ public class AuthenticationProviderService { private ConnectedLDAPConfiguration getLDAPConfiguration(String username, String password) throws GuacamoleException { - // Get relevant LDAP configurations for user - Collection configs = confService.getLDAPConfigurations(username); + // Get all LDAP server configurations + Collection configs = confService.getLDAPConfigurations(); if (configs.isEmpty()) { - logger.info("User \"{}\" does not map to any defined LDAP configurations.", username); + logger.info("Skipping LDAP authentication as no LDAP servers are configured."); return null; } @@ -206,8 +206,18 @@ public class AuthenticationProviderService { // authentication are successful for (LDAPConfiguration config : configs) { + // Attempt connection only if username matches + String translatedUsername = config.appliesTo(username); + if (translatedUsername == null) { + logger.debug("LDAP server \"{}\" does not match username \"{}\".", config.getServerHostname(), username); + continue; + } + + logger.debug("LDAP server \"{}\" matched username \"{}\" as \"{}\".", + config.getServerHostname(), username, translatedUsername); + // Derive DN of user within this LDAP server - Dn bindDn = getUserBindDN(config, username); + Dn bindDn = getUserBindDN(config, translatedUsername); if (bindDn == null || bindDn.isEmpty()) { logger.info("Unable to determine DN of user \"{}\" using LDAP " + "server \"{}\". Proceeding with next server...", diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java index f4a875e6e..997e02376 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java @@ -107,6 +107,11 @@ public class ConnectedLDAPConfiguration implements LDAPConfiguration, AutoClosea connection.close(); } + @Override + public String appliesTo(String username) throws GuacamoleException { + return config.appliesTo(username); + } + @Override public String getServerHostname() throws GuacamoleException { return config.getServerHostname(); diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index e8bafcaa6..3ae11c6d2 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -61,24 +61,18 @@ public class ConfigurationService { private Environment environment; /** - * Returns the configuration information for the LDAP servers related to - * the user having the given username, if any. If multiple servers are - * returned, each should be tried in order until a successful LDAP - * connection is established. - * - * @param username - * The username of the user whose corresponding LDAP server - * configuration should be retrieved. + * Returns the configuration information for all configured LDAP servers. + * If multiple servers are returned, each should be tried in order until a + * successful LDAP connection is established. * * @return - * The configurations of the LDAP servers related to the user having - * the given username. + * The configurations of all LDAP servers. * * @throws GuacamoleException - * If the configuration information of the LDAP servers related to the - * user having the given username cannot be retrieved due to an error. + * If the configuration information of the LDAP servers cannot be + * retrieved due to an error. */ - public Collection getLDAPConfigurations(String username) throws GuacamoleException { + public Collection getLDAPConfigurations() throws GuacamoleException { // Read configuration from YAML, if available File ldapServers = new File(environment.getGuacamoleHome(), LDAP_SERVERS_YML); diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java index 4dca7cde2..37c20c4d1 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java @@ -50,6 +50,11 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { this.environment = environment; } + @Override + public String appliesTo(String username) throws GuacamoleException { + return username; + } + @Override public String getServerHostname() throws GuacamoleException { return environment.getProperty( diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index 1676d8ac3..467ce6987 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -22,6 +22,8 @@ package org.apache.guacamole.auth.ldap.conf; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.directory.api.ldap.model.filter.ExprNode; import org.apache.directory.api.ldap.model.filter.PresenceNode; import org.apache.directory.api.ldap.model.message.AliasDerefMode; @@ -35,6 +37,13 @@ import org.apache.guacamole.GuacamoleServerException; */ public class JacksonLDAPConfiguration implements LDAPConfiguration { + /** + * The regular expressions that match all users that should be routed to + * the LDAP server represented by this configuration. + */ + @JsonProperty("match-usernames") + private List matchUsernames; + /** * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_HOSTNAME}. If * not set within the YAML, this will be null. @@ -174,7 +183,20 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { */ @JsonProperty("member-attribute-type") private String memberAttributeType; - + + @Override + public String appliesTo(String username) throws GuacamoleException { + + for (Pattern pattern : matchUsernames) { + Matcher matcher = pattern.matcher(username); + if (matcher.matches()) + return matcher.groupCount() >= 1 ? matcher.group(1) : username; + } + + return null; + + } + @Override public String getServerHostname() { return hostname != null ? hostname : "localhost"; diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java index 5c3315741..ad008380c 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java @@ -31,6 +31,23 @@ import org.apache.guacamole.GuacamoleException; */ public interface LDAPConfiguration { + /** + * Tests whether this LDAPConfiguration applies to the user having the + * given username. If the configuration applies, the username that should + * be used to derive the user's DN is returned. + * + * @param username + * The username to test. + * + * @return + * The username that should be used to derive this user's DN, or null + * if the configuration does not apply. + * + * @throws GuacamoleException + * If an error prevents testing against this configuration. + */ + String appliesTo(String username) throws GuacamoleException; + /** * Returns the hostname or IP address of the LDAP server. By default, this * will be "localhost". From 0fc7c03d7033fd99b799c82288801a77acc43263 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 00:56:37 -0700 Subject: [PATCH 05/14] GUACAMOLE-957: Allow single-element array values for YAML LDAP configuration to be specified as simple strings. --- .../guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index 467ce6987..15ba9ff5a 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -19,6 +19,8 @@ package org.apache.guacamole.auth.ldap.conf; +import com.fasterxml.jackson.annotation.JsonFormat; +import static com.fasterxml.jackson.annotation.JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collections; import java.util.List; @@ -42,6 +44,7 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { * the LDAP server represented by this configuration. */ @JsonProperty("match-usernames") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) private List matchUsernames; /** @@ -63,6 +66,7 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { * If not set within the YAML, this will be null. */ @JsonProperty("username-attribute") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) private List usernameAttributes; /** @@ -91,6 +95,7 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { * If not set within the YAML, this will be null. */ @JsonProperty("group-name-attribute") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) private List groupNameAttributes; /** @@ -168,6 +173,7 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { * If not set within the YAML, this will be null. */ @JsonProperty("user-attributes") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) private List userAttributes; /** From f322dc3f3e4851546f38b83a275a0a5b8d4aa7de Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 15:08:28 -0700 Subject: [PATCH 06/14] GUACAMOLE-957: Cache LDAP configuration YAML until modified. --- .../auth/ldap/conf/ConfigurationService.java | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index 3ae11c6d2..2622d163e 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -23,10 +23,12 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.inject.Inject; +import com.google.inject.Singleton; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Collections; +import java.util.concurrent.atomic.AtomicLong; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; @@ -36,6 +38,7 @@ import org.slf4j.LoggerFactory; /** * Service for retrieving configuration information regarding LDAP servers. */ +@Singleton public class ConfigurationService { /** @@ -53,6 +56,17 @@ public class ConfigurationService { * LDAP server (if not using guacamole.properties). */ private static final String LDAP_SERVERS_YML = "ldap-servers.yml"; + + /** + * The timestamp that the {@link #LDAP_SERVERS_YML} was last modified when + * it was read, as would be returned by {@link File#lastModified()}. + */ + private final AtomicLong lastModified = new AtomicLong(0); + + /** + * The cached copy of the configuration read from {@link #LDAP_SERVERS_YML}. + */ + private Collection cachedConfigurations = Collections.emptyList(); /** * The Guacamole server environment. @@ -77,14 +91,27 @@ public class ConfigurationService { // Read configuration from YAML, if available File ldapServers = new File(environment.getGuacamoleHome(), LDAP_SERVERS_YML); if (ldapServers.exists()) { - try { - logger.debug("Reading LDAP configuration from \"{}\"...", ldapServers); - return mapper.readValue(ldapServers, new TypeReference>() {}); - } - catch (IOException e) { - logger.error("\"{}\" could not be read/parsed: {}", ldapServers, e.getMessage()); - throw new GuacamoleServerException("Cannot read LDAP configuration from \"" + ldapServers + "\"", e); + + long oldLastModified = lastModified.get(); + long currentLastModified = ldapServers.lastModified(); + + // Update cached copy of YAML if things have changed, ensuring only + // one concurrent request updates the cache at any given time + if (currentLastModified > oldLastModified && lastModified.compareAndSet(oldLastModified, currentLastModified)) { + try { + logger.debug("Reading updated LDAP configuration from \"{}\"...", ldapServers); + cachedConfigurations = mapper.readValue(ldapServers, new TypeReference>() {}); + } + catch (IOException e) { + logger.error("\"{}\" could not be read/parsed: {}", ldapServers, e.getMessage()); + throw new GuacamoleServerException("Cannot read LDAP configuration from \"" + ldapServers + "\"", e); + } } + else + logger.debug("Using cached LDAP configuration from \"{}\".", ldapServers); + + return cachedConfigurations; + } // Use guacamole.properties if not using YAML From 160d29df95768583c491b3e378658de6bd9f66d7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 15:59:00 -0700 Subject: [PATCH 07/14] GUACAMOLE-957: Define all default LDAP configuration values in central location. --- .../ldap/conf/DefaultLDAPConfiguration.java | 144 +++++++++++++++++ .../conf/EnvironmentLDAPConfiguration.java | 47 +++--- .../ldap/conf/JacksonLDAPConfiguration.java | 149 +++++++++++------- .../auth/ldap/conf/LDAPConfiguration.java | 44 ++---- 4 files changed, 276 insertions(+), 108 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java new file mode 100644 index 000000000..c7b5e71c4 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java @@ -0,0 +1,144 @@ +/* + * 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.conf; + +import java.util.Collections; +import java.util.List; +import org.apache.directory.api.ldap.model.filter.ExprNode; +import org.apache.directory.api.ldap.model.filter.PresenceNode; +import org.apache.directory.api.ldap.model.message.AliasDerefMode; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; + +/** + * LDAPConfiguration implementation that returns the default values for all + * configuration parameters. For any configuration parameters that are + * required (such as {@link #getUserBaseDN()}), an exception is thrown. + */ +public class DefaultLDAPConfiguration implements LDAPConfiguration { + + @Override + public String appliesTo(String username) { + return null; + } + + @Override + public String getServerHostname() { + return "localhost"; + } + + @Override + public int getServerPort() { + return getEncryptionMethod().DEFAULT_PORT; + } + + @Override + public List getUsernameAttributes() { + return Collections.singletonList("uid"); + } + + @Override + public Dn getUserBaseDN() throws GuacamoleException { + throw new GuacamoleServerException("All LDAP servers must have a defined user base DN."); + } + + @Override + public Dn getConfigurationBaseDN() { + return null; + } + + @Override + public List getGroupNameAttributes() { + return Collections.singletonList("cn"); + } + + @Override + public Dn getGroupBaseDN() { + return null; + } + + @Override + public String getSearchBindDN() { + return null; + } + + @Override + public String getSearchBindPassword() { + return null; + } + + @Override + public EncryptionMethod getEncryptionMethod() { + return EncryptionMethod.NONE; + } + + @Override + public int getMaxResults() { + return 1000; + } + + @Override + public AliasDerefMode getDereferenceAliases() { + return AliasDerefMode.NEVER_DEREF_ALIASES; + } + + @Override + public boolean getFollowReferrals() { + return false; + } + + @Override + public int getMaxReferralHops() { + return 5; + } + + @Override + public ExprNode getUserSearchFilter() { + return new PresenceNode("objectClass"); + } + + @Override + public ExprNode getGroupSearchFilter() { + return new PresenceNode("objectClass"); + } + + @Override + public int getOperationTimeout() { + return 30; + } + + @Override + public List getAttributes() { + return Collections.emptyList(); + } + + @Override + public String getMemberAttribute() { + return "member"; + } + + @Override + public MemberAttributeType getMemberAttributeType() + throws GuacamoleException { + return MemberAttributeType.DN; + } + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java index 37c20c4d1..dfc60f98d 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java @@ -19,10 +19,8 @@ package org.apache.guacamole.auth.ldap.conf; -import java.util.Collections; import java.util.List; import org.apache.directory.api.ldap.model.filter.ExprNode; -import org.apache.directory.api.ldap.model.filter.PresenceNode; import org.apache.directory.api.ldap.model.message.AliasDerefMode; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.guacamole.GuacamoleException; @@ -39,6 +37,11 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { */ private final Environment environment; + /** + * The default configuration options for all parameters. + */ + private static final LDAPConfiguration DEFAULT = new DefaultLDAPConfiguration(); + /** * Creates a new EnvironmentLDAPConfiguration that reads its configuration * details from guacamole.properties, as exposed by the given Environment. @@ -59,7 +62,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public String getServerHostname() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_HOSTNAME, - "localhost" + DEFAULT.getServerHostname() ); } @@ -75,7 +78,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public List getUsernameAttributes() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE, - Collections.singletonList("uid") + DEFAULT.getUsernameAttributes() ); } @@ -89,7 +92,8 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { @Override public Dn getConfigurationBaseDN() throws GuacamoleException { return environment.getProperty( - LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN + LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN, + DEFAULT.getConfigurationBaseDN() ); } @@ -97,28 +101,31 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public List getGroupNameAttributes() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_GROUP_NAME_ATTRIBUTE, - Collections.singletonList("cn") + DEFAULT.getGroupNameAttributes() ); } @Override public Dn getGroupBaseDN() throws GuacamoleException { return environment.getProperty( - LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN + LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN, + DEFAULT.getGroupBaseDN() ); } @Override public String getSearchBindDN() throws GuacamoleException { return environment.getProperty( - LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN + LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN, + DEFAULT.getSearchBindDN() ); } @Override public String getSearchBindPassword() throws GuacamoleException { return environment.getProperty( - LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD + LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD, + DEFAULT.getSearchBindPassword() ); } @@ -126,7 +133,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public EncryptionMethod getEncryptionMethod() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, - EncryptionMethod.NONE + DEFAULT.getEncryptionMethod() ); } @@ -134,7 +141,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public int getMaxResults() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS, - 1000 + DEFAULT.getMaxResults() ); } @@ -142,7 +149,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public AliasDerefMode getDereferenceAliases() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES, - AliasDerefMode.NEVER_DEREF_ALIASES + DEFAULT.getDereferenceAliases() ); } @@ -150,7 +157,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public boolean getFollowReferrals() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS, - false + DEFAULT.getFollowReferrals() ); } @@ -158,7 +165,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public int getMaxReferralHops() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS, - 5 + DEFAULT.getMaxReferralHops() ); } @@ -166,7 +173,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public ExprNode getUserSearchFilter() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER, - new PresenceNode("objectClass") + DEFAULT.getUserSearchFilter() ); } @@ -174,7 +181,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public ExprNode getGroupSearchFilter() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER, - new PresenceNode("objectClass") + DEFAULT.getGroupSearchFilter() ); } @@ -182,7 +189,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public int getOperationTimeout() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_OPERATION_TIMEOUT, - 30 + DEFAULT.getOperationTimeout() ); } @@ -190,7 +197,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public List getAttributes() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_USER_ATTRIBUTES, - Collections.emptyList() + DEFAULT.getAttributes() ); } @@ -198,7 +205,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { public String getMemberAttribute() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE, - "member" + DEFAULT.getMemberAttribute() ); } @@ -207,7 +214,7 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE, - MemberAttributeType.DN + DEFAULT.getMemberAttributeType() ); } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index 15ba9ff5a..a697144c7 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -22,16 +22,13 @@ package org.apache.guacamole.auth.ldap.conf; import com.fasterxml.jackson.annotation.JsonFormat; import static com.fasterxml.jackson.annotation.JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.directory.api.ldap.model.filter.ExprNode; -import org.apache.directory.api.ldap.model.filter.PresenceNode; import org.apache.directory.api.ldap.model.message.AliasDerefMode; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleServerException; /** * LDAPConfiguration implementation that is annotated for deserialization by @@ -190,6 +187,72 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { @JsonProperty("member-attribute-type") private String memberAttributeType; + /** + * The default configuration options for all parameters. + */ + private static final LDAPConfiguration DEFAULT = new DefaultLDAPConfiguration(); + + /** + * Returns the given value, if non-null. If null, the given default value + * is returned. + * + * @param + * The type of value accepted and returned. + * + * @param value + * The possibly null value to return if non-null. + * + * @param defaultValue + * The value to return if the provided value is null. + * + * @return + * The provided value, if non-null, otherwise the provided default + * value. + */ + private T withDefault(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * Returns the given Integer value as an int, if non-null. If null, the + * given int default value is returned. This function is an Integer-specific + * variant of {@link #withDefault(java.lang.Object, java.lang.Object)} + * which avoids unnecessary boxing/unboxing. + * + * @param value + * The possibly null value to return if non-null. + * + * @param defaultValue + * The value to return if the provided value is null. + * + * @return + * The provided value, if non-null, otherwise the provided default + * value. + */ + private int withDefault(Integer value, int defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * Returns the given Boolean value as an boolean, if non-null. If null, the + * given boolean default value is returned. This function is a Boolean- + * specific variant of {@link #withDefault(java.lang.Object, java.lang.Object)} + * which avoids unnecessary boxing/unboxing. + * + * @param value + * The possibly null value to return if non-null. + * + * @param defaultValue + * The value to return if the provided value is null. + * + * @return + * The provided value, if non-null, otherwise the provided default + * value. + */ + private boolean withDefault(Boolean value, boolean defaultValue) { + return value != null ? value : defaultValue; + } + @Override public String appliesTo(String username) throws GuacamoleException { @@ -204,140 +267,104 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { } @Override - public String getServerHostname() { - return hostname != null ? hostname : "localhost"; + public String getServerHostname() throws GuacamoleException { + return withDefault(hostname, DEFAULT.getServerHostname()); } @Override public int getServerPort() throws GuacamoleException { - return port != null ? port : getEncryptionMethod().DEFAULT_PORT; + return withDefault(port, getEncryptionMethod().DEFAULT_PORT); } @Override - public List getUsernameAttributes() { - return usernameAttributes != null ? usernameAttributes : Collections.singletonList("uid"); + public List getUsernameAttributes() throws GuacamoleException { + return withDefault(usernameAttributes, DEFAULT.getUsernameAttributes()); } @Override public Dn getUserBaseDN() throws GuacamoleException { - - Dn parsedDn = LDAPGuacamoleProperties.LDAP_USER_BASE_DN.parseValue(userBaseDn); - if (parsedDn == null) - throw new GuacamoleServerException("The \"user-base-dn\" property is required for all LDAP servers."); - - return parsedDn; - + return withDefault(LDAPGuacamoleProperties.LDAP_USER_BASE_DN.parseValue(userBaseDn), DEFAULT.getUserBaseDN()); } @Override public Dn getConfigurationBaseDN() throws GuacamoleException { - return LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN.parseValue(configBaseDn); + return withDefault(LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN.parseValue(configBaseDn), DEFAULT.getConfigurationBaseDN()); } @Override public List getGroupNameAttributes() throws GuacamoleException { - return groupNameAttributes != null ? groupNameAttributes : Collections.singletonList("cn"); + return withDefault(groupNameAttributes, DEFAULT.getGroupNameAttributes()); } @Override public Dn getGroupBaseDN() throws GuacamoleException { - return LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN.parseValue(groupBaseDn); + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN.parseValue(groupBaseDn), DEFAULT.getGroupBaseDN()); } @Override public String getSearchBindDN() throws GuacamoleException { - return searchBindDn; + return withDefault(searchBindDn, DEFAULT.getSearchBindDN()); } @Override public String getSearchBindPassword() throws GuacamoleException { - return searchBindPassword; + return withDefault(searchBindPassword, DEFAULT.getSearchBindDN()); } @Override public EncryptionMethod getEncryptionMethod() throws GuacamoleException { - - EncryptionMethod parsedMethod = LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD.parseValue(encryptionMethod); - if (parsedMethod == null) - return EncryptionMethod.NONE; - - return parsedMethod; - + return withDefault(LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD.parseValue(encryptionMethod), DEFAULT.getEncryptionMethod()); } @Override public int getMaxResults() throws GuacamoleException { - return maxSearchResults != null ? maxSearchResults : 1000; + return withDefault(maxSearchResults, DEFAULT.getMaxResults()); } @Override public AliasDerefMode getDereferenceAliases() throws GuacamoleException { - - AliasDerefMode parsedMode = LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES.parseValue(dereferenceAliases); - if (parsedMode == null) - return AliasDerefMode.NEVER_DEREF_ALIASES; - - return parsedMode; - + return withDefault(LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES.parseValue(dereferenceAliases), DEFAULT.getDereferenceAliases()); } @Override public boolean getFollowReferrals() throws GuacamoleException { - return followReferrals != null ? followReferrals : false; + return withDefault(followReferrals, DEFAULT.getFollowReferrals()); } @Override public int getMaxReferralHops() throws GuacamoleException { - return maxReferralHops != null ? maxReferralHops : 5; + return withDefault(maxReferralHops, DEFAULT.getMaxReferralHops()); } @Override public ExprNode getUserSearchFilter() throws GuacamoleException { - - ExprNode parsedFilter = LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER.parseValue(userSearchFilter); - if (parsedFilter == null) - return new PresenceNode("objectClass"); - - return parsedFilter; - + return withDefault(LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER.parseValue(userSearchFilter), DEFAULT.getUserSearchFilter()); } @Override public ExprNode getGroupSearchFilter() throws GuacamoleException { - - ExprNode parsedFilter = LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER.parseValue(groupSearchFilter); - if (parsedFilter == null) - return new PresenceNode("objectClass"); - - return parsedFilter; - + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER.parseValue(groupSearchFilter), DEFAULT.getGroupSearchFilter()); } @Override public int getOperationTimeout() throws GuacamoleException { - return operationTimeout != null ? operationTimeout : 30; + return withDefault(operationTimeout, DEFAULT.getOperationTimeout()); } @Override public List getAttributes() throws GuacamoleException { - return userAttributes != null ? userAttributes : Collections.emptyList(); + return withDefault(userAttributes, DEFAULT.getAttributes()); } @Override public String getMemberAttribute() throws GuacamoleException { - return memberAttribute != null ? memberAttribute : "member"; + return withDefault(memberAttribute, DEFAULT.getMemberAttribute()); } @Override public MemberAttributeType getMemberAttributeType() throws GuacamoleException { - - MemberAttributeType parsedType = LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE.parseValue(memberAttributeType); - if (parsedType == null) - return MemberAttributeType.DN; - - return parsedType; - + return withDefault(LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE.parseValue(memberAttributeType), DEFAULT.getMemberAttributeType()); } } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java index ad008380c..99f36fa46 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java @@ -49,8 +49,7 @@ public interface LDAPConfiguration { String appliesTo(String username) throws GuacamoleException; /** - * Returns the hostname or IP address of the LDAP server. By default, this - * will be "localhost". + * Returns the hostname or IP address of the LDAP server. * * @return * The hostname or IP address of the LDAP server. @@ -76,8 +75,7 @@ public interface LDAPConfiguration { /** * Returns all username attributes which should be used to query and bind - * users using the LDAP directory. By default, this will be "uid" - a - * common attribute used for this purpose. + * users using the LDAP directory. * * @return * The username attributes which should be used to query and bind users @@ -118,7 +116,7 @@ public interface LDAPConfiguration { /** * Returns all attributes which should be used to determine the unique - * identifier of each user group. By default, this will be "cn". + * identifier of each user group. * * @return * The attributes which should be used to determine the unique @@ -175,7 +173,7 @@ public interface LDAPConfiguration { /** * Returns the encryption method that should be used when connecting to the - * LDAP server. By default, no encryption is used. + * LDAP server. * * @return * The encryption method that should be used when connecting to the @@ -187,8 +185,7 @@ public interface LDAPConfiguration { EncryptionMethod getEncryptionMethod() throws GuacamoleException; /** - * Returns maximum number of results a LDAP query can return. By default, - * this will be 1000. + * Returns maximum number of results a LDAP query can return. * * @return * The maximum number of results a LDAP query can return. @@ -199,8 +196,7 @@ public interface LDAPConfiguration { int getMaxResults() throws GuacamoleException; /** - * Returns whether or not LDAP aliases will be dereferenced. By default, - * aliases are never dereferenced. + * Returns whether or not LDAP aliases will be dereferenced. * * @return * The LDAP alias dereferencing mode. @@ -211,8 +207,7 @@ public interface LDAPConfiguration { AliasDerefMode getDereferenceAliases() throws GuacamoleException; /** - * Returns whether referrals should be automatically followed. By default, - * referrals are not followed. + * Returns whether referrals should be automatically followed. * * @return * Whether referrals should be followed. @@ -224,8 +219,7 @@ public interface LDAPConfiguration { boolean getFollowReferrals() throws GuacamoleException; /** - * Returns the maximum number of referral hops to follow. By default - * a maximum of 5 hops is allowed. + * Returns the maximum number of referral hops to follow. * * @return * The maximum number of referral hops to follow. @@ -237,13 +231,11 @@ public interface LDAPConfiguration { /** * Returns the search filter that should be used when querying the - * LDAP server for Guacamole users. If no filter is specified, - * a default of "(objectClass=user)" is returned. + * LDAP server for Guacamole users. * * @return * The search filter that should be used when querying the - * LDAP server for users that are valid in Guacamole, or - * "(objectClass=user)" if not specified. + * LDAP server for users that are valid in Guacamole. * * @throws GuacamoleException * If the user search filter cannot be retrieved. @@ -252,13 +244,11 @@ public interface LDAPConfiguration { /** * Returns the search filter that should be used when querying the - * LDAP server for Guacamole groups. If no filter is specified, - * a default of "(objectClass=*)" is used. + * LDAP server for Guacamole groups. * * @return * The search filter that should be used when querying the - * LDAP server for groups that are valid in Guacamole, or - * "(objectClass=*)" if not specified. + * LDAP server for groups that are valid in Guacamole. * * @throws GuacamoleException * If the group search filter cannot be retrieved. @@ -277,22 +267,22 @@ public interface LDAPConfiguration { int getOperationTimeout() throws GuacamoleException; /** - * Returns names for custom LDAP user attributes that should be made - * available as parameter tokens. By default, no additional LDAP attributes - * will be exposed as parameter tokens. + * Returns names of any LDAP user attributes that should be made available + * as parameter tokens. * * @return * A list of all LDAP user attributes that should be made available as * parameter tokens. * * @throws GuacamoleException - * If the names of custom LDAP user attributes cannot be retrieved. + * If the names of the LDAP user attributes to be exposed as parameter + * tokens cannot be retrieved. */ List getAttributes() throws GuacamoleException; /** * Returns the name of the LDAP attribute used to enumerate members in a - * group. By default, this will be "member". + * group. * * @return * The name of the LDAP attribute to use to enumerate From 55437faad06f46280bec49cdfd671abcd9d202c4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 16:07:28 -0700 Subject: [PATCH 08/14] GUACAMOLE-957: Use guacamole.properties for LDAP YAML defaults. --- .../auth/ldap/conf/ConfigurationService.java | 10 +++- .../ldap/conf/JacksonLDAPConfiguration.java | 53 ++++++++++++------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index 2622d163e..fab1a7027 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -99,8 +99,16 @@ public class ConfigurationService { // one concurrent request updates the cache at any given time if (currentLastModified > oldLastModified && lastModified.compareAndSet(oldLastModified, currentLastModified)) { try { + logger.debug("Reading updated LDAP configuration from \"{}\"...", ldapServers); - cachedConfigurations = mapper.readValue(ldapServers, new TypeReference>() {}); + Collection configs = mapper.readValue(ldapServers, new TypeReference>() {}); + + logger.debug("Reading LDAP configuration defaults from guacamole.properties..."); + LDAPConfiguration defaultConfig = new EnvironmentLDAPConfiguration(environment); + configs.forEach((config) -> config.setDefaults(defaultConfig)); + + cachedConfigurations = configs; + } catch (IOException e) { logger.error("\"{}\" could not be read/parsed: {}", ldapServers, e.getMessage()); diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index a697144c7..316205c87 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -190,7 +190,7 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { /** * The default configuration options for all parameters. */ - private static final LDAPConfiguration DEFAULT = new DefaultLDAPConfiguration(); + private LDAPConfiguration defaultConfig = new DefaultLDAPConfiguration(); /** * Returns the given value, if non-null. If null, the given default value @@ -253,6 +253,19 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { return value != null ? value : defaultValue; } + /** + * Sets the LDAPConfiguration that should be used for the default values of + * any configuration options omitted from the YAML. If not set, an instance + * of {@link DefaultLDAPConfiguration} will be used. + * + * @param defaultConfig + * The LDAPConfiguration to use for the default values of any omitted + * configuration options. + */ + public void setDefaults(LDAPConfiguration defaultConfig) { + this.defaultConfig = defaultConfig; + } + @Override public String appliesTo(String username) throws GuacamoleException { @@ -268,7 +281,7 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { @Override public String getServerHostname() throws GuacamoleException { - return withDefault(hostname, DEFAULT.getServerHostname()); + return withDefault(hostname, defaultConfig.getServerHostname()); } @Override @@ -278,93 +291,93 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { @Override public List getUsernameAttributes() throws GuacamoleException { - return withDefault(usernameAttributes, DEFAULT.getUsernameAttributes()); + return withDefault(usernameAttributes, defaultConfig.getUsernameAttributes()); } @Override public Dn getUserBaseDN() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_USER_BASE_DN.parseValue(userBaseDn), DEFAULT.getUserBaseDN()); + return withDefault(LDAPGuacamoleProperties.LDAP_USER_BASE_DN.parseValue(userBaseDn), defaultConfig.getUserBaseDN()); } @Override public Dn getConfigurationBaseDN() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN.parseValue(configBaseDn), DEFAULT.getConfigurationBaseDN()); + return withDefault(LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN.parseValue(configBaseDn), defaultConfig.getConfigurationBaseDN()); } @Override public List getGroupNameAttributes() throws GuacamoleException { - return withDefault(groupNameAttributes, DEFAULT.getGroupNameAttributes()); + return withDefault(groupNameAttributes, defaultConfig.getGroupNameAttributes()); } @Override public Dn getGroupBaseDN() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN.parseValue(groupBaseDn), DEFAULT.getGroupBaseDN()); + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN.parseValue(groupBaseDn), defaultConfig.getGroupBaseDN()); } @Override public String getSearchBindDN() throws GuacamoleException { - return withDefault(searchBindDn, DEFAULT.getSearchBindDN()); + return withDefault(searchBindDn, defaultConfig.getSearchBindDN()); } @Override public String getSearchBindPassword() throws GuacamoleException { - return withDefault(searchBindPassword, DEFAULT.getSearchBindDN()); + return withDefault(searchBindPassword, defaultConfig.getSearchBindDN()); } @Override public EncryptionMethod getEncryptionMethod() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD.parseValue(encryptionMethod), DEFAULT.getEncryptionMethod()); + return withDefault(LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD.parseValue(encryptionMethod), defaultConfig.getEncryptionMethod()); } @Override public int getMaxResults() throws GuacamoleException { - return withDefault(maxSearchResults, DEFAULT.getMaxResults()); + return withDefault(maxSearchResults, defaultConfig.getMaxResults()); } @Override public AliasDerefMode getDereferenceAliases() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES.parseValue(dereferenceAliases), DEFAULT.getDereferenceAliases()); + return withDefault(LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES.parseValue(dereferenceAliases), defaultConfig.getDereferenceAliases()); } @Override public boolean getFollowReferrals() throws GuacamoleException { - return withDefault(followReferrals, DEFAULT.getFollowReferrals()); + return withDefault(followReferrals, defaultConfig.getFollowReferrals()); } @Override public int getMaxReferralHops() throws GuacamoleException { - return withDefault(maxReferralHops, DEFAULT.getMaxReferralHops()); + return withDefault(maxReferralHops, defaultConfig.getMaxReferralHops()); } @Override public ExprNode getUserSearchFilter() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER.parseValue(userSearchFilter), DEFAULT.getUserSearchFilter()); + return withDefault(LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER.parseValue(userSearchFilter), defaultConfig.getUserSearchFilter()); } @Override public ExprNode getGroupSearchFilter() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER.parseValue(groupSearchFilter), DEFAULT.getGroupSearchFilter()); + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER.parseValue(groupSearchFilter), defaultConfig.getGroupSearchFilter()); } @Override public int getOperationTimeout() throws GuacamoleException { - return withDefault(operationTimeout, DEFAULT.getOperationTimeout()); + return withDefault(operationTimeout, defaultConfig.getOperationTimeout()); } @Override public List getAttributes() throws GuacamoleException { - return withDefault(userAttributes, DEFAULT.getAttributes()); + return withDefault(userAttributes, defaultConfig.getAttributes()); } @Override public String getMemberAttribute() throws GuacamoleException { - return withDefault(memberAttribute, DEFAULT.getMemberAttribute()); + return withDefault(memberAttribute, defaultConfig.getMemberAttribute()); } @Override public MemberAttributeType getMemberAttributeType() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE.parseValue(memberAttributeType), DEFAULT.getMemberAttributeType()); + return withDefault(LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE.parseValue(memberAttributeType), defaultConfig.getMemberAttributeType()); } } From 5a757d0418d671dce65c7ae911f1249bb9fdd7f1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 16:17:02 -0700 Subject: [PATCH 09/14] GUACAMOLE-957: Add support for configuring the general network timeout for LDAP. --- .../auth/ldap/ConnectedLDAPConfiguration.java | 5 +++++ .../guacamole/auth/ldap/LDAPConnectionService.java | 14 +++++++++++--- .../auth/ldap/conf/DefaultLDAPConfiguration.java | 5 +++++ .../ldap/conf/EnvironmentLDAPConfiguration.java | 8 ++++++++ .../auth/ldap/conf/JacksonLDAPConfiguration.java | 12 ++++++++++++ .../auth/ldap/conf/LDAPConfiguration.java | 13 +++++++++++++ .../auth/ldap/conf/LDAPGuacamoleProperties.java | 11 +++++++++++ 7 files changed, 65 insertions(+), 3 deletions(-) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java index 997e02376..5617bb785 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java @@ -197,6 +197,11 @@ public class ConnectedLDAPConfiguration implements LDAPConfiguration, AutoClosea return config.getOperationTimeout(); } + @Override + public int getNetworkTimeout() throws GuacamoleException { + return config.getNetworkTimeout(); + } + @Override public List getAttributes() throws GuacamoleException { return config.getAttributes(); 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 f9d2a0513..b8f5d30ad 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 @@ -67,6 +67,10 @@ public class LDAPConnectionService { * The encryption method that should be used to communicate with the * LDAP server. * + * @param timeout + * The maximum number of milliseconds to wait for a response from the + * LDAP server. + * * @return * A new instance of LdapNetworkConnection which uses the given * encryption method to communicate with the LDAP server at the given @@ -77,11 +81,13 @@ public class LDAPConnectionService { * bug). */ private LdapNetworkConnection createLDAPConnection(String host, int port, - EncryptionMethod encryptionMethod) throws GuacamoleException { + EncryptionMethod encryptionMethod, int timeout) + throws GuacamoleException { LdapConnectionConfig config = new LdapConnectionConfig(); config.setLdapHost(host); config.setLdapPort(port); + config.setTimeout(timeout); // Map encryption method to proper connection and socket factory switch (encryptionMethod) { @@ -140,7 +146,8 @@ public class LDAPConnectionService { return createLDAPConnection( config.getServerHostname(), config.getServerPort(), - config.getEncryptionMethod()); + config.getEncryptionMethod(), + config.getNetworkTimeout()); } /** @@ -209,7 +216,8 @@ public class LDAPConnectionService { if (port < 1) port = encryptionMethod.DEFAULT_PORT; - return createLDAPConnection(host, port, encryptionMethod); + return createLDAPConnection(host, port, encryptionMethod, + config.getNetworkTimeout()); } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java index c7b5e71c4..28ab8ed02 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java @@ -125,6 +125,11 @@ public class DefaultLDAPConfiguration implements LDAPConfiguration { return 30; } + @Override + public int getNetworkTimeout() { + return 30000; + } + @Override public List getAttributes() { return Collections.emptyList(); diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java index dfc60f98d..ae2d3cf3f 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java @@ -193,6 +193,14 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { ); } + @Override + public int getNetworkTimeout() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_NETWORK_TIMEOUT, + DEFAULT.getNetworkTimeout() + ); + } + @Override public List getAttributes() throws GuacamoleException { return environment.getProperty( diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index 316205c87..e3e1496ed 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -165,6 +165,13 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { @JsonProperty("operation-timeout") private Integer operationTimeout; + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_NETWORK_TIMEOUT}. + * If not set within the YAML, this will be null. + */ + @JsonProperty("network-timeout") + private Integer networkTimeout; + /** * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USER_ATTRIBUTES}. * If not set within the YAML, this will be null. @@ -364,6 +371,11 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { return withDefault(operationTimeout, defaultConfig.getOperationTimeout()); } + @Override + public int getNetworkTimeout() throws GuacamoleException { + return withDefault(networkTimeout, defaultConfig.getNetworkTimeout()); + } + @Override public List getAttributes() throws GuacamoleException { return withDefault(userAttributes, defaultConfig.getAttributes()); diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java index 99f36fa46..77eb31511 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java @@ -255,6 +255,19 @@ public interface LDAPConfiguration { */ ExprNode getGroupSearchFilter() throws GuacamoleException; + /** + * Returns the maximum number of milliseconds to wait for a response when + * communicating with the LDAP server. + * + * @return + * The maximum number of milliseconds to wait for responses from the + * LDAP server. + * + * @throws GuacamoleException + * If the LDAP network timeout cannot be retrieved. + */ + int getNetworkTimeout() throws GuacamoleException; + /** * Returns the maximum number of seconds to wait for LDAP operations. * diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java index 5bf5cfbd6..1db4f723c 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java @@ -254,6 +254,17 @@ public class LDAPGuacamoleProperties { }; + /** + * Number of milliseconds to wait for responses from the LDAP server. + */ + public static final IntegerGuacamoleProperty LDAP_NETWORK_TIMEOUT = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "ldap-network-timeout"; } + + }; + /** * Custom attribute or attributes to query from Guacamole user's record in * the LDAP directory. From a216ec902a311aa5b9853129efad2e90034823a1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 16:34:39 -0700 Subject: [PATCH 10/14] GUACAMOLE-957: Match any user for LDAP YAML by default. --- .../guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index e3e1496ed..1d00c6b40 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -276,6 +276,10 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { @Override public String appliesTo(String username) throws GuacamoleException { + // Match any user by default + if (matchUsernames == null || matchUsernames.isEmpty()) + return username; + for (Pattern pattern : matchUsernames) { Matcher matcher = pattern.matcher(username); if (matcher.matches()) From d80ab47c973433a2e89af22408da748e1ac53d90 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 17:15:22 -0700 Subject: [PATCH 11/14] GUACAMOLE-957: Use case insensitive matching for LDAP YAML regular expressions by default. --- .../CaseInsensitivePatternDeserializer.java | 81 +++++++++++++++++++ .../auth/ldap/conf/ConfigurationService.java | 5 +- 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/CaseInsensitivePatternDeserializer.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/CaseInsensitivePatternDeserializer.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/CaseInsensitivePatternDeserializer.java new file mode 100644 index 000000000..0525b1dbe --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/CaseInsensitivePatternDeserializer.java @@ -0,0 +1,81 @@ +/* + * 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.conf; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; +import java.io.IOException; +import java.io.Serializable; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Custom JSON (or YAML) deserializer for Jackson that deserializes string + * values as Patterns with the case insensitive flag set by default. Jackson + * will actually handle deserialization of Patterns automatically, but does not + * provide for setting the default flags. + */ +public class CaseInsensitivePatternDeserializer extends StdScalarDeserializer { + + /** + * Unique version identifier of this {@link Serializable} class. + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new CaseInsensitivePatternDeserializer which deserializes + * string values to Pattern objects with the case insensitive flag set. + */ + public CaseInsensitivePatternDeserializer() { + super(Pattern.class); + } + + @Override + public LogicalType logicalType() { + return LogicalType.Textual; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public Pattern deserialize(JsonParser parser, DeserializationContext context) + throws IOException, JsonProcessingException { + + if (!parser.hasToken(JsonToken.VALUE_STRING)) + throw new JsonParseException(parser, "Regular expressions may only be represented as strings."); + + try { + return Pattern.compile(parser.getText(), Pattern.CASE_INSENSITIVE); + } + catch (PatternSyntaxException e) { + throw new JsonParseException(parser, "Invalid regular expression.", e); + } + + } + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index fab1a7027..f1e560ec1 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -21,6 +21,7 @@ package org.apache.guacamole.auth.ldap.conf; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -29,6 +30,7 @@ import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; @@ -49,7 +51,8 @@ public class ConfigurationService { /** * ObjectMapper for deserializing YAML. */ - private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()) + .registerModule(new SimpleModule().addDeserializer(Pattern.class, new CaseInsensitivePatternDeserializer())); /** * The name of the file within GUACAMOLE_HOME that defines each available From 0f96d5e122b2055fbd38ab3557db3da067aa6049 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 21 Oct 2021 17:16:23 -0700 Subject: [PATCH 12/14] GUACAMOLE-957: Continue to use cached version of LDAP YAML if an error occurs. --- .../apache/guacamole/auth/ldap/conf/ConfigurationService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java index f1e560ec1..3b08d3332 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java @@ -32,7 +32,6 @@ import java.util.Collections; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -115,7 +114,6 @@ public class ConfigurationService { } catch (IOException e) { logger.error("\"{}\" could not be read/parsed: {}", ldapServers, e.getMessage()); - throw new GuacamoleServerException("Cannot read LDAP configuration from \"" + ldapServers + "\"", e); } } else From b45fc9b6e5cc690882ede57b5ad89500bfcc1c04 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 23 Oct 2021 15:12:06 -0700 Subject: [PATCH 13/14] GUACAMOLE-957: Evaluate default value of LDAP configuration only if provided value is null. The function supplying the default value may throw a GuacamoleException, thus the function providing that default should only be invoked when actually necessary. --- .../ldap/conf/JacksonLDAPConfiguration.java | 137 +++++++++++------- 1 file changed, 83 insertions(+), 54 deletions(-) diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index 1d00c6b40..4440739a7 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -29,6 +29,7 @@ import org.apache.directory.api.ldap.model.filter.ExprNode; import org.apache.directory.api.ldap.model.message.AliasDerefMode; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.properties.GuacamoleProperty; /** * LDAPConfiguration implementation that is annotated for deserialization by @@ -199,6 +200,31 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { */ private LDAPConfiguration defaultConfig = new DefaultLDAPConfiguration(); + /** + * Supplier of default values for LDAP configurations. Unlike + * {@link java.util.function.Supplier}, the {@link #get()} function of + * DefaultSupplier may throw a {@link GuacamoleException}. + * + * @param + * The type of value returned by this DefaultSupplier. + */ + @FunctionalInterface + private interface DefaultSupplier { + + /** + * Returns the value supplied by this DefaultSupplier. The value + * returned is not cached and may be non-deterministic. + * + * @return + * The value supplied by this DefaultSupplier. + * + * @throws GuacamoleException + * If an error occurs while producing/retrieving the value. + */ + T get() throws GuacamoleException; + + } + /** * Returns the given value, if non-null. If null, the given default value * is returned. @@ -210,54 +236,50 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { * The possibly null value to return if non-null. * * @param defaultValue - * The value to return if the provided value is null. + * A function which supplies the value to return if the provided value + * is null. * * @return * The provided value, if non-null, otherwise the provided default * value. + * + * @throws GuacamoleException + * If an error occurs while producing/retrieving the default value. */ - private T withDefault(T value, T defaultValue) { - return value != null ? value : defaultValue; + private T withDefault(T value, DefaultSupplier defaultValue) + throws GuacamoleException { + return value != null ? value : defaultValue.get(); } /** - * Returns the given Integer value as an int, if non-null. If null, the - * given int default value is returned. This function is an Integer-specific - * variant of {@link #withDefault(java.lang.Object, java.lang.Object)} - * which avoids unnecessary boxing/unboxing. + * Parses and returns the given value, if non-null. If null, the given + * default value is returned. + * + * @param + * The type of value accepted and returned. + * + * @param property + * The GuacamoleProperty implementation to use to parse the provided + * String value. * * @param value * The possibly null value to return if non-null. * * @param defaultValue - * The value to return if the provided value is null. - * + * A function which supplies the value to return if the provided value + * is null. + * * @return * The provided value, if non-null, otherwise the provided default * value. + * + * @throws GuacamoleException + * If an error occurs while producing/retrieving the default value. */ - private int withDefault(Integer value, int defaultValue) { - return value != null ? value : defaultValue; - } - - /** - * Returns the given Boolean value as an boolean, if non-null. If null, the - * given boolean default value is returned. This function is a Boolean- - * specific variant of {@link #withDefault(java.lang.Object, java.lang.Object)} - * which avoids unnecessary boxing/unboxing. - * - * @param value - * The possibly null value to return if non-null. - * - * @param defaultValue - * The value to return if the provided value is null. - * - * @return - * The provided value, if non-null, otherwise the provided default - * value. - */ - private boolean withDefault(Boolean value, boolean defaultValue) { - return value != null ? value : defaultValue; + private T withDefault(GuacamoleProperty property, String value, + DefaultSupplier defaultValue) + throws GuacamoleException { + return withDefault(property.parseValue(value), defaultValue); } /** @@ -292,108 +314,115 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { @Override public String getServerHostname() throws GuacamoleException { - return withDefault(hostname, defaultConfig.getServerHostname()); + return withDefault(hostname, defaultConfig::getServerHostname); } @Override public int getServerPort() throws GuacamoleException { - return withDefault(port, getEncryptionMethod().DEFAULT_PORT); + return withDefault(port, () -> getEncryptionMethod().DEFAULT_PORT); } @Override public List getUsernameAttributes() throws GuacamoleException { - return withDefault(usernameAttributes, defaultConfig.getUsernameAttributes()); + return withDefault(usernameAttributes, defaultConfig::getUsernameAttributes); } @Override public Dn getUserBaseDN() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_USER_BASE_DN.parseValue(userBaseDn), defaultConfig.getUserBaseDN()); + return withDefault(LDAPGuacamoleProperties.LDAP_USER_BASE_DN, + userBaseDn, defaultConfig::getUserBaseDN); } @Override public Dn getConfigurationBaseDN() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN.parseValue(configBaseDn), defaultConfig.getConfigurationBaseDN()); + return withDefault(LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN, + configBaseDn, defaultConfig::getConfigurationBaseDN); } @Override public List getGroupNameAttributes() throws GuacamoleException { - return withDefault(groupNameAttributes, defaultConfig.getGroupNameAttributes()); + return withDefault(groupNameAttributes, defaultConfig::getGroupNameAttributes); } @Override public Dn getGroupBaseDN() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN.parseValue(groupBaseDn), defaultConfig.getGroupBaseDN()); + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN, + groupBaseDn, defaultConfig::getGroupBaseDN); } @Override public String getSearchBindDN() throws GuacamoleException { - return withDefault(searchBindDn, defaultConfig.getSearchBindDN()); + return withDefault(searchBindDn, defaultConfig::getSearchBindDN); } @Override public String getSearchBindPassword() throws GuacamoleException { - return withDefault(searchBindPassword, defaultConfig.getSearchBindDN()); + return withDefault(searchBindPassword, defaultConfig::getSearchBindDN); } @Override public EncryptionMethod getEncryptionMethod() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD.parseValue(encryptionMethod), defaultConfig.getEncryptionMethod()); + return withDefault(LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, + encryptionMethod, defaultConfig::getEncryptionMethod); } @Override public int getMaxResults() throws GuacamoleException { - return withDefault(maxSearchResults, defaultConfig.getMaxResults()); + return withDefault(maxSearchResults, defaultConfig::getMaxResults); } @Override public AliasDerefMode getDereferenceAliases() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES.parseValue(dereferenceAliases), defaultConfig.getDereferenceAliases()); + return withDefault(LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES, + dereferenceAliases, defaultConfig::getDereferenceAliases); } @Override public boolean getFollowReferrals() throws GuacamoleException { - return withDefault(followReferrals, defaultConfig.getFollowReferrals()); + return withDefault(followReferrals, defaultConfig::getFollowReferrals); } @Override public int getMaxReferralHops() throws GuacamoleException { - return withDefault(maxReferralHops, defaultConfig.getMaxReferralHops()); + return withDefault(maxReferralHops, defaultConfig::getMaxReferralHops); } @Override public ExprNode getUserSearchFilter() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER.parseValue(userSearchFilter), defaultConfig.getUserSearchFilter()); + return withDefault(LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER, + userSearchFilter, defaultConfig::getUserSearchFilter); } @Override public ExprNode getGroupSearchFilter() throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER.parseValue(groupSearchFilter), defaultConfig.getGroupSearchFilter()); + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER, + groupSearchFilter, defaultConfig::getGroupSearchFilter); } @Override public int getOperationTimeout() throws GuacamoleException { - return withDefault(operationTimeout, defaultConfig.getOperationTimeout()); + return withDefault(operationTimeout, defaultConfig::getOperationTimeout); } @Override public int getNetworkTimeout() throws GuacamoleException { - return withDefault(networkTimeout, defaultConfig.getNetworkTimeout()); + return withDefault(networkTimeout, defaultConfig::getNetworkTimeout); } @Override public List getAttributes() throws GuacamoleException { - return withDefault(userAttributes, defaultConfig.getAttributes()); + return withDefault(userAttributes, defaultConfig::getAttributes); } @Override public String getMemberAttribute() throws GuacamoleException { - return withDefault(memberAttribute, defaultConfig.getMemberAttribute()); + return withDefault(memberAttribute, defaultConfig::getMemberAttribute); } @Override - public MemberAttributeType getMemberAttributeType() - throws GuacamoleException { - return withDefault(LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE.parseValue(memberAttributeType), defaultConfig.getMemberAttributeType()); + public MemberAttributeType getMemberAttributeType() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE, + memberAttributeType, defaultConfig::getMemberAttributeType); } } From 00f83145a36f7ae0d446f4c8824463d6dd557f3d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 23 Oct 2021 15:53:56 -0700 Subject: [PATCH 14/14] GUACAMOLE-957: Leverage capturing group in user match regex to determine Guacamole LDAP user identities. --- .../ldap/AuthenticationProviderService.java | 24 +++--- .../auth/ldap/user/LDAPAuthenticatedUser.java | 4 +- .../auth/ldap/user/UserLDAPConfiguration.java | 79 +++++++++++++++++++ 3 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserLDAPConfiguration.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java index 486c524bd..654b403c3 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java @@ -19,6 +19,7 @@ package org.apache.guacamole.auth.ldap; +import org.apache.guacamole.auth.ldap.user.UserLDAPConfiguration; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Collection; @@ -169,30 +170,31 @@ public class AuthenticationProviderService { } /** - * Returns a new ConnectedLDAPConfiguration that is connected to an LDAP - * server associated with the user having the given username and bound + * Returns a new UserLDAPConfiguration that is connected to an LDAP server + * associated with the Guacamole user having the given username and bound * using the provided password. All LDAP servers associated with the given * user are tried until the connection and authentication attempt succeeds. * If no LDAP servers are available, or no LDAP servers are associated with - * the given user, null is returned. + * the given user, null is returned. The Guacamole username will be + * internally translated to a fully-qualified LDAP DN according to the + * configuration of the LDAP server that is ultimately chosen. * * @param username - * The username or DN of the user to bind as. + * The username of the Guacamole user to bind as. * * @param password * The password of the user to bind as. * * @return - * A new ConnectedLDAPConfiguration which is bound to an LDAP server - * using the provided credentials, or null if no LDAP servers are - * available for the given user or connecting/authenticating has - * failed. + * A new UserLDAPConfiguration which is bound to an LDAP server using + * the provided credentials, or null if no LDAP servers are available + * for the given user or connecting/authenticating has failed. * * @throws GuacamoleException * If configuration information for the user's LDAP server(s) cannot * be retrieved. */ - private ConnectedLDAPConfiguration getLDAPConfiguration(String username, + private UserLDAPConfiguration getLDAPConfiguration(String username, String password) throws GuacamoleException { // Get all LDAP server configurations @@ -236,7 +238,7 @@ public class AuthenticationProviderService { // Connection and bind were successful logger.info("User \"{}\" was successfully authenticated by LDAP server \"{}\".", username, config.getServerHostname()); - return new ConnectedLDAPConfiguration(config, bindDn, ldapConnection); + return new UserLDAPConfiguration(config, translatedUsername, bindDn, ldapConnection); } @@ -278,7 +280,7 @@ public class AuthenticationProviderService { + " authentication provider.", CredentialsInfo.USERNAME_PASSWORD); } - ConnectedLDAPConfiguration config = getLDAPConfiguration(username, password); + UserLDAPConfiguration config = getLDAPConfiguration(username, password); if (config == null) throw new GuacamoleInvalidCredentialsException("Invalid login.", CredentialsInfo.USERNAME_PASSWORD); diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java index 6889e61a7..2abe4aef0 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java @@ -89,14 +89,14 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { * The unique identifiers of all user groups which affect the * permissions available to this user. */ - public void init(ConnectedLDAPConfiguration config, Credentials credentials, + public void init(UserLDAPConfiguration config, Credentials credentials, Map tokens, Set effectiveGroups) { this.config = config; this.credentials = credentials; this.tokens = Collections.unmodifiableMap(tokens); this.effectiveGroups = effectiveGroups; this.bindDn = config.getBindDN(); - setIdentifier(credentials.getUsername()); + setIdentifier(config.getGuacamoleUsername()); } /** diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserLDAPConfiguration.java new file mode 100644 index 000000000..3d2cd8221 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserLDAPConfiguration.java @@ -0,0 +1,79 @@ +/* + * 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.user; + +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.directory.ldap.client.api.LdapNetworkConnection; +import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration; +import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; + +/** + * LDAPConfiguration implementation that represents the configuration and + * network connection of an LDAP that has been bound on behalf of a Guacamole + * user. + */ +public class UserLDAPConfiguration extends ConnectedLDAPConfiguration { + + /** + * The username of the associated Guacamole user. + */ + private final String username; + + /** + * Creates a new UserLDAPConfiguration that associates the given + * LDAPConfiguration of an LDAP server with the active network connection to + * that server, as well as the username of the Guacamole user on behalf of + * whom that connection was established. All functions inherited from the + * LDAPConfiguration interface are delegated to the given LDAPConfiguration. + * It is the responsibility of the caller to ensure the provided + * LdapNetworkConnection is closed after it is no longer needed. + * + * @param config + * The LDAPConfiguration to wrap. + * + * @param username + * The username of the associated Guacamole user. + * + * @param bindDn + * The LDAP DN that was used to bind with the LDAP server to produce + * the given LdapNetworkConnection. + * + * @param connection + * The connection to the LDAP server represented by the given + * configuration. + */ + public UserLDAPConfiguration(LDAPConfiguration config, + String username, Dn bindDn, LdapNetworkConnection connection) { + super(config, bindDn, connection); + this.username = username; + } + + /** + * Returns the username of the Guacamole user on behalf of whom the + * associated LDAP network connection was established. + * + * @return + * The username of the associated Guacamole user. + */ + public String getGuacamoleUsername() { + return username; + } + +}