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 833674c1b..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; @@ -35,6 +36,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 +107,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 +120,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 +142,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,10 +165,89 @@ public class AuthenticationProviderService { } // Otherwise, derive user DN from base DN - return userService.deriveUserDN(username); + return userService.deriveUserDN(config, username); } + /** + * 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 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 of the Guacamole user to bind as. + * + * @param password + * The password of the user to bind as. + * + * @return + * 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 UserLDAPConfiguration getLDAPConfiguration(String username, + String password) throws GuacamoleException { + + // Get all LDAP server configurations + Collection configs = confService.getLDAPConfigurations(); + if (configs.isEmpty()) { + logger.info("Skipping LDAP authentication as no LDAP servers are configured."); + return null; + } + + // Try each possible LDAP configuration until the TCP connection and + // 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, translatedUsername); + 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 UserLDAPConfiguration(config, translatedUsername, 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 @@ -196,39 +279,30 @@ public class AuthenticationProviderService { "Anonymous bind is not currently allowed by the LDAP" + " authentication provider.", CredentialsInfo.USERNAME_PASSWORD); } - - Dn bindDn = getUserBindDN(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(bindDn.getName(), password); - if (ldapConnection == null) + + UserLDAPConfiguration 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(ldapConnection, - bindDn); + userGroupService.getParentUserGroupIdentifiers(config, config.getBindDN()); // Return AuthenticatedUser if bind succeeds LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(credentials, getAttributeTokens(ldapConnection, - bindDn), effectiveGroups, bindDn); + authenticatedUser.init(config, credentials, + getAttributeTokens(config), effectiveGroups); return authenticatedUser; } - // Always disconnect - finally { - ldapConnection.close(); + catch (GuacamoleException | RuntimeException | Error e) { + config.close(); + throw e; } } @@ -240,11 +314,8 @@ 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 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. + * @param config + * The configuration of the LDAP server being queried. * * @return * A map of parameter tokens generated from attributes on the user @@ -255,11 +326,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(ConnectedLDAPConfiguration config) + 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()) @@ -272,7 +343,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(); @@ -312,35 +383,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) { - Dn bindDn = ((LDAPAuthenticatedUser) authenticatedUser).getBindDn(); - LdapNetworkConnection ldapConnection = - ldapService.bindAs(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); - } + LDAPAuthenticatedUser ldapAuthenticatedUser = (LDAPAuthenticatedUser) authenticatedUser; + ConnectedLDAPConfiguration config = ldapAuthenticatedUser.getLDAPConfiguration(); try { // Build user context by querying LDAP LDAPUserContext userContext = userContextProvider.get(); - userContext.init(authenticatedUser, 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..5617bb785 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java @@ -0,0 +1,220 @@ +/* + * 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 appliesTo(String username) throws GuacamoleException { + return config.appliesTo(username); + } + + @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 int getNetworkTimeout() throws GuacamoleException { + return config.getNetworkTimeout(); + } + + @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/LDAPConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java index d93ae23a8..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 @@ -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 @@ -74,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 @@ -84,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) { @@ -130,6 +129,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 +141,13 @@ 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(), + config.getNetworkTimeout()); } /** @@ -156,6 +159,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 +176,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 +203,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); @@ -210,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()); } @@ -329,6 +336,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 +354,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 +383,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 +408,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/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 2071dfa02..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 @@ -19,21 +19,57 @@ 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; +import java.io.File; +import java.io.IOException; +import java.util.Collection; 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 java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Service for retrieving configuration information regarding the LDAP server. + * Service for retrieving configuration information regarding LDAP servers. */ +@Singleton 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()) + .registerModule(new SimpleModule().addDeserializer(Pattern.class, new CaseInsensitivePatternDeserializer())); + + /** + * 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 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. */ @@ -41,375 +77,56 @@ public class ConfigurationService { private Environment environment; /** - * Returns the hostname of the LDAP server as configured with - * guacamole.properties. By default, this will be "localhost". + * 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 hostname of the LDAP server, as configured with - * guacamole.properties. + * The configurations of all LDAP servers. * * @throws GuacamoleException - * If guacamole.properties cannot be parsed. + * If the configuration information of the LDAP servers cannot be + * retrieved due to an error. */ - public String getServerHostname() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_HOSTNAME, - "localhost" - ); - } + public Collection getLDAPConfigurations() throws GuacamoleException { - /** - * 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 - ); - } + // Read configuration from YAML, if available + File ldapServers = new File(environment.getGuacamoleHome(), LDAP_SERVERS_YML); + if (ldapServers.exists()) { - /** - * 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") - ); - } + long oldLastModified = lastModified.get(); + long currentLastModified = ldapServers.lastModified(); - /** - * 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 - ); - } + // 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 { - /** - * 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 - ); - } + logger.debug("Reading updated LDAP configuration from \"{}\"...", ldapServers); + Collection configs = mapper.readValue(ldapServers, new TypeReference>() {}); - /** - * 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") - ); - } + logger.debug("Reading LDAP configuration defaults from guacamole.properties..."); + LDAPConfiguration defaultConfig = new EnvironmentLDAPConfiguration(environment); + configs.forEach((config) -> config.setDefaults(defaultConfig)); - /** - * 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 - ); - } + cachedConfigurations = configs; - /** - * 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 guacamole.properties cannot be parsed. - */ - public String getSearchBindDN() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN - ); - } + } + catch (IOException e) { + logger.error("\"{}\" could not be read/parsed: {}", ldapServers, e.getMessage()); + } + } + else + logger.debug("Using cached LDAP configuration from \"{}\".", ldapServers); - /** - * 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 guacamole.properties cannot be parsed. - */ - public String getSearchBindPassword() throws GuacamoleException { - return environment.getProperty( - LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD - ); - } + return cachedConfigurations; - /** - * 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 - ); - } + // Use guacamole.properties if not using YAML + logger.debug("Reading LDAP configuration from guacamole.properties..."); + return Collections.singletonList(new EnvironmentLDAPConfiguration(environment)); - /** - * 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 - ); } } 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..28ab8ed02 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java @@ -0,0 +1,149 @@ +/* + * 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 int getNetworkTimeout() { + return 30000; + } + + @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 new file mode 100644 index 000000000..ae2d3cf3f --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java @@ -0,0 +1,229 @@ +/* + * 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; +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; + + /** + * 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. + * + * @param environment + * The Guacamole server environment. + */ + public EnvironmentLDAPConfiguration(Environment environment) { + this.environment = environment; + } + + @Override + public String appliesTo(String username) throws GuacamoleException { + return username; + } + + @Override + public String getServerHostname() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_HOSTNAME, + DEFAULT.getServerHostname() + ); + } + + @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, + DEFAULT.getUsernameAttributes() + ); + } + + @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, + DEFAULT.getConfigurationBaseDN() + ); + } + + @Override + public List getGroupNameAttributes() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_GROUP_NAME_ATTRIBUTE, + DEFAULT.getGroupNameAttributes() + ); + } + + @Override + public Dn getGroupBaseDN() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN, + DEFAULT.getGroupBaseDN() + ); + } + + @Override + public String getSearchBindDN() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN, + DEFAULT.getSearchBindDN() + ); + } + + @Override + public String getSearchBindPassword() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD, + DEFAULT.getSearchBindPassword() + ); + } + + @Override + public EncryptionMethod getEncryptionMethod() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, + DEFAULT.getEncryptionMethod() + ); + } + + @Override + public int getMaxResults() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS, + DEFAULT.getMaxResults() + ); + } + + @Override + public AliasDerefMode getDereferenceAliases() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES, + DEFAULT.getDereferenceAliases() + ); + } + + @Override + public boolean getFollowReferrals() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS, + DEFAULT.getFollowReferrals() + ); + } + + @Override + public int getMaxReferralHops() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS, + DEFAULT.getMaxReferralHops() + ); + } + + @Override + public ExprNode getUserSearchFilter() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER, + DEFAULT.getUserSearchFilter() + ); + } + + @Override + public ExprNode getGroupSearchFilter() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER, + DEFAULT.getGroupSearchFilter() + ); + } + + @Override + public int getOperationTimeout() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_OPERATION_TIMEOUT, + DEFAULT.getOperationTimeout() + ); + } + + @Override + public int getNetworkTimeout() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_NETWORK_TIMEOUT, + DEFAULT.getNetworkTimeout() + ); + } + + @Override + public List getAttributes() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_USER_ATTRIBUTES, + DEFAULT.getAttributes() + ); + } + + @Override + public String getMemberAttribute() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE, + DEFAULT.getMemberAttribute() + ); + } + + @Override + public MemberAttributeType getMemberAttributeType() + throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE, + 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 new file mode 100644 index 000000000..4440739a7 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -0,0 +1,428 @@ +/* + * 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.JsonFormat; +import static com.fasterxml.jackson.annotation.JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY; +import com.fasterxml.jackson.annotation.JsonProperty; +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.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 + * Jackson. + */ +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") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) + private List matchUsernames; + + /** + * 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") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) + 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") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) + 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_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. + */ + @JsonProperty("user-attributes") + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) + 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; + + /** + * The default configuration options for all parameters. + */ + 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. + * + * @param + * The type of value accepted and returned. + * + * @param value + * The possibly null value to return if non-null. + * + * @param defaultValue + * 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, DefaultSupplier defaultValue) + throws GuacamoleException { + return value != null ? value : defaultValue.get(); + } + + /** + * 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 + * 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(GuacamoleProperty property, String value, + DefaultSupplier defaultValue) + throws GuacamoleException { + return withDefault(property.parseValue(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 { + + // Match any user by default + if (matchUsernames == null || matchUsernames.isEmpty()) + return username; + + 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() throws GuacamoleException { + return withDefault(hostname, defaultConfig::getServerHostname); + } + + @Override + public int getServerPort() throws GuacamoleException { + return withDefault(port, () -> getEncryptionMethod().DEFAULT_PORT); + } + + @Override + public List getUsernameAttributes() throws GuacamoleException { + return withDefault(usernameAttributes, defaultConfig::getUsernameAttributes); + } + + @Override + public Dn getUserBaseDN() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_USER_BASE_DN, + userBaseDn, defaultConfig::getUserBaseDN); + } + + @Override + public Dn getConfigurationBaseDN() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN, + configBaseDn, defaultConfig::getConfigurationBaseDN); + } + + @Override + public List getGroupNameAttributes() throws GuacamoleException { + return withDefault(groupNameAttributes, defaultConfig::getGroupNameAttributes); + } + + @Override + public Dn getGroupBaseDN() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN, + groupBaseDn, defaultConfig::getGroupBaseDN); + } + + @Override + public String getSearchBindDN() throws GuacamoleException { + return withDefault(searchBindDn, defaultConfig::getSearchBindDN); + } + + @Override + public String getSearchBindPassword() throws GuacamoleException { + return withDefault(searchBindPassword, defaultConfig::getSearchBindDN); + } + + @Override + public EncryptionMethod getEncryptionMethod() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, + encryptionMethod, defaultConfig::getEncryptionMethod); + } + + @Override + public int getMaxResults() throws GuacamoleException { + return withDefault(maxSearchResults, defaultConfig::getMaxResults); + } + + @Override + public AliasDerefMode getDereferenceAliases() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES, + dereferenceAliases, defaultConfig::getDereferenceAliases); + } + + @Override + public boolean getFollowReferrals() throws GuacamoleException { + return withDefault(followReferrals, defaultConfig::getFollowReferrals); + } + + @Override + public int getMaxReferralHops() throws GuacamoleException { + return withDefault(maxReferralHops, defaultConfig::getMaxReferralHops); + } + + @Override + public ExprNode getUserSearchFilter() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER, + userSearchFilter, defaultConfig::getUserSearchFilter); + } + + @Override + public ExprNode getGroupSearchFilter() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER, + groupSearchFilter, defaultConfig::getGroupSearchFilter); + } + + @Override + public int getOperationTimeout() throws GuacamoleException { + 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); + } + + @Override + public String getMemberAttribute() throws GuacamoleException { + return withDefault(memberAttribute, defaultConfig::getMemberAttribute); + } + + @Override + public MemberAttributeType getMemberAttributeType() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE, + memberAttributeType, defaultConfig::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 new file mode 100644 index 000000000..77eb31511 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java @@ -0,0 +1,323 @@ +/* + * 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 { + + /** + * 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. + * + * @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. + * + * @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. + * + * @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. + * + * @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. + * + * @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. + * + * @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. + * + * @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. + * + * @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. + * + * @return + * The search filter that should be used when querying the + * LDAP server for users that are valid in Guacamole. + * + * @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. + * + * @return + * The search filter that should be used when querying the + * LDAP server for groups that are valid in Guacamole. + * + * @throws GuacamoleException + * If the group search filter cannot be retrieved. + */ + 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. + * + * @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 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 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. + * + * @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/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. 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..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,16 +35,13 @@ 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.auth.ldap.conf.ConfigurationService; 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.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 +60,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. */ @@ -124,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 @@ -143,34 +129,27 @@ public class ConnectionService { * @throws GuacamoleException * If an error occurs preventing retrieval of connections. */ - public Map getConnections(AuthenticatedUser user, - LdapNetworkConnection ldapConnection) throws GuacamoleException { + public Map getConnections(LDAPAuthenticatedUser user) + throws GuacamoleException { + + ConnectedLDAPConfiguration 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(); 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(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(ldapConnection, + List results = queryService.search(ldapConfig, ldapConfig.getLDAPConnection(), configurationBaseDN, connectionSearchFilter, 0, GUAC_CONFIG_LDAP_ATTRIBUTES); // Return a map of all readable connections @@ -261,8 +240,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; @@ -277,14 +255,11 @@ 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 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. + * @param user + * The AuthenticatedUser object associated with the user who is + * currently authenticated with Guacamole. * * @return * An LDAP search filter which queries all guacConfigGroup objects @@ -296,10 +271,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) throws LdapException, GuacamoleException { + ConnectedLDAPConfiguration config = user.getLDAPConfiguration(); + Dn userDN = config.getBindDN(); + 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 +284,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, 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..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,11 +33,12 @@ 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.ConfigurationService; 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; 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")) @@ -100,12 +98,11 @@ 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 ldapConnection - * The current connection to the LDAP server, associated with the - * current user. + * @param user + * The AuthenticatedUser object associated with the user who is + * currently authenticated with Guacamole. * * @return * All user groups accessible to the user currently bound under the @@ -115,25 +112,28 @@ public class UserGroupService { * @throws GuacamoleException * If an error occurs preventing retrieval of user groups. */ - public Map getUserGroups(LdapNetworkConnection ldapConnection) + public Map getUserGroups(LDAPAuthenticatedUser user) throws GuacamoleException { + ConnectedLDAPConfiguration 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( - ldapConnection, + config, + config.getLDAPConnection(), groupBaseDN, - getGroupSearchFilter(), + getGroupSearchFilter(config), attributes, null, groupAttributes @@ -167,9 +167,8 @@ public class UserGroupService { * user is a member of. Only user groups which are readable by the current * user will be retrieved. * - * @param ldapConnection - * The current connection to the LDAP server, associated with the - * current user. + * @param config + * The configuration of the LDAP server being queried. * * @param userDN * The DN of the user whose group membership should be retrieved. @@ -181,24 +180,25 @@ 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(ConnectedLDAPConfiguration config, 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( - ldapConnection, + config, + config.getLDAPConnection(), userDN, - confService.getUserSearchFilter(), + config.getUserSearchFilter(), 0, userAttributes); // ... there can surely only be one @@ -222,16 +222,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( - ldapConnection, + config, + config.getLDAPConnection(), groupBaseDN, - getGroupSearchFilter(), + getGroupSearchFilter(config), Collections.singleton(memberAttribute), userIDorDN, groupAttributes @@ -244,9 +245,8 @@ public class UserGroupService { * member of. Only identifiers of user groups which are readable by the * current user will be retrieved. * - * @param ldapConnection - * The current connection to the LDAP server, associated with the - * current user. + * @param config + * The configuration of the LDAP server being queried. * * @param userDN * The DN of the user whose group membership should be retrieved. @@ -258,11 +258,11 @@ 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(ConnectedLDAPConfiguration config, Dn userDN) + throws GuacamoleException { - Collection attributes = confService.getGroupNameAttributes(); - List userGroups = getParentUserGroupEntries(ldapConnection, userDN); + Collection attributes = config.getGroupNameAttributes(); + 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 44296432f..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 @@ -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.ConnectedLDAPConfiguration; import org.apache.guacamole.net.auth.AbstractAuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; @@ -63,9 +64,19 @@ 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 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 + * queries related to this AuthenticatedUser. * * @param credentials * The credentials provided when this user was authenticated. @@ -77,17 +88,15 @@ 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(Credentials credentials, Map tokens, - Set effectiveGroups, Dn bindDn) { + 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 = bindDn; - setIdentifier(credentials.getUsername()); + this.bindDn = config.getBindDN(); + setIdentifier(config.getGuacamoleUsername()); } /** @@ -103,7 +112,7 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser { public Map getTokens() { return tokens; } - + /** * Returns the LDAP DN used to bind this user. * @@ -114,6 +123,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 ConnectedLDAPConfiguration getLDAPConfiguration() { + return config; + } + @Override public AuthenticationProvider getAuthenticationProvider() { return authProvider; @@ -129,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 b5c789e1e..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,13 +21,11 @@ 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; 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; @@ -101,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 may have been authenticated by a different authentication - * provider (not LDAP). - * - * @param ldapConnection - * The connection to the LDAP server to use when querying accessible - * Guacamole users and connections. + * user will always have been authenticated via LDAP, as LDAP data is + * not provided to non-LDAP users. * * @throws GuacamoleException * If associated data stored within the LDAP directory cannot be * queried due to an error. */ - public void init(AuthenticatedUser user, LdapNetworkConnection ldapConnection) - throws GuacamoleException { + public void init(LDAPAuthenticatedUser user) throws GuacamoleException { // Query all accessible users userDirectory = new SimpleDirectory<>( - userService.getUsers(ldapConnection) + userService.getUsers(user) ); // Query all accessible user groups userGroupDirectory = new SimpleDirectory<>( - userGroupService.getUserGroups(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/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; + } + +} 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..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 @@ -32,11 +32,12 @@ 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.ConnectedLDAPConfiguration; 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 +54,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. */ @@ -66,12 +61,11 @@ 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 ldapConnection - * The current connection to the LDAP server, associated with the - * current user. + * @param user + * The AuthenticatedUser object associated with the user who is + * currently authenticated with Guacamole. * * @return * All users accessible to the user currently bound under the given @@ -81,19 +75,24 @@ public class UserService { * @throws GuacamoleException * If an error occurs preventing retrieval of users. */ - public Map getUsers(LdapNetworkConnection ldapConnection) + public Map getUsers(LDAPAuthenticatedUser user) throws GuacamoleException { + ConnectedLDAPConfiguration 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(), - usernameAttrs, - null, - attributes); + attributes.addAll(config.getAttributes()); + 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 -> { @@ -124,6 +123,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 +141,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 +166,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 +179,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 +198,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); 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