mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 13:41:21 +00:00
GUAC-1115: Accept multiple username attributes.
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
@@ -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")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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"; }
|
||||||
|
@@ -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));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user