GUAC-1115: Accept multiple username attributes.

This commit is contained in:
Michael Jumper
2015-10-20 14:57:09 -07:00
parent 024557feeb
commit 8c284399b1
5 changed files with 216 additions and 57 deletions

View File

@@ -27,6 +27,7 @@ import com.google.inject.Provider;
import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.List;
import org.glyptodon.guacamole.auth.ldap.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.ldap.user.AuthenticatedUser;
import org.glyptodon.guacamole.auth.ldap.user.UserContext; import org.glyptodon.guacamole.auth.ldap.user.UserContext;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
@@ -73,6 +74,42 @@ public class AuthenticationProviderService {
@Inject @Inject
private Provider<UserContext> userContextProvider; 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 {
// Pull username attributes from properties
List<String> 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 * Binds to the LDAP server using the provided Guacamole credentials. The
* DN of the user is derived using the LDAP configuration properties * DN of the user is derived using the LDAP configuration properties
@@ -94,15 +131,18 @@ public class AuthenticationProviderService {
LDAPConnection ldapConnection; LDAPConnection ldapConnection;
// Get username and password from credentials
String username = credentials.getUsername();
String password = credentials.getPassword();
// Require username // Require username
if (credentials.getUsername() == null) { if (username == null || username.isEmpty()) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider."); logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null; return null;
} }
// Require password, and do not allow anonymous binding // Require password, and do not allow anonymous binding
if (credentials.getPassword() == null if (password == null || password.isEmpty()) {
|| credentials.getPassword().length() == 0) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider."); logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null; return null;
} }
@@ -124,16 +164,17 @@ public class AuthenticationProviderService {
// Bind using provided credentials // Bind using provided credentials
try { try {
// Construct user DN // Determine user DN
String userDN = String userDN = getUserBindDN(username);
escapingService.escapeDN(confService.getUsernameAttribute()) if (userDN == null) {
+ "=" + escapingService.escapeDN(credentials.getUsername()) logger.error("Unable to determine DN for user \"{}\".", username);
+ "," + confService.getUserBaseDN(); return null;
}
// Bind as user // Bind as user
try { try {
ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, ldapConnection.bind(LDAPConnection.LDAP_V3, userDN,
credentials.getPassword().getBytes("UTF-8")); password.getBytes("UTF-8"));
} }
catch (UnsupportedEncodingException e) { catch (UnsupportedEncodingException e) {
logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage()); 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) public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException { throws GuacamoleException {
// Attempt bind // 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) if (ldapConnection == null)
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD); throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);

View File

@@ -23,6 +23,8 @@
package org.glyptodon.guacamole.auth.ldap; package org.glyptodon.guacamole.auth.ldap;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Collections;
import java.util.List;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.environment.Environment; 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 * users using the LDAP directory. By default, this will be "uid" - a
* common attribute used for this purpose. * common attribute used for this purpose.
* *
* @return * @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. * using the LDAP directory.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If guacamole.properties cannot be parsed. * If guacamole.properties cannot be parsed.
*/ */
public String getUsernameAttribute() throws GuacamoleException { public List<String> getUsernameAttributes() throws GuacamoleException {
return environment.getProperty( return environment.getProperty(
LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE, LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE,
"uid" Collections.singletonList("uid")
); );
} }

View File

@@ -62,11 +62,14 @@ public class LDAPGuacamoleProperties {
}; };
/** /**
* The attribute which identifies users. This attribute must be part of * The attribute or attributes which identify users. One of these
* each user's DN such that the concatenation of this attribute and * attributes must be present within the each Guacamole user's record in
* LDAP_USER_BASE_DN equals the users full DN. * 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 @Override
public String getName() { return "ldap-username-attribute"; } public String getName() { return "ldap-username-attribute"; }

View File

@@ -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<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
return Arrays.asList(DELIMITER_PATTERN.split(values));
}
}

View File

@@ -64,6 +64,64 @@ public class UserService {
@Inject @Inject
private ConfigurationService confService; 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 * Returns all Guacamole users accessible to the user currently bound under
* the given LDAP connection. * the given LDAP connection.
@@ -83,46 +141,13 @@ public class UserService {
public Map<String, User> getUsers(LDAPConnection ldapConnection) public Map<String, User> getUsers(LDAPConnection ldapConnection)
throws GuacamoleException { throws GuacamoleException {
try { // Build map of users by querying each username attribute separately
Map<String, User> users = new HashMap<String, User>();
for (String usernameAttribute : confService.getUsernameAttributes())
putAllUsers(users, ldapConnection, usernameAttribute);
// Get username attribute // Return map of all users
String usernameAttribute = confService.getUsernameAttribute(); return users;
// 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<String, User> users = new HashMap<String, User>();
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);
}
} }