GUAC-586: Implement listing of visible users within LDAP (by completely rewriting the LDAP auth provider).

This commit is contained in:
Michael Jumper
2015-09-01 01:15:51 -07:00
parent 5e5a6487d3
commit 71052fa126
12 changed files with 1282 additions and 239 deletions

View File

@@ -102,6 +102,18 @@
<version>4.3</version>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-multibindings</artifactId>
<version>3.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Glyptodon LLC
* 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
@@ -23,25 +23,15 @@
package net.sourceforge.guacamole.net.auth.ldap;
import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
import org.glyptodon.guacamole.auth.ldap.AuthenticationProviderService;
import org.glyptodon.guacamole.auth.ldap.LDAPAuthenticationProviderModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials;
import net.sourceforge.guacamole.net.auth.ldap.properties.LDAPGuacamoleProperties;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.environment.LocalEnvironment;
import org.glyptodon.guacamole.net.auth.simple.SimpleAuthenticationProvider;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glyptodon.guacamole.net.auth.UserContext;
/**
* Allows users to be authenticated against an LDAP server. Each user may have
@@ -50,17 +40,13 @@ import org.slf4j.LoggerFactory;
*
* @author Michael Jumper
*/
public class LDAPAuthenticationProvider extends SimpleAuthenticationProvider {
public class LDAPAuthenticationProvider implements AuthenticationProvider {
/**
* Logger for this class.
* Injector which will manage the object graph of this authentication
* provider.
*/
private Logger logger = LoggerFactory.getLogger(LDAPAuthenticationProvider.class);
/**
* Guacamole server environment.
*/
private final Environment environment;
private final Injector injector;
/**
* Creates a new LDAPAuthenticationProvider that authenticates users
@@ -71,7 +57,12 @@ public class LDAPAuthenticationProvider extends SimpleAuthenticationProvider {
* a property.
*/
public LDAPAuthenticationProvider() throws GuacamoleException {
environment = new LocalEnvironment();
// Set up Guice injector.
injector = Guice.createInjector(
new LDAPAuthenticationProviderModule(this)
);
}
@Override
@@ -79,219 +70,33 @@ public class LDAPAuthenticationProvider extends SimpleAuthenticationProvider {
return "ldap";
}
// Courtesy of OWASP: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
private static String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\5c");
break;
case '*':
sb.append("\\2a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(curChar);
}
}
return sb.toString();
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException {
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
// Courtesy of OWASP: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
private static String escapeDN(String name) {
StringBuilder sb = new StringBuilder();
if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
sb.append('\\'); // add the leading backslash if needed
}
for (int i = 0; i < name.length(); i++) {
char curChar = name.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\\\");
break;
case ',':
sb.append("\\,");
break;
case '+':
sb.append("\\+");
break;
case '"':
sb.append("\\\"");
break;
case '<':
sb.append("\\<");
break;
case '>':
sb.append("\\>");
break;
case ';':
sb.append("\\;");
break;
default:
sb.append(curChar);
}
}
if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
}
return sb.toString();
}
@Override
public Map<String, GuacamoleConfiguration> getAuthorizedConfigurations(Credentials credentials) throws GuacamoleException {
// Require username
if (credentials.getUsername() == null) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null;
public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException {
return authenticatedUser;
}
// Require password, and do not allow anonymous binding
if (credentials.getPassword() == null
|| credentials.getPassword().length() == 0) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null;
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Connect to LDAP server
LDAPConnection ldapConnection;
try {
ldapConnection = new LDAPConnection();
ldapConnection.connect(
environment.getRequiredProperty(LDAPGuacamoleProperties.LDAP_HOSTNAME),
environment.getRequiredProperty(LDAPGuacamoleProperties.LDAP_PORT)
);
}
catch (LDAPException e) {
throw new GuacamoleServerException("Unable to connect to LDAP server.", e);
}
// Get username attribute
String username_attribute = environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE
);
// Get user base DN
String user_base_dn = environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USER_BASE_DN
);
// Construct user DN
String user_dn =
escapeDN(username_attribute) + "=" + escapeDN(credentials.getUsername())
+ "," + user_base_dn;
try {
// Bind as user
try {
ldapConnection.bind(
LDAPConnection.LDAP_V3,
user_dn,
credentials.getPassword().getBytes("UTF-8")
);
}
catch (UnsupportedEncodingException e) {
throw new GuacamoleException(e);
}
}
catch (LDAPException e) {
logger.debug("LDAP bind failed.", e);
return null;
}
// Get config base DN
String config_base_dn = environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN
);
// Pull all connections
try {
// Find all guac configs for this user
LDAPSearchResults results = ldapConnection.search(
config_base_dn,
LDAPConnection.SCOPE_SUB,
"(&(objectClass=guacConfigGroup)(member=" + escapeLDAPSearchFilter(user_dn) + "))",
null,
false
);
// Add all configs
Map<String, GuacamoleConfiguration> configs = new TreeMap<String, GuacamoleConfiguration>();
while (results.hasMore()) {
LDAPEntry entry = results.next();
// New empty configuration
GuacamoleConfiguration config = new GuacamoleConfiguration();
// Get CN
LDAPAttribute cn = entry.getAttribute("cn");
if (cn == null)
throw new GuacamoleException("guacConfigGroup without cn");
// Get protocol
LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
if (protocol == null)
throw new GuacamoleException("guacConfigGroup without guacConfigProtocol");
// Set protocol
config.setProtocol(protocol.getStringValue());
// Get parameters, if any
LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
if (parameterAttribute != null) {
// For each parameter
Enumeration<?> parameters = parameterAttribute.getStringValues();
while (parameters.hasMoreElements()) {
String parameter = (String) parameters.nextElement();
// Parse parameter
int equals = parameter.indexOf('=');
if (equals != -1) {
// Parse name
String name = parameter.substring(0, equals);
String value = parameter.substring(equals+1);
config.setParameter(name, value);
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.getUserContext(authenticatedUser);
}
}
}
// Store config by CN
configs.put(cn.getStringValue(), config);
}
// Disconnect
ldapConnection.disconnect();
return configs;
}
catch (LDAPException e) {
throw new GuacamoleServerException("Error while querying for connections.", e);
}
@Override
public UserContext updateUserContext(UserContext context,
AuthenticatedUser authenticatedUser) throws GuacamoleException {
return context;
}
}

