From 8c284399b17fc9cb9818f6ed07dd6beb75368a7a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 20 Oct 2015 14:57:09 -0700 Subject: [PATCH] GUAC-1115: Accept multiple username attributes. --- .../ldap/AuthenticationProviderService.java | 86 +++++++++++++-- .../auth/ldap/ConfigurationService.java | 10 +- .../auth/ldap/LDAPGuacamoleProperties.java | 11 +- .../auth/ldap/StringListProperty.java | 63 +++++++++++ .../guacamole/auth/ldap/user/UserService.java | 103 +++++++++++------- 5 files changed, 216 insertions(+), 57 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/StringListProperty.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/AuthenticationProviderService.java index 63cbeddfe..4d63b5fea 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/AuthenticationProviderService.java @@ -27,6 +27,7 @@ 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; @@ -73,6 +74,42 @@ public class AuthenticationProviderService { @Inject private Provider 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 { + + // Pull username attributes from properties + List usernameAttributes = confService.getUsernameAttributes(); + if (usernameAttributes.isEmpty()) + return null; + + // We need exactly one base DN to derive the user DN + if (usernameAttributes.size() != 1) + return null; + + // Derive user DN from base DN + return + escapingService.escapeDN(usernameAttributes.get(0)) + + "=" + escapingService.escapeDN(username) + + "," + confService.getUserBaseDN(); + + } + /** * Binds to the LDAP server using the provided Guacamole credentials. The * DN of the user is derived using the LDAP configuration properties @@ -94,15 +131,18 @@ public class AuthenticationProviderService { 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; } @@ -124,16 +164,17 @@ public class AuthenticationProviderService { // Bind using provided credentials try { - // Construct user DN - String userDN = - escapingService.escapeDN(confService.getUsernameAttribute()) - + "=" + escapingService.escapeDN(credentials.getUsername()) - + "," + confService.getUserBaseDN(); + // Determine user DN + String userDN = getUserBindDN(username); + if (userDN == null) { + logger.error("Unable to determine DN for user \"{}\".", username); + return null; + } // Bind as user try { ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, - credentials.getPassword().getBytes("UTF-8")); + password.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage()); @@ -157,11 +198,36 @@ public class AuthenticationProviderService { } + /** + * 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); diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/ConfigurationService.java index 692300499..7d3f77b29 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/ConfigurationService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/ConfigurationService.java @@ -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 getUsernameAttributes() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE, - "uid" + Collections.singletonList("uid") ); } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/LDAPGuacamoleProperties.java b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/LDAPGuacamoleProperties.java index 8fabf816e..ff7c76c1e 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/LDAPGuacamoleProperties.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/LDAPGuacamoleProperties.java @@ -62,11 +62,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 the 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"; } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/StringListProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/StringListProperty.java new file mode 100644 index 000000000..f5f75cd1c --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/StringListProperty.java @@ -0,0 +1,63 @@ +/* + * 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> { + + /** + * 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 parseValue(String values) throws GuacamoleException { + + // If no property provided, return null. + if (values == null) + return null; + + // Split string into a list of individual values + return Arrays.asList(DELIMITER_PATTERN.split(values)); + + } + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/UserService.java b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/UserService.java index 9b72c0f44..9e9533dd9 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/UserService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/UserService.java @@ -64,6 +64,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 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,46 +141,13 @@ public class UserService { public Map getUsers(LDAPConnection ldapConnection) throws GuacamoleException { - try { + // Build map of users by querying each username attribute separately + Map users = new HashMap(); + for (String usernameAttribute : confService.getUsernameAttributes()) + putAllUsers(users, ldapConnection, usernameAttribute); - // Get username attribute - String usernameAttribute = confService.getUsernameAttribute(); - - // Find all Guacamole users underneath base DN - LDAPSearchResults results = ldapConnection.search( - confService.getUserBaseDN(), - LDAPConnection.SCOPE_ONE, - "(&(objectClass=*)(" + escapingService.escapeLDAPSearchFilter(usernameAttribute) + "=*))", - null, - false - ); - - // Read all visible users - Map users = new HashMap(); - 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)); - - } - - // Return map of all connections - return users; - - } - catch (LDAPException e) { - throw new GuacamoleServerException("Error while querying users.", e); - } + // Return map of all users + return users; }