mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 21:27:40 +00:00
Merge pull request #284 from glyptodon/complex-ldap
GUAC-1115: Add support for complex LDAP directory layouts
This commit is contained in:
@@ -25,11 +25,11 @@ package org.glyptodon.guacamole.auth.ldap;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.novell.ldap.LDAPConnection;
|
||||
import com.novell.ldap.LDAPException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
import org.glyptodon.guacamole.auth.ldap.user.AuthenticatedUser;
|
||||
import org.glyptodon.guacamole.auth.ldap.user.UserContext;
|
||||
import org.glyptodon.guacamole.GuacamoleException;
|
||||
import org.glyptodon.guacamole.auth.ldap.user.UserService;
|
||||
import org.glyptodon.guacamole.net.auth.Credentials;
|
||||
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
|
||||
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
||||
@@ -50,10 +50,10 @@ public class AuthenticationProviderService {
|
||||
private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
|
||||
|
||||
/**
|
||||
* Service for escaping parts of LDAP queries.
|
||||
* Service for creating and managing connections to LDAP servers.
|
||||
*/
|
||||
@Inject
|
||||
private EscapingService escapingService;
|
||||
private LDAPConnectionService ldapService;
|
||||
|
||||
/**
|
||||
* Service for retrieving LDAP server configuration information.
|
||||
@@ -61,6 +61,12 @@ public class AuthenticationProviderService {
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Service for retrieving users and their corresponding LDAP DNs.
|
||||
*/
|
||||
@Inject
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* Provider for AuthenticatedUser objects.
|
||||
*/
|
||||
@@ -73,6 +79,72 @@ public class AuthenticationProviderService {
|
||||
@Inject
|
||||
private Provider<UserContext> userContextProvider;
|
||||
|
||||
/**
|
||||
* Determines the DN which corresponds to the user having the given
|
||||
* username. The DN will either be derived directly from the user base DN,
|
||||
* or queried from the LDAP server, depending on how LDAP authentication
|
||||
* has been configured.
|
||||
*
|
||||
* @param username
|
||||
* The username of the user whose corresponding DN should be returned.
|
||||
*
|
||||
* @return
|
||||
* The DN which corresponds to the user having the given username.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If required properties are missing, and thus the user DN cannot be
|
||||
* determined.
|
||||
*/
|
||||
private String getUserBindDN(String username)
|
||||
throws GuacamoleException {
|
||||
|
||||
// If a search DN is provided, search the LDAP directory for the DN
|
||||
// corresponding to the given username
|
||||
String searchBindDN = confService.getSearchBindDN();
|
||||
if (searchBindDN != null) {
|
||||
|
||||
// Create an LDAP connection using the search account
|
||||
LDAPConnection searchConnection = ldapService.bindAs(
|
||||
searchBindDN,
|
||||
confService.getSearchBindPassword()
|
||||
);
|
||||
|
||||
// Warn of failure to find
|
||||
if (searchConnection == null) {
|
||||
logger.error("Unable to bind using search DN \"{}\"", searchBindDN);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// Retrieve all DNs associated with the given username
|
||||
List<String> userDNs = userService.getUserDNs(searchConnection, username);
|
||||
if (userDNs.isEmpty())
|
||||
return null;
|
||||
|
||||
// Warn if multiple DNs exist for the same user
|
||||
if (userDNs.size() != 1) {
|
||||
logger.warn("Multiple DNs possible for user \"{}\": {}", username, userDNs);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return the single possible DN
|
||||
return userDNs.get(0);
|
||||
|
||||
}
|
||||
|
||||
// Always disconnect
|
||||
finally {
|
||||
ldapService.disconnect(searchConnection);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Otherwise, derive user DN from base DN
|
||||
return userService.deriveUserDN(username);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds to the LDAP server using the provided Guacamole credentials. The
|
||||
* DN of the user is derived using the LDAP configuration properties
|
||||
@@ -92,76 +164,64 @@ public class AuthenticationProviderService {
|
||||
private LDAPConnection bindAs(Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
|
||||
LDAPConnection ldapConnection;
|
||||
// Get username and password from credentials
|
||||
String username = credentials.getUsername();
|
||||
String password = credentials.getPassword();
|
||||
|
||||
// Require username
|
||||
if (credentials.getUsername() == null) {
|
||||
if (username == null || username.isEmpty()) {
|
||||
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Require password, and do not allow anonymous binding
|
||||
if (credentials.getPassword() == null
|
||||
|| credentials.getPassword().length() == 0) {
|
||||
if (password == null || password.isEmpty()) {
|
||||
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Connect to LDAP server
|
||||
try {
|
||||
ldapConnection = new LDAPConnection();
|
||||
ldapConnection.connect(
|
||||
confService.getServerHostname(),
|
||||
confService.getServerPort()
|
||||
);
|
||||
}
|
||||
catch (LDAPException e) {
|
||||
logger.error("Unable to connect to LDAP server: {}", e.getMessage());
|
||||
logger.debug("Failed to connect to LDAP server.", e);
|
||||
// Determine user DN
|
||||
String userDN = getUserBindDN(username);
|
||||
if (userDN == null) {
|
||||
logger.debug("Unable to determine DN for user \"{}\".", username);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bind using provided credentials
|
||||
try {
|
||||
|
||||
// Construct user DN
|
||||
String userDN =
|
||||
escapingService.escapeDN(confService.getUsernameAttribute())
|
||||
+ "=" + escapingService.escapeDN(credentials.getUsername())
|
||||
+ "," + confService.getUserBaseDN();
|
||||
|
||||
// Bind as user
|
||||
try {
|
||||
ldapConnection.bind(LDAPConnection.LDAP_V3, userDN,
|
||||
credentials.getPassword().getBytes("UTF-8"));
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
|
||||
logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Disconnect if an error occurs during bind
|
||||
catch (LDAPException e) {
|
||||
ldapConnection.disconnect();
|
||||
throw e;
|
||||
}
|
||||
|
||||
}
|
||||
catch (LDAPException e) {
|
||||
logger.debug("LDAP bind failed.", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ldapConnection;
|
||||
// Bind using user's DN
|
||||
return ldapService.bindAs(userDN, password);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an AuthenticatedUser representing the user authenticated by the
|
||||
* given credentials.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials to use for authentication.
|
||||
*
|
||||
* @return
|
||||
* An AuthenticatedUser representing the user authenticated by the
|
||||
* given credentials.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs while authenticating the user, or if access is
|
||||
* denied.
|
||||
*/
|
||||
public AuthenticatedUser authenticateUser(Credentials credentials)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Attempt bind
|
||||
LDAPConnection ldapConnection = bindAs(credentials);
|
||||
LDAPConnection ldapConnection;
|
||||
try {
|
||||
ldapConnection = bindAs(credentials);
|
||||
}
|
||||
catch (GuacamoleException e) {
|
||||
logger.error("Cannot bind with LDAP server: {}", e.getMessage());
|
||||
logger.debug("Error binding with LDAP server.", e);
|
||||
ldapConnection = null;
|
||||
}
|
||||
|
||||
// If bind fails, permission to login is denied
|
||||
if (ldapConnection == null)
|
||||
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
|
||||
|
||||
@@ -176,18 +236,7 @@ public class AuthenticationProviderService {
|
||||
|
||||
// Always disconnect
|
||||
finally {
|
||||
|
||||
// Attempt disconnect
|
||||
try {
|
||||
ldapConnection.disconnect();
|
||||
}
|
||||
|
||||
// Warn if disconnect unexpectedly fails
|
||||
catch (LDAPException e) {
|
||||
logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage());
|
||||
logger.debug("LDAP disconnect failed.", e);
|
||||
}
|
||||
|
||||
ldapService.disconnect(ldapConnection);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -226,18 +275,7 @@ public class AuthenticationProviderService {
|
||||
|
||||
// Always disconnect
|
||||
finally {
|
||||
|
||||
// Attempt disconnect
|
||||
try {
|
||||
ldapConnection.disconnect();
|
||||
}
|
||||
|
||||
// Warn if disconnect unexpectedly fails
|
||||
catch (LDAPException e) {
|
||||
logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage());
|
||||
logger.debug("LDAP disconnect failed.", e);
|
||||
}
|
||||
|
||||
ldapService.disconnect(ldapConnection);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -23,6 +23,8 @@
|
||||
package org.glyptodon.guacamole.auth.ldap;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.glyptodon.guacamole.GuacamoleException;
|
||||
import org.glyptodon.guacamole.environment.Environment;
|
||||
|
||||
@@ -77,21 +79,21 @@ public class ConfigurationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username attribute which should be used to query and bind
|
||||
* 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 attribute which should be used to query and bind users
|
||||
* 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 String getUsernameAttribute() throws GuacamoleException {
|
||||
public List<String> getUsernameAttributes() throws GuacamoleException {
|
||||
return environment.getProperty(
|
||||
LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE,
|
||||
"uid"
|
||||
Collections.singletonList("uid")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,20 +117,59 @@ public class ConfigurationService {
|
||||
|
||||
/**
|
||||
* Returns the base DN under which all Guacamole configurations
|
||||
* (connections) will be stored within the LDAP directory.
|
||||
* (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.
|
||||
* 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, or if the configuration
|
||||
* base DN property is not specified.
|
||||
* If guacamole.properties cannot be parsed.
|
||||
*/
|
||||
public String getConfigurationBaseDN() throws GuacamoleException {
|
||||
return environment.getRequiredProperty(
|
||||
return environment.getProperty(
|
||||
LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DN 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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -81,6 +81,7 @@ public class LDAPAuthenticationProviderModule extends AbstractModule {
|
||||
bind(ConfigurationService.class);
|
||||
bind(ConnectionService.class);
|
||||
bind(EscapingService.class);
|
||||
bind(LDAPConnectionService.class);
|
||||
bind(UserService.class);
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Glyptodon LLC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.glyptodon.guacamole.auth.ldap;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.novell.ldap.LDAPConnection;
|
||||
import com.novell.ldap.LDAPException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import org.glyptodon.guacamole.GuacamoleException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Service for creating and managing connections to LDAP servers.
|
||||
*
|
||||
* @author Michael Jumper
|
||||
*/
|
||||
public class LDAPConnectionService {
|
||||
|
||||
/**
|
||||
* Logger for this class.
|
||||
*/
|
||||
private final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
|
||||
|
||||
/**
|
||||
* Service for retrieving LDAP server configuration information.
|
||||
*/
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Binds to the LDAP server using the provided user DN and password.
|
||||
*
|
||||
* @param userDN
|
||||
* The DN of the user to bind as, or null to bind anonymously.
|
||||
*
|
||||
* @param password
|
||||
* The password to use when binding as the specified user, or null to
|
||||
* attempt to bind without a password.
|
||||
*
|
||||
* @return
|
||||
* A bound LDAP connection, or null if the connection could not be
|
||||
* bound.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs while binding to the LDAP server.
|
||||
*/
|
||||
public LDAPConnection bindAs(String userDN, String password)
|
||||
throws GuacamoleException {
|
||||
|
||||
LDAPConnection ldapConnection;
|
||||
|
||||
// Connect to LDAP server
|
||||
try {
|
||||
ldapConnection = new LDAPConnection();
|
||||
ldapConnection.connect(
|
||||
confService.getServerHostname(),
|
||||
confService.getServerPort()
|
||||
);
|
||||
}
|
||||
catch (LDAPException e) {
|
||||
logger.error("Unable to connect to LDAP server: {}", e.getMessage());
|
||||
logger.debug("Failed to connect to LDAP server.", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bind using provided credentials
|
||||
try {
|
||||
|
||||
byte[] passwordBytes;
|
||||
try {
|
||||
|
||||
// Convert password into corresponding byte array
|
||||
if (password != null)
|
||||
passwordBytes = password.getBytes("UTF-8");
|
||||
else
|
||||
passwordBytes = null;
|
||||
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
|
||||
logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
|
||||
disconnect(ldapConnection);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bind as user
|
||||
ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, passwordBytes);
|
||||
|
||||
}
|
||||
|
||||
// Disconnect if an error occurs during bind
|
||||
catch (LDAPException e) {
|
||||
logger.debug("LDAP bind failed.", e);
|
||||
disconnect(ldapConnection);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ldapConnection;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the given LDAP connection, logging any failure to do so
|
||||
* appropriately.
|
||||
*
|
||||
* @param ldapConnection
|
||||
* The LDAP connection to disconnect.
|
||||
*/
|
||||
public void disconnect(LDAPConnection ldapConnection) {
|
||||
|
||||
// Attempt disconnect
|
||||
try {
|
||||
ldapConnection.disconnect();
|
||||
}
|
||||
|
||||
// Warn if disconnect unexpectedly fails
|
||||
catch (LDAPException e) {
|
||||
logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage());
|
||||
logger.debug("LDAP disconnect failed.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -51,8 +51,10 @@ public class LDAPGuacamoleProperties {
|
||||
};
|
||||
|
||||
/**
|
||||
* The base DN of users. All users must be direct children of this DN,
|
||||
* varying only by LDAP_USERNAME_ATTRIBUTE.
|
||||
* The base DN of users. All users must be contained somewhere within the
|
||||
* subtree of this DN. If the LDAP authentication will not be given its own
|
||||
* credentials for querying other LDAP users, all users must be direct
|
||||
* children of this base DN, varying only by LDAP_USERNAME_ATTRIBUTE.
|
||||
*/
|
||||
public static final StringGuacamoleProperty LDAP_USER_BASE_DN = new StringGuacamoleProperty() {
|
||||
|
||||
@@ -62,11 +64,14 @@ public class LDAPGuacamoleProperties {
|
||||
};
|
||||
|
||||
/**
|
||||
* The attribute which identifies users. This attribute must be part of
|
||||
* each user's DN such that the concatenation of this attribute and
|
||||
* LDAP_USER_BASE_DN equals the users full DN.
|
||||
* The attribute or attributes which identify users. One of these
|
||||
* attributes must be present within each Guacamole user's record in the
|
||||
* LDAP directory. If the LDAP authentication will not be given its own
|
||||
* credentials for querying other LDAP users, this list may contain only
|
||||
* one attribute, and the concatenation of that attribute and the value of
|
||||
* LDAP_USER_BASE_DN must equal the user's full DN.
|
||||
*/
|
||||
public static final StringGuacamoleProperty LDAP_USERNAME_ATTRIBUTE = new StringGuacamoleProperty() {
|
||||
public static final StringListProperty LDAP_USERNAME_ATTRIBUTE = new StringListProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ldap-username-attribute"; }
|
||||
@@ -93,4 +98,30 @@ public class LDAPGuacamoleProperties {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The DN of the user that the LDAP authentication should bind as when
|
||||
* searching for the user accounts of users attempting to log in. If not
|
||||
* specified, the DNs of users attempting to log in will be derived from
|
||||
* the LDAP_BASE_DN and LDAP_USERNAME_ATTRIBUTE directly.
|
||||
*/
|
||||
public static final StringGuacamoleProperty LDAP_SEARCH_BIND_DN = new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ldap-search-bind-dn"; }
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The password to provide to the LDAP server when binding as
|
||||
* LDAP_SEARCH_BIND_DN. If LDAP_SEARCH_BIND_DN is not specified, this
|
||||
* property has no effect. If this property is not specified, no password
|
||||
* will be provided when attempting to bind as LDAP_SEARCH_BIND_DN.
|
||||
*/
|
||||
public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD = new StringGuacamoleProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() { return "ldap-search-bind-password"; }
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Glyptodon LLC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.glyptodon.guacamole.auth.ldap;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import org.glyptodon.guacamole.GuacamoleException;
|
||||
import org.glyptodon.guacamole.properties.GuacamoleProperty;
|
||||
|
||||
/**
|
||||
* A GuacamoleProperty whose value is a List of Strings. The string value
|
||||
* parsed to produce this list is a comma-delimited list. Duplicate values are
|
||||
* ignored, as is any whitespace following delimiters. To maintain
|
||||
* compatibility with the behavior of Java properties in general, only
|
||||
* whitespace at the beginning of each value is ignored; trailing whitespace
|
||||
* becomes part of the value.
|
||||
*
|
||||
* @author Michael Jumper
|
||||
*/
|
||||
public abstract class StringListProperty implements GuacamoleProperty<List<String>> {
|
||||
|
||||
/**
|
||||
* A pattern which matches against the delimiters between values. This is
|
||||
* currently simply a comma and any following whitespace. Parts of the
|
||||
* input string which match this pattern will not be included in the parsed
|
||||
* result.
|
||||
*/
|
||||
private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*");
|
||||
|
||||
@Override
|
||||
public List<String> parseValue(String values) throws GuacamoleException {
|
||||
|
||||
// If no property provided, return null.
|
||||
if (values == null)
|
||||
return null;
|
||||
|
||||
// Split string into a list of individual values
|
||||
List<String> stringValues = Arrays.asList(DELIMITER_PATTERN.split(values));
|
||||
if (stringValues.isEmpty())
|
||||
return null;
|
||||
|
||||
return stringValues;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -28,6 +28,7 @@ import com.novell.ldap.LDAPConnection;
|
||||
import com.novell.ldap.LDAPEntry;
|
||||
import com.novell.ldap.LDAPException;
|
||||
import com.novell.ldap.LDAPSearchResults;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -86,6 +87,11 @@ public class ConnectionService {
|
||||
public Map<String, Connection> getConnections(LDAPConnection ldapConnection)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Do not return any connections if base DN is not specified
|
||||
String configurationBaseDN = confService.getConfigurationBaseDN();
|
||||
if (configurationBaseDN == null)
|
||||
return Collections.<String, Connection>emptyMap();
|
||||
|
||||
try {
|
||||
|
||||
// Pull the current user DN from the LDAP connection
|
||||
@@ -98,7 +104,7 @@ public class ConnectionService {
|
||||
|
||||
// Find all Guacamole connections for the given user
|
||||
LDAPSearchResults results = ldapConnection.search(
|
||||
confService.getConfigurationBaseDN(),
|
||||
configurationBaseDN,
|
||||
LDAPConnection.SCOPE_SUB,
|
||||
"(&(objectClass=guacConfigGroup)(member=" + escapingService.escapeLDAPSearchFilter(userDN) + "))",
|
||||
null,
|
||||
|
@@ -28,7 +28,9 @@ import com.novell.ldap.LDAPConnection;
|
||||
import com.novell.ldap.LDAPEntry;
|
||||
import com.novell.ldap.LDAPException;
|
||||
import com.novell.ldap.LDAPSearchResults;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.glyptodon.guacamole.auth.ldap.ConfigurationService;
|
||||
import org.glyptodon.guacamole.auth.ldap.EscapingService;
|
||||
@@ -64,6 +66,64 @@ public class UserService {
|
||||
@Inject
|
||||
private ConfigurationService confService;
|
||||
|
||||
/**
|
||||
* Adds all Guacamole users accessible to the user currently bound under
|
||||
* the given LDAP connection to the provided map. Only users with the
|
||||
* specified attribute are added. If the same username is encountered
|
||||
* multiple times, warnings about possible ambiguity will be logged.
|
||||
*
|
||||
* @param ldapConnection
|
||||
* The current connection to the LDAP server, associated with the
|
||||
* current user.
|
||||
*
|
||||
* @return
|
||||
* All users accessible to the user currently bound under the given
|
||||
* LDAP connection, as a map of connection identifier to corresponding
|
||||
* user object.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs preventing retrieval of users.
|
||||
*/
|
||||
private void putAllUsers(Map<String, User> users, LDAPConnection ldapConnection,
|
||||
String usernameAttribute) throws GuacamoleException {
|
||||
|
||||
try {
|
||||
|
||||
// Find all Guacamole users underneath base DN
|
||||
LDAPSearchResults results = ldapConnection.search(
|
||||
confService.getUserBaseDN(),
|
||||
LDAPConnection.SCOPE_SUB,
|
||||
"(&(objectClass=*)(" + escapingService.escapeLDAPSearchFilter(usernameAttribute) + "=*))",
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
// Read all visible users
|
||||
while (results.hasMore()) {
|
||||
|
||||
LDAPEntry entry = results.next();
|
||||
|
||||
// Get username from record
|
||||
LDAPAttribute username = entry.getAttribute(usernameAttribute);
|
||||
if (username == null) {
|
||||
logger.warn("Queried user is missing the username attribute \"{}\".", usernameAttribute);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store user using their username as the identifier
|
||||
String identifier = username.getStringValue();
|
||||
if (users.put(identifier, new SimpleUser(identifier)) != null)
|
||||
logger.warn("Possibly ambiguous user account: \"{}\".", identifier);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (LDAPException e) {
|
||||
throw new GuacamoleServerException("Error while querying users.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all Guacamole users accessible to the user currently bound under
|
||||
* the given LDAP connection.
|
||||
@@ -83,47 +143,169 @@ public class UserService {
|
||||
public Map<String, User> getUsers(LDAPConnection ldapConnection)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Build map of users by querying each username attribute separately
|
||||
Map<String, User> users = new HashMap<String, User>();
|
||||
for (String usernameAttribute : confService.getUsernameAttributes()) {
|
||||
|
||||
// Attempt to pull all users with given attribute
|
||||
try {
|
||||
putAllUsers(users, ldapConnection, usernameAttribute);
|
||||
}
|
||||
|
||||
// Log any errors non-fatally
|
||||
catch (GuacamoleException e) {
|
||||
logger.warn("Could not query list of all users for attribute \"{}\": {}",
|
||||
usernameAttribute, e.getMessage());
|
||||
logger.debug("Error querying list of all users.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Return map of all users
|
||||
return users;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a properly-escaped LDAP query which finds all objects having
|
||||
* at least one username attribute set to the specified username, where
|
||||
* the possible username attributes are defined within
|
||||
* guacamole.properties.
|
||||
*
|
||||
* @param username
|
||||
* The username that the resulting LDAP query should search for within
|
||||
* objects within the LDAP directory.
|
||||
*
|
||||
* @return
|
||||
* An LDAP query which will search for arbitrary LDAP objects
|
||||
* containing at least one username attribute set to the specified
|
||||
* username.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If the LDAP query cannot be generated because the list of username
|
||||
* attributes cannot be parsed from guacamole.properties.
|
||||
*/
|
||||
private String generateLDAPQuery(String username)
|
||||
throws GuacamoleException {
|
||||
|
||||
List<String> usernameAttributes = confService.getUsernameAttributes();
|
||||
|
||||
// Build LDAP query for users having at least one username attribute
|
||||
// with the specified username as its value
|
||||
StringBuilder ldapQuery = new StringBuilder("(&(objectClass=*)");
|
||||
|
||||
// Include all attributes within OR clause if there are more than one
|
||||
if (usernameAttributes.size() > 1)
|
||||
ldapQuery.append("(|");
|
||||
|
||||
// Add equality comparison for each possible username attribute
|
||||
for (String usernameAttribute : usernameAttributes) {
|
||||
ldapQuery.append("(");
|
||||
ldapQuery.append(escapingService.escapeLDAPSearchFilter(usernameAttribute));
|
||||
ldapQuery.append("=");
|
||||
ldapQuery.append(escapingService.escapeLDAPSearchFilter(username));
|
||||
ldapQuery.append(")");
|
||||
}
|
||||
|
||||
// Close OR clause, if any
|
||||
if (usernameAttributes.size() > 1)
|
||||
ldapQuery.append(")");
|
||||
|
||||
// Close overall query (AND clause)
|
||||
ldapQuery.append(")");
|
||||
|
||||
return ldapQuery.toString();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all DNs corresponding to the users having the given
|
||||
* username. If multiple username attributes are defined, or if uniqueness
|
||||
* is not enforced across the username attribute, it is possible that this
|
||||
* will return multiple DNs.
|
||||
*
|
||||
* @param ldapConnection
|
||||
* The connection to the LDAP server to use when querying user DNs.
|
||||
*
|
||||
* @param username
|
||||
* The username of the user whose corresponding user account DNs are
|
||||
* to be retrieved.
|
||||
*
|
||||
* @return
|
||||
* A list of all DNs corresponding to the users having the given
|
||||
* username. If no such DNs exist, this list will be empty.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs while querying the user DNs, or if the username
|
||||
* attribute property cannot be parsed within guacamole.properties.
|
||||
*/
|
||||
public List<String> getUserDNs(LDAPConnection ldapConnection,
|
||||
String username) throws GuacamoleException {
|
||||
|
||||
try {
|
||||
|
||||
// Get username attribute
|
||||
String usernameAttribute = confService.getUsernameAttribute();
|
||||
List<String> userDNs = new ArrayList<String>();
|
||||
|
||||
// Find all Guacamole users underneath base DN
|
||||
// Find all Guacamole users underneath base DN and matching the
|
||||
// specified username
|
||||
LDAPSearchResults results = ldapConnection.search(
|
||||
confService.getUserBaseDN(),
|
||||
LDAPConnection.SCOPE_ONE,
|
||||
"(&(objectClass=*)(" + escapingService.escapeLDAPSearchFilter(usernameAttribute) + "=*))",
|
||||
LDAPConnection.SCOPE_SUB,
|
||||
generateLDAPQuery(username),
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
// Read all visible users
|
||||
Map<String, User> users = new HashMap<String, User>();
|
||||
// Add all DNs for found users
|
||||
while (results.hasMore()) {
|
||||
|
||||
LDAPEntry entry = results.next();
|
||||
|
||||
// Get common name (CN)
|
||||
LDAPAttribute username = entry.getAttribute(usernameAttribute);
|
||||
if (username == null) {
|
||||
logger.warn("Queried user is missing the username attribute \"{}\".", usernameAttribute);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store connection using cn for both identifier and name
|
||||
String identifier = username.getStringValue();
|
||||
users.put(identifier, new SimpleUser(identifier));
|
||||
|
||||
userDNs.add(entry.getDN());
|
||||
}
|
||||
|
||||
// Return map of all connections
|
||||
return users;
|
||||
// Return all discovered DNs (if any)
|
||||
return userDNs;
|
||||
|
||||
}
|
||||
catch (LDAPException e) {
|
||||
throw new GuacamoleServerException("Error while querying users.", e);
|
||||
throw new GuacamoleServerException("Error while query user DNs.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the DN which corresponds to the user having the given
|
||||
* username. The DN will either be derived directly from the user base DN,
|
||||
* or queried from the LDAP server, depending on how LDAP authentication
|
||||
* has been configured.
|
||||
*
|
||||
* @param username
|
||||
* The username of the user whose corresponding DN should be returned.
|
||||
*
|
||||
* @return
|
||||
* The DN which corresponds to the user having the given username.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If required properties are missing, and thus the user DN cannot be
|
||||
* determined.
|
||||
*/
|
||||
public String deriveUserDN(String username)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Pull username attributes from properties
|
||||
List<String> usernameAttributes = confService.getUsernameAttributes();
|
||||
|
||||
// We need exactly one base DN to derive the user DN
|
||||
if (usernameAttributes.size() != 1) {
|
||||
logger.warn("Cannot directly derive user DN when multiple username attributes are specified");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Derive user DN from base DN
|
||||
return
|
||||
escapingService.escapeDN(usernameAttributes.get(0))
|
||||
+ "=" + escapingService.escapeDN(username)
|
||||
+ "," + confService.getUserBaseDN();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user