View File

@@ -0,0 +1,245 @@
/*
* 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.google.inject.Provider;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPException;
import java.io.UnsupportedEncodingException;
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.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service providing convenience functions for the LDAP AuthenticationProvider
* implementation.
*
* @author Michael Jumper
*/
public class AuthenticationProviderService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
/**
* Service for escaping parts of LDAP queries.
*/
@Inject
private EscapingService escapingService;
/**
* Service for retrieving LDAP server configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<AuthenticatedUser> authenticatedUserProvider;
/**
* Provider for UserContext objects.
*/
@Inject
private Provider<UserContext> userContextProvider;
/**
* Binds to the LDAP server using the provided Guacamole credentials. The
* DN of the user is derived using the LDAP configuration properties
* provided in guacamole.properties, as is the server hostname and port
* information.
*
* @param credentials
* The credentials to use to bind to the LDAP server.
*
* @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.
*/
private LDAPConnection bindAs(Credentials credentials)
throws GuacamoleException {
LDAPConnection ldapConnection;
// Require username
if (credentials.getUsername() == null) {
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) {
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);
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;
}
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Attempt bind
LDAPConnection ldapConnection = bindAs(credentials);
if (ldapConnection == null)
throw new GuacamoleInsufficientCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
try {
// Return AuthenticatedUser if bind succeeds
AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(credentials);
return authenticatedUser;
}
// 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);
}
}
}
/**
* Returns a UserContext object initialized with data accessible to the
* given AuthenticatedUser.
*
* @param authenticatedUser
* The AuthenticatedUser to retrieve data for.
*
* @return
* A UserContext object initialized with data accessible to the given
* AuthenticatedUser.
*
* @throws GuacamoleException
* If the UserContext cannot be created due to an error.
*/
public UserContext getUserContext(org.glyptodon.guacamole.net.auth.AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Bind using credentials associated with AuthenticatedUser
Credentials credentials = authenticatedUser.getCredentials();
LDAPConnection ldapConnection = bindAs(credentials);
if (ldapConnection == null)
return null;
try {
// Build user context by querying LDAP
UserContext userContext = userContextProvider.get();
userContext.init(authenticatedUser, ldapConnection);
return userContext;
}
// 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);
}
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* 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 org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.environment.Environment;
/**
* Service for retrieving configuration information regarding the LDAP server.
*
* @author Michael Jumper
*/
public class ConfigurationService {
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* Returns the hostname of the LDAP server as configured with
* guacamole.properties. By default, this will be "localhost".
*
* @return
* The hostname of the LDAP server, as configured with
* guacamole.properties.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getServerHostname() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_HOSTNAME,
"localhost"
);
}
/**
* Returns the port of the LDAP server configured with
* guacamole.properties. By default, this will be 389 - the standard LDAP
* port.
*
* @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,
389
);
}
/**
* Returns the username attribute 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
* using the LDAP directory.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getUsernameAttribute() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE,
"uid"
);
}
/**
* 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 String getUserBaseDN() throws GuacamoleException {
return environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USER_BASE_DN
);
}
/**
* Returns the base DN under which all Guacamole configurations
* (connections) will be stored within the LDAP directory.
*
* @return
* The base DN under which all 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.
*/
public String getConfigurationBaseDN() throws GuacamoleException {
return environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN
);
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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;
/**
* Service for escaping LDAP filters, distinguished names (DN's), etc.
*
* @author Michael Jumper
*/
public class EscapingService {
/**
* Escapes the given string for use within an LDAP search filter. This
* implementation is provided courtesy of OWASP:
*
* https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
*
* @param filter
* The string to escape such that it has no special meaning within an
* LDAP search filter.
*
* @return
* The escaped string, safe for use within an LDAP search filter.
*/
public String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\5c");
break;
case '*':
sb.append("\\2a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(curChar);
}
}
return sb.toString();
}
/**
* Escapes the given string such that it is safe for use within an LDAP
* distinguished name (DN). This implementation is provided courtesy of
* OWASP:
*
* https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
*
* @param name
* The string to escape such that it has no special meaning within an
* LDAP DN.
*
* @return
* The escaped string, safe for use within an LDAP DN.
*/
public String escapeDN(String name) {
StringBuilder sb = new StringBuilder();
if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
sb.append('\\'); // add the leading backslash if needed
}
for (int i = 0; i < name.length(); i++) {
char curChar = name.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\\\");
break;
case ',':
sb.append("\\,");
break;
case '+':
sb.append("\\+");
break;
case '"':
sb.append("\\\"");
break;
case '<':
sb.append("\\<");
break;
case '>':
sb.append("\\>");
break;
case ';':
sb.append("\\;");
break;
default:
sb.append(curChar);
}
}
if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
}
return sb.toString();
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.AbstractModule;
import org.glyptodon.guacamole.auth.ldap.connection.ConnectionService;
import org.glyptodon.guacamole.auth.ldap.user.UserService;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.environment.LocalEnvironment;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
/**
* Guice module which configures LDAP-specific injections.
*
* @author Michael Jumper
*/
public class LDAPAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* A reference to the LDAPAuthenticationProvider on behalf of which this
* module has configured injection.
*/
private final AuthenticationProvider authProvider;
/**
* Creates a new LDAP authentication provider module which configures
* injection for the LDAPAuthenticationProvider.
*
* @param authProvider
* The AuthenticationProvider for which injection is being configured.
*
* @throws GuacamoleException
* If an error occurs while retrieving the Guacamole server
* environment.
*/
public LDAPAuthenticationProviderModule(AuthenticationProvider authProvider)
throws GuacamoleException {
// Get local environment
this.environment = new LocalEnvironment();
// Store associated auth provider
this.authProvider = authProvider;
}
@Override
protected void configure() {
// Bind core implementations of guacamole-ext classes
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind LDAP-specific services
bind(ConfigurationService.class);
bind(ConnectionService.class);
bind(EscapingService.class);
bind(UserService.class);
}
}

View File

@@ -20,7 +20,7 @@
* THE SOFTWARE.
*/
package net.sourceforge.guacamole.net.auth.ldap.properties;
package org.glyptodon.guacamole.auth.ldap;
import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty;
import org.glyptodon.guacamole.properties.StringGuacamoleProperty;

View File

@@ -0,0 +1,170 @@
/*
* 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.connection;
import com.google.inject.Inject;
import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.glyptodon.guacamole.auth.ldap.ConfigurationService;
import org.glyptodon.guacamole.auth.ldap.EscapingService;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.simple.SimpleConnection;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for querying the connections available to a particular Guacamole
* user according to an LDAP directory.
*
* @author Michael Jumper
*/
public class ConnectionService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
/**
* Service for escaping parts of LDAP queries.
*/
@Inject
private EscapingService escapingService;
/**
* Service for retrieving LDAP server configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Returns all Guacamole connections accessible to the user currently bound
* under the given LDAP connection.
*
* @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
* corresponding connection object.
*
* @throws GuacamoleException
* If an error occurs preventing retrieval of connections.
*/
public Map<String, Connection> getConnections(LDAPConnection ldapConnection)
throws GuacamoleException {
try {
// Pull the current user DN from the LDAP connection
String userDN = ldapConnection.getAuthenticationDN();
// Find all Guacamole connections for the given user
LDAPSearchResults results = ldapConnection.search(
confService.getConfigurationBaseDN(),
LDAPConnection.SCOPE_SUB,
"(&(objectClass=guacConfigGroup)(member=" + escapingService.escapeLDAPSearchFilter(userDN) + "))",
null,
false
);
// Produce connections for each readable configuration
Map<String, Connection> connections = new HashMap<String, Connection>();
while (results.hasMore()) {
LDAPEntry entry = results.next();
// Get common name (CN)
LDAPAttribute cn = entry.getAttribute("cn");
if (cn == null) {
logger.warn("guacConfigGroup is missing a cn.");
continue;
}
// Get associated protocol
LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
if (protocol == null) {
logger.warn("guacConfigGroup \"{}\" is missing the "
+ "required \"guacConfigProtocol\" attribute.",
cn.getStringValue());
continue;
}
// Set protocol
GuacamoleConfiguration config = new GuacamoleConfiguration();
config.setProtocol(protocol.getStringValue());
// Get parameters, if any
LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
if (parameterAttribute != null) {
// For each parameter
Enumeration<?> parameters = parameterAttribute.getStringValues();
while (parameters.hasMoreElements()) {
String parameter = (String) parameters.nextElement();
// Parse parameter
int equals = parameter.indexOf('=');
if (equals != -1) {
// Parse name
String name = parameter.substring(0, equals);
String value = parameter.substring(equals+1);
config.setParameter(name, value);
}
}
}
// Store connection using cn for both identifier and name
String name = cn.getStringValue();
connections.put(name, new SimpleConnection(name, name, config));
}
// Return map of all connections
return connections;
}
catch (LDAPException e) {
throw new GuacamoleServerException("Error while querying for connections.", e);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.user;
import com.google.inject.Inject;
import org.glyptodon.guacamole.net.auth.AbstractAuthenticatedUser;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials;
/**
* An LDAP-specific implementation of AuthenticatedUser, associating a
* particular set of credentials with the LDAP authentication provider.
*
* @author Michael Jumper
*/
public class AuthenticatedUser extends AbstractAuthenticatedUser {
/**
* Reference to the authentication provider associated with this
* authenticated user.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* The credentials provided when this user was authenticated.
*/
private Credentials credentials;
/**
* Initializes this AuthenticatedUser using the given credentials.
*
* @param credentials
* The credentials provided when this user was authenticated.
*/
public void init(Credentials credentials) {
this.credentials = credentials;
setIdentifier(credentials.getUsername());
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Credentials getCredentials() {
return credentials;
}
}

View File

@@ -0,0 +1,215 @@
/*
* 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.user;
import com.google.inject.Inject;
import com.novell.ldap.LDAPConnection;
import java.util.Collection;
import java.util.Collections;
import org.glyptodon.guacamole.auth.ldap.connection.ConnectionService;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.form.Form;
import org.glyptodon.guacamole.net.auth.ActiveConnection;
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionGroup;
import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionGroupDirectory;
import org.glyptodon.guacamole.net.auth.simple.SimpleDirectory;
import org.glyptodon.guacamole.net.auth.simple.SimpleUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An LDAP-specific implementation of UserContext which queries all Guacamole
* connections and users from the LDAP directory.
*
* @author Michael Jumper
*/
public class UserContext implements org.glyptodon.guacamole.net.auth.UserContext {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(UserContext.class);
/**
* The identifier reserved for the root connection group.
*/
private static final String ROOT_CONNECTION_GROUP = "ROOT";
/**
* Service for retrieving Guacamole connections from the LDAP server.
*/
@Inject
private ConnectionService connectionService;
/**
* Service for retrieving Guacamole users from the LDAP server.
*/
@Inject
private UserService userService;
/**
* Reference to the AuthenticationProvider associated with this
* UserContext.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* Reference to a User object representing the user whose access level
* dictates the users and connections visible through this UserContext.
*/
private User self;
/**
* Directory containing all User objects accessible to the user associated
* with this UserContext.
*/
private Directory<User> userDirectory;
/**
* Directory containing all Connection objects accessible to the user
* associated with this UserContext.
*/
private Directory<Connection> connectionDirectory;
/**
* Directory containing all ConnectionGroup objects accessible to the user
* associated with this UserContext.
*/
private Directory<ConnectionGroup> connectionGroupDirectory;
/**
* Reference to the root connection group.
*/
private ConnectionGroup rootGroup;
/**
* Initializes this UserContext using the provided AuthenticatedUser and
* LDAPConnection.
*
* @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.
*
* @throws GuacamoleException
* If associated data stored within the LDAP directory cannot be
* queried due to an error.
*/
public void init(AuthenticatedUser user, LDAPConnection ldapConnection)
throws GuacamoleException {
// Query all accessible users
userDirectory = new SimpleDirectory<User>(
userService.getUsers(ldapConnection)
);
// Query all accessible connections
connectionDirectory = new SimpleDirectory<Connection>(
connectionService.getConnections(ldapConnection)
);
// Root group contains only connections
rootGroup = new SimpleConnectionGroup(
ROOT_CONNECTION_GROUP, ROOT_CONNECTION_GROUP,
connectionDirectory.getIdentifiers(),
Collections.<String>emptyList()
);
// Expose only the root group in the connection group directory
connectionGroupDirectory = new SimpleConnectionGroupDirectory(Collections.singleton(rootGroup));
// Init self with basic permissions
self = new SimpleUser(
user.getIdentifier(),
userDirectory.getIdentifiers(),
connectionDirectory.getIdentifiers(),
connectionGroupDirectory.getIdentifiers()
);
}
@Override
public User self() {
return self;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Directory<User> getUserDirectory() throws GuacamoleException {
return userDirectory;
}
@Override
public Directory<Connection> getConnectionDirectory()
throws GuacamoleException {
return connectionDirectory;
}
@Override
public Directory<ConnectionGroup> getConnectionGroupDirectory()
throws GuacamoleException {
return connectionGroupDirectory;
}
@Override
public ConnectionGroup getRootConnectionGroup() throws GuacamoleException {
return rootGroup;
}
@Override
public Directory<ActiveConnection> getActiveConnectionDirectory()
throws GuacamoleException {
return new SimpleDirectory<ActiveConnection>();
}
@Override
public Collection<Form> getUserAttributes() {
return Collections.<Form>emptyList();
}
@Override
public Collection<Form> getConnectionAttributes() {
return Collections.<Form>emptyList();
}
@Override
public Collection<Form> getConnectionGroupAttributes() {
return Collections.<Form>emptyList();
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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.user;
import com.google.inject.Inject;
import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;
import java.util.HashMap;
import java.util.Map;
import org.glyptodon.guacamole.auth.ldap.ConfigurationService;
import org.glyptodon.guacamole.auth.ldap.EscapingService;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.simple.SimpleUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for queries the users visible to a particular Guacamole user
* according to an LDAP directory.
*
* @author Michael Jumper
*/
public class UserService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* Service for escaping parts of LDAP queries.
*/
@Inject
private EscapingService escapingService;
/**
* Service for retrieving LDAP server configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Returns all Guacamole users accessible to the user currently bound under
* the given LDAP connection.
*
* @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.
*/
public Map<String, User> getUsers(LDAPConnection ldapConnection)
throws GuacamoleException {
try {
// 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<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);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Glyptodon LLC
* 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
@@ -28,7 +28,6 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleSecurityException;
import org.glyptodon.guacamole.net.auth.AbstractUser;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermission;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet;
@@ -41,6 +40,12 @@ import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet;
*/
public class SimpleUser extends AbstractUser {
/**
* All connection permissions granted to this user.
*/
private final Set<ObjectPermission> userPermissions =
new HashSet<ObjectPermission>();
/**
* All connection permissions granted to this user.
*/
@@ -59,6 +64,19 @@ public class SimpleUser extends AbstractUser {
public SimpleUser() {
}
/**
* Creates a new SimpleUser having the given username and no permissions.
*
* @param username
* The username to assign to this SimpleUser.
*/
public SimpleUser(String username) {
// Set username
setIdentifier(username);
}
/**
* Adds a new READ permission to the given set of permissions for each of
* the given identifiers.
@@ -89,6 +107,7 @@ public class SimpleUser extends AbstractUser {
*
* @param username
* The username to assign to this SimpleUser.
*
* @param connectionIdentifiers
* The identifiers of all connections this user has READ access to.
*
@@ -100,8 +119,7 @@ public class SimpleUser extends AbstractUser {
Collection<String> connectionIdentifiers,
Collection<String> connectionGroupIdentifiers) {
// Set username
setIdentifier(username);
this(username);
// Add permissions
addReadPermissions(connectionPermissions, connectionIdentifiers);
@@ -109,6 +127,37 @@ public class SimpleUser extends AbstractUser {
}
/**
* Creates a new SimpleUser having the given username and READ access to
* the users, connections, and groups having the given identifiers.
*
* @param username
* The username to assign to this SimpleUser.
*
* @param userIdentifiers
* The identifiers of all users this user has READ access to.
*
* @param connectionIdentifiers
* The identifiers of all connections this user has READ access to.
*
* @param connectionGroupIdentifiers
* The identifiers of all connection groups this user has READ access
* to.
*/
public SimpleUser(String username,
Collection<String> userIdentifiers,
Collection<String> connectionIdentifiers,
Collection<String> connectionGroupIdentifiers) {
this(username);
// Add permissions
addReadPermissions(userPermissions, userIdentifiers);
addReadPermissions(connectionPermissions, connectionIdentifiers);
addReadPermissions(connectionGroupPermissions, connectionGroupIdentifiers);
}
@Override
public Map<String, String> getAttributes() {
return Collections.<String, String>emptyMap();
@@ -140,7 +189,7 @@ public class SimpleUser extends AbstractUser {
@Override
public ObjectPermissionSet getUserPermissions()
throws GuacamoleException {
return new SimpleObjectPermissionSet();
return new SimpleObjectPermissionSet(userPermissions);
}
@Override