Merge pull request #250 from glyptodon/multiple-ext

GUAC-586: Load multiple AuthenticationProviders
This commit is contained in:
James Muehlner
2015-09-01 17:17:36 -07:00
45 changed files with 1449 additions and 620 deletions

View File

@@ -63,6 +63,7 @@ import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionPermis
import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionService; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionService;
import org.glyptodon.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection; import org.glyptodon.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection;
import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.mybatis.guice.MyBatisModule; import org.mybatis.guice.MyBatisModule;
import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider; import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider;
@@ -86,19 +87,31 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule {
*/ */
private final GuacamoleTunnelService tunnelService; private final GuacamoleTunnelService tunnelService;
/**
* The AuthenticationProvider which is using this module to configure
* injection.
*/
private final AuthenticationProvider authProvider;
/** /**
* Creates a new JDBC authentication provider module that configures the * Creates a new JDBC authentication provider module that configures the
* various injected base classes using the given environment, and provides * various injected base classes using the given environment, and provides
* connections using the given socket service. * connections using the given socket service.
* *
* @param authProvider
* The AuthenticationProvider which is using this module to configure
* injection.
*
* @param environment * @param environment
* The environment to use to configure injected classes. * The environment to use to configure injected classes.
* *
* @param tunnelService * @param tunnelService
* The tunnel service to use to provide tunnels sockets for connections. * The tunnel service to use to provide tunnels sockets for connections.
*/ */
public JDBCAuthenticationProviderModule(Environment environment, public JDBCAuthenticationProviderModule(AuthenticationProvider authProvider,
Environment environment,
GuacamoleTunnelService tunnelService) { GuacamoleTunnelService tunnelService) {
this.authProvider = authProvider;
this.environment = environment; this.environment = environment;
this.tunnelService = tunnelService; this.tunnelService = tunnelService;
} }
@@ -126,6 +139,7 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule {
// Bind core implementations of guacamole-ext classes // Bind core implementations of guacamole-ext classes
bind(ActiveConnectionDirectory.class); bind(ActiveConnectionDirectory.class);
bind(ActiveConnectionPermissionSet.class); bind(ActiveConnectionPermissionSet.class);
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment); bind(Environment.class).toInstance(environment);
bind(ConnectionDirectory.class); bind(ConnectionDirectory.class);
bind(ConnectionGroupDirectory.class); bind(ConnectionGroupDirectory.class);

View File

@@ -25,6 +25,7 @@ package org.glyptodon.guacamole.auth.jdbc.user;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Credentials;
/** /**
@@ -32,7 +33,7 @@ import org.glyptodon.guacamole.net.auth.Credentials;
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
public class AuthenticatedUser { public class AuthenticatedUser implements org.glyptodon.guacamole.net.auth.AuthenticatedUser {
/** /**
* The user that authenticated. * The user that authenticated.
@@ -44,6 +45,11 @@ public class AuthenticatedUser {
*/ */
private final Credentials credentials; private final Credentials credentials;
/**
* The AuthenticationProvider that authenticated this user.
*/
private final AuthenticationProvider authenticationProvider;
/** /**
* The host from which this user authenticated. * The host from which this user authenticated.
*/ */
@@ -106,13 +112,18 @@ public class AuthenticatedUser {
* Creates a new AuthenticatedUser associating the given user with their * Creates a new AuthenticatedUser associating the given user with their
* corresponding credentials. * corresponding credentials.
* *
* @param authenticationProvider
* The AuthenticationProvider that has authenticated the given user.
*
* @param user * @param user
* The user this object should represent. * The user this object should represent.
* *
* @param credentials * @param credentials
* The credentials given by the user when they authenticated. * The credentials given by the user when they authenticated.
*/ */
public AuthenticatedUser(ModeledUser user, Credentials credentials) { public AuthenticatedUser(AuthenticationProvider authenticationProvider,
ModeledUser user, Credentials credentials) {
this.authenticationProvider = authenticationProvider;
this.user = user; this.user = user;
this.credentials = credentials; this.credentials = credentials;
this.remoteHost = getRemoteHost(credentials); this.remoteHost = getRemoteHost(credentials);
@@ -134,6 +145,7 @@ public class AuthenticatedUser {
* @return * @return
* The credentials given during authentication by this user. * The credentials given during authentication by this user.
*/ */
@Override
public Credentials getCredentials() { public Credentials getCredentials() {
return credentials; return credentials;
} }
@@ -148,4 +160,19 @@ public class AuthenticatedUser {
return remoteHost; return remoteHost;
} }
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authenticationProvider;
}
@Override
public String getIdentifier() {
return user.getIdentifier();
}
@Override
public void setIdentifier(String identifier) {
user.setIdentifier(identifier);
}
} }

View File

@@ -25,17 +25,19 @@ package org.glyptodon.guacamole.auth.jdbc.user;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
/** /**
* Service which creates new UserContext instances for valid users based on * Service which authenticates users based on credentials and provides for
* credentials. * the creation of corresponding, new UserContext objects for authenticated
* users.
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
public class UserContextService { public class AuthenticationProviderService {
/** /**
* Service for accessing users. * Service for accessing users.
@@ -51,11 +53,44 @@ public class UserContextService {
/** /**
* Authenticates the user having the given credentials, returning a new * Authenticates the user having the given credentials, returning a new
* UserContext instance only if the credentials are valid. If the * AuthenticatedUser instance only if the credentials are valid. If the
* credentials are invalid or expired, an appropriate GuacamoleException * credentials are invalid or expired, an appropriate GuacamoleException
* will be thrown. * will be thrown.
* *
* @param authenticationProvider
* The AuthenticationProvider on behalf of which the user is being
* authenticated.
*
* @param credentials * @param credentials
* The credentials to use to produce the AuthenticatedUser.
*
* @return
* A new AuthenticatedUser instance for the user identified by the
* given credentials.
*
* @throws GuacamoleException
* If an error occurs during authentication, or if the given
* credentials are invalid or expired.
*/
public AuthenticatedUser authenticateUser(AuthenticationProvider authenticationProvider,
Credentials credentials) throws GuacamoleException {
// Authenticate user
AuthenticatedUser user = userService.retrieveAuthenticatedUser(authenticationProvider, credentials);
if (user != null)
return user;
// Otherwise, unauthorized
throw new GuacamoleInvalidCredentialsException("Invalid login", CredentialsInfo.USERNAME_PASSWORD);
}
/**
* Returning a new UserContext instance for the given already-authenticated
* user. A new placeholder account will be created for any user that does
* not already exist within the database.
*
* @param authenticatedUser
* The credentials to use to produce the UserContext. * The credentials to use to produce the UserContext.
* *
* @return * @return
@@ -66,23 +101,18 @@ public class UserContextService {
* If an error occurs during authentication, or if the given * If an error occurs during authentication, or if the given
* credentials are invalid or expired. * credentials are invalid or expired.
*/ */
public org.glyptodon.guacamole.net.auth.UserContext public UserContext getUserContext(org.glyptodon.guacamole.net.auth.AuthenticatedUser authenticatedUser)
getUserContext(Credentials credentials)
throws GuacamoleException { throws GuacamoleException {
// Authenticate user // Retrieve user account for already-authenticated user
ModeledUser user = userService.retrieveUser(credentials); ModeledUser user = userService.retrieveUser(authenticatedUser);
if (user != null) { if (user == null)
return null;
// Upon successful authentication, return new user context // Link to user context
UserContext context = userContextProvider.get(); UserContext context = userContextProvider.get();
context.init(user.getCurrentUser()); context.init(user.getCurrentUser());
return context; return context;
}
// Otherwise, unauthorized
throw new GuacamoleInvalidCredentialsException("Invalid login", CredentialsInfo.USERNAME_PASSWORD);
} }

View File

@@ -36,6 +36,7 @@ import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection;
import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.Form;
import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.ActiveConnection;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Directory;
@@ -51,6 +52,12 @@ import org.glyptodon.guacamole.net.auth.User;
public class UserContext extends RestrictedObject public class UserContext extends RestrictedObject
implements org.glyptodon.guacamole.net.auth.UserContext { implements org.glyptodon.guacamole.net.auth.UserContext {
/**
* The AuthenticationProvider that created this UserContext.
*/
@Inject
private AuthenticationProvider authProvider;
/** /**
* User directory restricted by the permissions of the user associated * User directory restricted by the permissions of the user associated
* with this context. * with this context.
@@ -103,6 +110,11 @@ public class UserContext extends RestrictedObject
return getCurrentUser().getUser(); return getCurrentUser().getUser();
} }
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override @Override
public Directory<User> getUserDirectory() throws GuacamoleException { public Directory<User> getUserDirectory() throws GuacamoleException {
return userDirectory; return userDirectory;

View File

@@ -40,6 +40,7 @@ import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionMapper;
import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService;
import org.glyptodon.guacamole.form.Field; import org.glyptodon.guacamole.form.Field;
import org.glyptodon.guacamole.form.PasswordField; import org.glyptodon.guacamole.form.PasswordField;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
@@ -265,18 +266,22 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
* the necessary additional parameters to reset the user's password, the * the necessary additional parameters to reset the user's password, the
* password is reset. * password is reset.
* *
* @param authenticationProvider
* The AuthenticationProvider on behalf of which the user is being
* retrieved.
*
* @param credentials * @param credentials
* The credentials to use when locating the user. * The credentials to use when locating the user.
* *
* @return * @return
* The existing ModeledUser object if the credentials given are valid, * An AuthenticatedUser containing the existing ModeledUser object if
* null otherwise. * the credentials given are valid, null otherwise.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If the provided credentials to not conform to expectations. * If the provided credentials to not conform to expectations.
*/ */
public ModeledUser retrieveUser(Credentials credentials) public AuthenticatedUser retrieveAuthenticatedUser(AuthenticationProvider authenticationProvider,
throws GuacamoleException { Credentials credentials) throws GuacamoleException {
// Get username and password // Get username and password
String username = credentials.getUsername(); String username = credentials.getUsername();
@@ -298,7 +303,7 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
// Create corresponding user object, set up cyclic reference // Create corresponding user object, set up cyclic reference
ModeledUser user = getObjectInstance(null, userModel); ModeledUser user = getObjectInstance(null, userModel);
user.setCurrentUser(new AuthenticatedUser(user, credentials)); user.setCurrentUser(new AuthenticatedUser(authenticationProvider, user, credentials));
// Verify user account is still valid as of today // Verify user account is still valid as of today
if (!user.isAccountValid()) if (!user.isAccountValid())
@@ -343,6 +348,40 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
} }
// Return now-authenticated user // Return now-authenticated user
return user.getCurrentUser();
}
/**
* Retrieves the user corresponding to the given AuthenticatedUser from the
* database.
*
* @param authenticatedUser
* The AuthenticatedUser to retrieve the corresponding ModeledUser of.
*
* @return
* The ModeledUser which corresponds to the given AuthenticatedUser, or
* null if no such user exists.
*/
public ModeledUser retrieveUser(org.glyptodon.guacamole.net.auth.AuthenticatedUser authenticatedUser) {
// If we already queried this user, return that rather than querying again
if (authenticatedUser instanceof AuthenticatedUser)
return ((AuthenticatedUser) authenticatedUser).getUser();
// Get username
String username = authenticatedUser.getIdentifier();
// Retrieve corresponding user model, if such a user exists
UserModel userModel = userMapper.selectOne(username);
if (userModel == null)
return null;
// Create corresponding user object, set up cyclic reference
ModeledUser user = getObjectInstance(null, userModel);
user.setCurrentUser(new AuthenticatedUser(authenticatedUser.getAuthenticationProvider(), user, authenticatedUser.getCredentials()));
// Return already-authenticated user
return user; return user;
} }

View File

@@ -31,9 +31,10 @@ import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule; import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule;
import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
import org.glyptodon.guacamole.auth.jdbc.tunnel.ConfigurableGuacamoleTunnelService; import org.glyptodon.guacamole.auth.jdbc.tunnel.ConfigurableGuacamoleTunnelService;
import org.glyptodon.guacamole.auth.jdbc.user.UserContextService; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticationProviderService;
import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.environment.LocalEnvironment;
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -184,25 +185,50 @@ public class MySQLAuthenticationProvider implements AuthenticationProvider {
new MySQLAuthenticationProviderModule(environment), new MySQLAuthenticationProviderModule(environment),
// Configure JDBC authentication core // Configure JDBC authentication core
new JDBCAuthenticationProviderModule(environment, getTunnelService(environment)) new JDBCAuthenticationProviderModule(this, environment,
getTunnelService(environment))
); );
} }
@Override @Override
public UserContext getUserContext(Credentials credentials) public String getIdentifier() {
return "mysql";
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Create AuthenticatedUser based on credentials, if valid
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(this, credentials);
}
@Override
public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException {
// No need to update authenticated users
return authenticatedUser;
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException { throws GuacamoleException {
// Create UserContext based on credentials, if valid // Create UserContext based on credentials, if valid
UserContextService userContextService = injector.getInstance(UserContextService.class); AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return userContextService.getUserContext(credentials); return authProviderService.getUserContext(authenticatedUser);
} }
@Override @Override
public UserContext updateUserContext(UserContext context, public UserContext updateUserContext(UserContext context,
Credentials credentials) throws GuacamoleException { AuthenticatedUser authenticatedUser) throws GuacamoleException {
// No need to update the context // No need to update the context
return context; return context;

View File

@@ -31,9 +31,10 @@ import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule; import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule;
import org.glyptodon.guacamole.auth.jdbc.tunnel.ConfigurableGuacamoleTunnelService; import org.glyptodon.guacamole.auth.jdbc.tunnel.ConfigurableGuacamoleTunnelService;
import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
import org.glyptodon.guacamole.auth.jdbc.user.UserContextService; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticationProviderService;
import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.environment.LocalEnvironment;
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -184,25 +185,50 @@ public class PostgreSQLAuthenticationProvider implements AuthenticationProvider
new PostgreSQLAuthenticationProviderModule(environment), new PostgreSQLAuthenticationProviderModule(environment),
// Configure JDBC authentication core // Configure JDBC authentication core
new JDBCAuthenticationProviderModule(environment, getTunnelService(environment)) new JDBCAuthenticationProviderModule(this, environment,
getTunnelService(environment))
); );
} }
@Override @Override
public UserContext getUserContext(Credentials credentials) public String getIdentifier() {
return "postgresql";
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Create AuthenticatedUser based on credentials, if valid
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(this, credentials);
}
@Override
public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException {
// No need to update authenticated users
return authenticatedUser;
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException { throws GuacamoleException {
// Create UserContext based on credentials, if valid // Create UserContext based on credentials, if valid
UserContextService userContextService = injector.getInstance(UserContextService.class); AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return userContextService.getUserContext(credentials); return authProviderService.getUserContext(authenticatedUser);
} }
@Override @Override
public UserContext updateUserContext(UserContext context, public UserContext updateUserContext(UserContext context,
Credentials credentials) throws GuacamoleException { AuthenticatedUser authenticatedUser) throws GuacamoleException {
// No need to update the context // No need to update the context
return context; return context;

View File

@@ -74,6 +74,11 @@ public class LDAPAuthenticationProvider extends SimpleAuthenticationProvider {
environment = new LocalEnvironment(); environment = new LocalEnvironment();
} }
@Override
public String getIdentifier() {
return "ldap";
}
// Courtesy of OWASP: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java // Courtesy of OWASP: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
private static String escapeLDAPSearchFilter(String filter) { private static String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View File

@@ -122,6 +122,11 @@ public class NoAuthenticationProvider extends SimpleAuthenticationProvider {
environment = new LocalEnvironment(); environment = new LocalEnvironment();
} }
@Override
public String getIdentifier() {
return "noauth";
}
/** /**
* Retrieves the configuration file, as defined within guacamole.properties. * Retrieves the configuration file, as defined within guacamole.properties.
* *

View File

@@ -119,7 +119,8 @@ public class LocalEnvironment implements Environment {
} }
catch (IOException e) { catch (IOException e) {
throw new GuacamoleServerException("Error reading guacamole.properties", e); logger.warn("The guacamole.properties file within GUACAMOLE_HOME cannot be read: {}", e.getMessage());
logger.debug("Error reading guacamole.properties.", e);
} }
// Read all protocols // Read all protocols
@@ -190,11 +191,8 @@ public class LocalEnvironment implements Environment {
* *
* @return * @return
* A map of all available protocols. * A map of all available protocols.
*
* @throws GuacamoleException
* If an error occurs while reading the various protocol JSON files.
*/ */
private Map<String, ProtocolInfo> readProtocols() throws GuacamoleException { private Map<String, ProtocolInfo> readProtocols() {
// Map of all available protocols // Map of all available protocols
Map<String, ProtocolInfo> protocols = new HashMap<String, ProtocolInfo>(); Map<String, ProtocolInfo> protocols = new HashMap<String, ProtocolInfo>();

View File

@@ -0,0 +1,73 @@
/*
* 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.net.auth;
/**
* Basic implementation of an AuthenticatedUser which uses the username to
* determine equality. Username comparison is case-sensitive.
*
* @author Michael Jumper
*/
public abstract class AbstractAuthenticatedUser implements AuthenticatedUser {
/**
* The name of this user.
*/
private String username;
@Override
public String getIdentifier() {
return username;
}
@Override
public void setIdentifier(String username) {
this.username = username;
}
@Override
public int hashCode() {
if (username == null) return 0;
return username.hashCode();
}
@Override
public boolean equals(Object obj) {
// Not equal if null or not a User
if (obj == null) return false;
if (!(obj instanceof AbstractAuthenticatedUser)) return false;
// Get username
String objUsername = ((AbstractAuthenticatedUser) obj).username;
// If null, equal only if this username is null
if (objUsername == null) return username == null;
// Otherwise, equal only if strings are identical
return objUsername.equals(username);
}
}

View File

@@ -1,16 +1,16 @@
/* /*
* Copyright (C) 2014 Glyptodon LLC * Copyright (C) 2015 Glyptodon LLC
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights * in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is * copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions: * furnished to do so, subject to the following conditions:
* *
* The above copyright notice and this permission notice shall be included in * The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software. * all copies or substantial portions of the Software.
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -20,50 +20,32 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package org.glyptodon.guacamole.net.basic.rest.auth; package org.glyptodon.guacamole.net.auth;
/** /**
* A simple object to represent an auth token/userID pair in the API. * A user of the Guacamole web application who has been authenticated by an
* * AuthenticationProvider.
* @author James Muehlner *
* @author Michael Jumper
*/ */
public class APIAuthToken { public interface AuthenticatedUser extends Identifiable {
/**
* The auth token.
*/
private final String authToken;
/**
* The user ID.
*/
private final String userID;
/** /**
* Get the auth token. * Returns the AuthenticationProvider that authenticated this user.
* @return The auth token. *
* @return
* The AuthenticationProvider that authenticated this user.
*/ */
public String getAuthToken() { AuthenticationProvider getAuthenticationProvider();
return authToken;
}
/** /**
* Get the user ID. * Returns the credentials that the user provided when they successfully
* @return The user ID. * authenticated.
*
* @return
* The credentials provided by the user when they authenticated.
*/ */
public String getUserID() { Credentials getCredentials();
return userID;
}
/**
* Create a new APIAuthToken Object with the given auth token.
*
* @param authToken The auth token to create the new APIAuthToken with.
* @param userID The ID of the user owning the given token.
*/
public APIAuthToken(String authToken, String userID) {
this.authToken = authToken;
this.userID = userID;
}
} }

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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -25,46 +25,124 @@ package org.glyptodon.guacamole.net.auth;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
/** /**
* Provides means of accessing and managing the available * Provides means of authorizing users and for accessing and managing data
* GuacamoleConfiguration objects and User objects. Access to each configuration * associated with those users. Access to such data is limited according to the
* and each user is limited by a given Credentials object. * AuthenticationProvider implementation.
* *
* @author Michael Jumper * @author Michael Jumper
*/ */
public interface AuthenticationProvider { public interface AuthenticationProvider {
/** /**
* Returns the UserContext of the user authorized by the given credentials. * Returns the identifier which uniquely and consistently identifies this
* AuthenticationProvider implementation. This identifier may not be null
* and must be unique across all AuthenticationProviders loaded by the
* Guacamole web application.
* *
* @param credentials The credentials to use to retrieve the environment. * @return
* @return The UserContext of the user authorized by the given credentials, * The unique identifier assigned to this AuthenticationProvider, which
* or null if the credentials are not authorized. * may not be null.
*
* @throws GuacamoleException If an error occurs while creating the
* UserContext.
*/ */
UserContext getUserContext(Credentials credentials) String getIdentifier();
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials, if any.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* An AuthenticatedUser representing the user authenticated by the
* given credentials, if any, or null if the credentials are invalid.
*
* @throws GuacamoleException
* If an error occurs while authenticating the user, or if access is
* temporarily, permanently, or conditionally denied, such as if the
* supplied credentials are insufficient or invalid.
*/
AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException; throws GuacamoleException;
/** /**
* Returns a new or updated UserContext for the user authorized by the * Returns a new or updated AuthenticatedUser for the given credentials
* give credentials and having the given existing UserContext. Note that * already having produced the given AuthenticatedUser. Note that because
* because this function will be called for all future requests after * this function will be called for all future requests after initial
* initial authentication, including tunnel requests, care must be taken * authentication, including tunnel requests, care must be taken to avoid
* to avoid using functions of HttpServletRequest which invalidate the * using functions of HttpServletRequest which invalidate the entire request
* entire request body, such as getParameter(). * body, such as getParameter(). Doing otherwise may cause the
* * GuacamoleHTTPTunnelServlet to fail.
* @param context The existing UserContext belonging to the user in *
* question. * @param credentials
* @param credentials The credentials to use to retrieve or update the * The credentials to use for authentication.
* environment. *
* @return The updated UserContext, which need not be the same as the * @param authenticatedUser
* UserContext given, or null if the user is no longer authorized. * An AuthenticatedUser object representing the user authenticated by
* * an arbitrary set of credentials. The AuthenticatedUser may come from
* @throws GuacamoleException If an error occurs while updating the * this AuthenticationProvider or any other installed
* UserContext. * AuthenticationProvider.
*
* @return
* An updated AuthenticatedUser representing the user authenticated by
* the given credentials, if any, or null if the credentials are
* invalid.
*
* @throws GuacamoleException
* If an error occurs while updating the AuthenticatedUser.
*/ */
UserContext updateUserContext(UserContext context, Credentials credentials) AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException;
/**
* Returns the UserContext of the user authenticated by the given
* credentials.
*
* @param authenticatedUser
* An AuthenticatedUser object representing the user authenticated by
* an arbitrary set of credentials. The AuthenticatedUser may come from
* this AuthenticationProvider or any other installed
* AuthenticationProvider.
*
* @return
* A UserContext describing the permissions, connection, connection
* groups, etc. accessible or associated with the given authenticated
* user, or null if this AuthenticationProvider refuses to provide any
* such data.
*
* @throws GuacamoleException
* If an error occurs while creating the UserContext.
*/
UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException; throws GuacamoleException;
/**
* Returns a new or updated UserContext for the given AuthenticatedUser
* already having the given UserContext. Note that because this function
* will be called for all future requests after initial authentication,
* including tunnel requests, care must be taken to avoid using functions
* of HttpServletRequest which invalidate the entire request body, such as
* getParameter(). Doing otherwise may cause the GuacamoleHTTPTunnelServlet
* to fail.
*
* @param context
* The existing UserContext belonging to the user in question.
*
* @param authenticatedUser
* An AuthenticatedUser object representing the user authenticated by
* an arbitrary set of credentials. The AuthenticatedUser may come from
* this AuthenticationProvider or any other installed
* AuthenticationProvider.
*
* @return
* An updated UserContext describing the permissions, connection,
* connection groups, etc. accessible or associated with the given
* authenticated user, or null if this AuthenticationProvider refuses
* to provide any such data.
*
* @throws GuacamoleException
* If an error occurs while updating the UserContext.
*/
UserContext updateUserContext(UserContext context,
AuthenticatedUser authenticatedUser) throws GuacamoleException;
} }

View File

@@ -43,6 +43,16 @@ public interface UserContext {
*/ */
User self(); User self();
/**
* Returns the AuthenticationProvider which created this UserContext, which
* may not be the same AuthenticationProvider that authenticated the user
* associated with this UserContext.
*
* @return
* The AuthenticationProvider that created this UserContext.
*/
AuthenticationProvider getAuthenticationProvider();
/** /**
* Retrieves a Directory which can be used to view and manipulate other * Retrieves a Directory which can be used to view and manipulate other
* users, but only as allowed by the permissions given to the user of this * users, but only as allowed by the permissions given to the user of this

View File

@@ -23,8 +23,11 @@
package org.glyptodon.guacamole.net.auth.simple; package org.glyptodon.guacamole.net.auth.simple;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.AbstractAuthenticatedUser;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
@@ -62,12 +65,100 @@ public abstract class SimpleAuthenticationProvider
getAuthorizedConfigurations(Credentials credentials) getAuthorizedConfigurations(Credentials credentials)
throws GuacamoleException; throws GuacamoleException;
@Override /**
public UserContext getUserContext(Credentials credentials) * AuthenticatedUser which contains its own predefined set of authorized
throws GuacamoleException { * configurations.
*
* @author Michael Jumper
*/
private class SimpleAuthenticatedUser extends AbstractAuthenticatedUser {
// Get username, if any /**
String username = credentials.getUsername(); * The credentials provided when this AuthenticatedUser was
* authenticated.
*/
private final Credentials credentials;
/**
* The GuacamoleConfigurations that this AuthenticatedUser is
* authorized to use.
*/
private final Map<String, GuacamoleConfiguration> configs;
/**
* Creates a new SimpleAuthenticatedUser associated with the given
* credentials and having access to the given Map of
* GuacamoleConfigurations.
*
* @param credentials
* The credentials provided by the user when they authenticated.
*
* @param configs
* A Map of all GuacamoleConfigurations for which this user has
* access. The keys of this Map are Strings which uniquely identify
* each configuration.
*/
public SimpleAuthenticatedUser(Credentials credentials, Map<String, GuacamoleConfiguration> configs) {
// Store credentials and configurations
this.credentials = credentials;
this.configs = configs;
// Pull username from credentials if it exists
String username = credentials.getUsername();
if (username != null && !username.isEmpty())
setIdentifier(username);
// Otherwise generate a random username
else
setIdentifier(UUID.randomUUID().toString());
}
/**
* Returns a Map containing all GuacamoleConfigurations that this user
* is authorized to use. The keys of this Map are Strings which
* uniquely identify each configuration.
*
* @return
* A Map of all configurations for which this user is authorized.
*/
public Map<String, GuacamoleConfiguration> getAuthorizedConfigurations() {
return configs;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return SimpleAuthenticationProvider.this;
}
@Override
public Credentials getCredentials() {
return credentials;
}
}
/**
* Given an arbitrary credentials object, returns a Map containing all
* configurations authorized by those credentials, filtering those
* configurations using a TokenFilter and the standard credential tokens
* (like ${GUAC_USERNAME} and ${GUAC_PASSWORD}). The keys of this Map
* are Strings which uniquely identify each configuration.
*
* @param credentials
* The credentials to use to retrieve authorized configurations.
*
* @return
* A Map of all configurations authorized by the given credentials, or
* null if the credentials given are not authorized.
*
* @throws GuacamoleException
* If an error occurs while retrieving configurations.
*/
private Map<String, GuacamoleConfiguration>
getFilteredAuthorizedConfigurations(Credentials credentials)
throws GuacamoleException {
// Get configurations // Get configurations
Map<String, GuacamoleConfiguration> configs = Map<String, GuacamoleConfiguration> configs =
@@ -80,24 +171,90 @@ public abstract class SimpleAuthenticationProvider
// Build credential TokenFilter // Build credential TokenFilter
TokenFilter tokenFilter = new TokenFilter(); TokenFilter tokenFilter = new TokenFilter();
StandardTokens.addStandardTokens(tokenFilter, credentials); StandardTokens.addStandardTokens(tokenFilter, credentials);
// Filter each configuration // Filter each configuration
for (GuacamoleConfiguration config : configs.values()) for (GuacamoleConfiguration config : configs.values())
tokenFilter.filterValues(config.getParameters()); tokenFilter.filterValues(config.getParameters());
// Return user context restricted to authorized configs
if (username != null && !username.isEmpty())
return new SimpleUserContext(username, configs);
// If there is no associated username, let SimpleUserContext generate one return configs;
else
return new SimpleUserContext(configs); }
/**
* Given a user who has already been authenticated, returns a Map
* containing all configurations for which that user is authorized,
* filtering those configurations using a TokenFilter and the standard
* credential tokens (like ${GUAC_USERNAME} and ${GUAC_PASSWORD}). The keys
* of this Map are Strings which uniquely identify each configuration.
*
* @param authenticatedUser
* The user whose authorized configurations are to be retrieved.
*
* @return
* A Map of all configurations authorized for use by the given user, or
* null if the user is not authorized to use any configurations.
*
* @throws GuacamoleException
* If an error occurs while retrieving configurations.
*/
private Map<String, GuacamoleConfiguration>
getFilteredAuthorizedConfigurations(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Pull cached configurations, if any
if (authenticatedUser instanceof SimpleAuthenticatedUser)
return ((SimpleAuthenticatedUser) authenticatedUser).getAuthorizedConfigurations();
// Otherwise, pull using credentials
return getFilteredAuthorizedConfigurations(authenticatedUser.getCredentials());
}
@Override
public AuthenticatedUser authenticateUser(final Credentials credentials)
throws GuacamoleException {
// Get configurations
Map<String, GuacamoleConfiguration> configs =
getFilteredAuthorizedConfigurations(credentials);
// Return as unauthorized if not authorized to retrieve configs
if (configs == null)
return null;
return new SimpleAuthenticatedUser(credentials, configs);
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Get configurations
Map<String, GuacamoleConfiguration> configs =
getFilteredAuthorizedConfigurations(authenticatedUser);
// Return as unauthorized if not authorized to retrieve configs
if (configs == null)
return null;
// Return user context restricted to authorized configs
return new SimpleUserContext(this, authenticatedUser.getIdentifier(), configs);
}
@Override
public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException {
// Simply return the given user, updating nothing
return authenticatedUser;
} }
@Override @Override
public UserContext updateUserContext(UserContext context, public UserContext updateUserContext(UserContext context,
Credentials credentials) throws GuacamoleException { AuthenticatedUser authorizedUser) throws GuacamoleException {
// Simply return the given context, updating nothing // Simply return the given context, updating nothing
return context; return context;

View File

@@ -30,6 +30,7 @@ import java.util.UUID;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.Form;
import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.ActiveConnection;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Connection;
import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Directory;
@@ -50,7 +51,12 @@ public class SimpleUserContext implements UserContext {
* The unique identifier of the root connection group. * The unique identifier of the root connection group.
*/ */
private static final String ROOT_IDENTIFIER = "ROOT"; private static final String ROOT_IDENTIFIER = "ROOT";
/**
* The AuthenticationProvider that created this UserContext.
*/
private final AuthenticationProvider authProvider;
/** /**
* Reference to the user whose permissions dictate the configurations * Reference to the user whose permissions dictate the configurations
* accessible within this UserContext. * accessible within this UserContext.
@@ -84,24 +90,35 @@ public class SimpleUserContext implements UserContext {
* Creates a new SimpleUserContext which provides access to only those * Creates a new SimpleUserContext which provides access to only those
* configurations within the given Map. The username is assigned * configurations within the given Map. The username is assigned
* arbitrarily. * arbitrarily.
* *
* @param configs A Map of all configurations for which the user associated * @param authProvider
* with this UserContext has read access. * The AuthenticationProvider creating this UserContext.
*
* @param configs
* A Map of all configurations for which the user associated with this
* UserContext has read access.
*/ */
public SimpleUserContext(Map<String, GuacamoleConfiguration> configs) { public SimpleUserContext(AuthenticationProvider authProvider,
this(UUID.randomUUID().toString(), configs); Map<String, GuacamoleConfiguration> configs) {
this(authProvider, UUID.randomUUID().toString(), configs);
} }
/** /**
* Creates a new SimpleUserContext for the user with the given username * Creates a new SimpleUserContext for the user with the given username
* which provides access to only those configurations within the given Map. * which provides access to only those configurations within the given Map.
* *
* @param username The username of the user associated with this * @param authProvider
* UserContext. * The AuthenticationProvider creating this UserContext.
* @param configs A Map of all configurations for which the user associated *
* with this UserContext has read access. * @param username
* The username of the user associated with this UserContext.
*
* @param configs
* A Map of all configurations for which the user associated with
* this UserContext has read access.
*/ */
public SimpleUserContext(String username, Map<String, GuacamoleConfiguration> configs) { public SimpleUserContext(AuthenticationProvider authProvider,
String username, Map<String, GuacamoleConfiguration> configs) {
Collection<String> connectionIdentifiers = new ArrayList<String>(configs.size()); Collection<String> connectionIdentifiers = new ArrayList<String>(configs.size());
Collection<String> connectionGroupIdentifiers = Collections.singleton(ROOT_IDENTIFIER); Collection<String> connectionGroupIdentifiers = Collections.singleton(ROOT_IDENTIFIER);
@@ -138,7 +155,10 @@ public class SimpleUserContext implements UserContext {
this.userDirectory = new SimpleUserDirectory(self); this.userDirectory = new SimpleUserDirectory(self);
this.connectionDirectory = new SimpleConnectionDirectory(connections); this.connectionDirectory = new SimpleConnectionDirectory(connections);
this.connectionGroupDirectory = new SimpleConnectionGroupDirectory(Collections.singleton(this.rootGroup)); this.connectionGroupDirectory = new SimpleConnectionGroupDirectory(Collections.singleton(this.rootGroup));
// Associate provided AuthenticationProvider
this.authProvider = authProvider;
} }
@Override @Override
@@ -146,6 +166,11 @@ public class SimpleUserContext implements UserContext {
return self; return self;
} }
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override @Override
public Directory<User> getUserDirectory() public Directory<User> getUserDirectory()
throws GuacamoleException { throws GuacamoleException {

View File

@@ -36,7 +36,7 @@ import org.glyptodon.guacamole.net.auth.simple.SimpleAuthenticationProvider;
import org.glyptodon.guacamole.net.basic.auth.Authorization; import org.glyptodon.guacamole.net.basic.auth.Authorization;
import org.glyptodon.guacamole.net.basic.auth.UserMapping; import org.glyptodon.guacamole.net.basic.auth.UserMapping;
import org.glyptodon.guacamole.xml.DocumentHandler; import org.glyptodon.guacamole.xml.DocumentHandler;
import org.glyptodon.guacamole.net.basic.xml.user_mapping.UserMappingTagHandler; import org.glyptodon.guacamole.net.basic.xml.usermapping.UserMappingTagHandler;
import org.glyptodon.guacamole.properties.FileGuacamoleProperty; import org.glyptodon.guacamole.properties.FileGuacamoleProperty;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -58,17 +58,19 @@ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvide
/** /**
* Logger for this class. * Logger for this class.
*/ */
private Logger logger = LoggerFactory.getLogger(BasicFileAuthenticationProvider.class); private final Logger logger = LoggerFactory.getLogger(BasicFileAuthenticationProvider.class);
/** /**
* The time the user mapping file was last modified. * The time the user mapping file was last modified. If the file has never
* been read, and thus no modification time exists, this will be
* Long.MIN_VALUE.
*/ */
private long mod_time; private long lastModified = Long.MIN_VALUE;
/** /**
* The parsed UserMapping read when the user mapping file was last parsed. * The parsed UserMapping read when the user mapping file was last parsed.
*/ */
private UserMapping user_mapping; private UserMapping cachedUserMapping;
/** /**
* Guacamole server environment. * Guacamole server environment.
@@ -103,30 +105,48 @@ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvide
environment = new LocalEnvironment(); environment = new LocalEnvironment();
} }
@Override
public String getIdentifier() {
return "default";
}
/** /**
* Returns a UserMapping containing all authorization data given within * Returns a UserMapping containing all authorization data given within
* the XML file specified by the "basic-user-mapping" property in * the XML file specified by the "basic-user-mapping" property in
* guacamole.properties. If the XML file has been modified or has not yet * guacamole.properties. If the XML file has been modified or has not yet
* been read, this function may reread the file. * been read, this function may reread the file.
* *
* @return A UserMapping containing all authorization data within the * @return
* user mapping XML file. * A UserMapping containing all authorization data within the user
* @throws GuacamoleException If the user mapping property is missing or * mapping XML file, or null if the file cannot be found/parsed.
* an error occurs while parsing the XML file.
*/ */
private UserMapping getUserMapping() throws GuacamoleException { private UserMapping getUserMapping() {
// Get user mapping file, defaulting to GUACAMOLE_HOME/user-mapping.xml // Get user mapping file, defaulting to GUACAMOLE_HOME/user-mapping.xml
File user_mapping_file = environment.getProperty(BASIC_USER_MAPPING); File userMappingFile;
if (user_mapping_file == null) try {
user_mapping_file = new File(environment.getGuacamoleHome(), DEFAULT_USER_MAPPING); userMappingFile = environment.getProperty(BASIC_USER_MAPPING);
if (userMappingFile == null)
userMappingFile = new File(environment.getGuacamoleHome(), DEFAULT_USER_MAPPING);
}
// If user_mapping not yet read, or user_mapping has been modified, reread // Abort if property cannot be parsed
if (user_mapping == null || catch (GuacamoleException e) {
(user_mapping_file.exists() logger.warn("Unable to read user mapping filename from properties: {}", e.getMessage());
&& mod_time < user_mapping_file.lastModified())) { logger.debug("Error parsing user mapping property.", e);
return null;
}
logger.debug("Reading user mapping file: \"{}\"", user_mapping_file); // Abort if user mapping does not exist
if (!userMappingFile.exists()) {
logger.debug("User mapping file \"{}\" does not exist and will not be read.", userMappingFile);
return null;
}
// Refresh user mapping if file has changed
if (lastModified < userMappingFile.lastModified()) {
logger.debug("Reading user mapping file: \"{}\"", userMappingFile);
// Parse document // Parse document
try { try {
@@ -144,26 +164,34 @@ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvide
parser.setContentHandler(contentHandler); parser.setContentHandler(contentHandler);
// Read and parse file // Read and parse file
InputStream input = new BufferedInputStream(new FileInputStream(user_mapping_file)); InputStream input = new BufferedInputStream(new FileInputStream(userMappingFile));
parser.parse(new InputSource(input)); parser.parse(new InputSource(input));
input.close(); input.close();
// Store mod time and user mapping // Store mod time and user mapping
mod_time = user_mapping_file.lastModified(); lastModified = userMappingFile.lastModified();
user_mapping = userMappingHandler.asUserMapping(); cachedUserMapping = userMappingHandler.asUserMapping();
} }
// If the file is unreadable, return no mapping
catch (IOException e) { catch (IOException e) {
throw new GuacamoleException("Error reading basic user mapping file.", e); logger.warn("Unable to read user mapping file \"{}\": {}", userMappingFile, e.getMessage());
logger.debug("Error reading user mapping file.", e);
return null;
} }
// If the file cannot be parsed, return no mapping
catch (SAXException e) { catch (SAXException e) {
throw new GuacamoleException("Error parsing basic user mapping XML.", e); logger.warn("User mapping file \"{}\" is not valid: {}", userMappingFile, e.getMessage());
logger.debug("Error parsing user mapping file.", e);
return null;
} }
} }
// Return (possibly cached) user mapping // Return (possibly cached) user mapping
return user_mapping; return cachedUserMapping;
} }
@@ -172,6 +200,11 @@ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvide
getAuthorizedConfigurations(Credentials credentials) getAuthorizedConfigurations(Credentials credentials)
throws GuacamoleException { throws GuacamoleException {
// Abort authorization if no user mapping exists
UserMapping userMapping = getUserMapping();
if (userMapping == null)
return null;
// Validate and return info for given user and pass // Validate and return info for given user and pass
Authorization auth = getUserMapping().getAuthorization(credentials.getUsername()); Authorization auth = getUserMapping().getAuthorization(credentials.getUsername());
if (auth != null && auth.validate(credentials.getUsername(), credentials.getPassword())) if (auth != null && auth.validate(credentials.getUsername(), credentials.getPassword()))

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2014 Glyptodon LLC * Copyright (C) 2015 Glyptodon LLC
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -22,18 +22,15 @@
package org.glyptodon.guacamole.net.basic; package org.glyptodon.guacamole.net.basic;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleTunnel;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -51,19 +48,15 @@ public class GuacamoleSession {
private static final Logger logger = LoggerFactory.getLogger(GuacamoleSession.class); private static final Logger logger = LoggerFactory.getLogger(GuacamoleSession.class);
/** /**
* The credentials provided when the user authenticated. * The user associated with this session.
*/ */
private Credentials credentials; private AuthenticatedUser authenticatedUser;
/** /**
* The user context associated with this session. * All UserContexts associated with this session. Each
* AuthenticationProvider may provide its own UserContext.
*/ */
private UserContext userContext; private List<UserContext> userContexts;
/**
* The current clipboard state.
*/
private final ClipboardState clipboardState = new ClipboardState();
/** /**
* All currently-active tunnels, indexed by tunnel UUID. * All currently-active tunnels, indexed by tunnel UUID.
@@ -76,80 +69,110 @@ public class GuacamoleSession {
private long lastAccessedTime; private long lastAccessedTime;
/** /**
* Creates a new Guacamole session associated with the given user context. * Creates a new Guacamole session associated with the given
* AuthenticatedUser and UserContexts.
* *
* @param environment * @param environment
* The environment of the Guacamole server associated with this new * The environment of the Guacamole server associated with this new
* session. * session.
* *
* @param credentials * @param authenticatedUser
* The credentials provided by the user during login. * The authenticated user to associate this session with.
* *
* @param userContext * @param userContexts
* The user context to associate this session with. * The List of UserContexts to associate with this session.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If an error prevents the session from being created. * If an error prevents the session from being created.
*/ */
public GuacamoleSession(Environment environment, Credentials credentials, public GuacamoleSession(Environment environment,
UserContext userContext) throws GuacamoleException { AuthenticatedUser authenticatedUser,
List<UserContext> userContexts)
throws GuacamoleException {
this.lastAccessedTime = System.currentTimeMillis(); this.lastAccessedTime = System.currentTimeMillis();
this.credentials = credentials; this.authenticatedUser = authenticatedUser;
this.userContext = userContext; this.userContexts = userContexts;
} }
/** /**
* Returns the credentials used when the user associated with this session * Returns the authenticated user associated with this session.
* authenticated.
* *
* @return * @return
* The credentials used when the user associated with this session * The authenticated user associated with this session.
* authenticated.
*/ */
public Credentials getCredentials() { public AuthenticatedUser getAuthenticatedUser() {
return credentials; return authenticatedUser;
} }
/** /**
* Replaces the credentials associated with this session with the given * Replaces the authenticated user associated with this session with the
* credentials. * given authenticated user.
* *
* @param credentials * @param authenticatedUser
* The credentials to associate with this session. * The authenticated user to associated with this session.
*/ */
public void setCredentials(Credentials credentials) { public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) {
this.credentials = credentials; this.authenticatedUser = authenticatedUser;
} }
/** /**
* Returns the UserContext associated with this session. * Returns the UserContext associated with this session.
* *
* @return The UserContext associated with this session. * @return The UserContext associated with this session.
*/ */
public UserContext getUserContext() { public UserContext getUserContext() {
return userContext;
// Warn of deprecation
logger.debug(
"\n****************************************************************"
+ "\n"
+ "\n !!!! PLEASE DO NOT USE getUserContext() !!!!"
+ "\n"
+ "\n getUserContext() has been replaced by getUserContexts(), which"
+ "\n properly handles multiple authentication providers. All use of"
+ "\n the old getUserContext() must be removed before GUAC-586 can"
+ "\n be considered complete."
+ "\n"
+ "\n****************************************************************"
);
// Return the UserContext associated with the AuthenticationProvider
// that authenticated the current user.
String authProviderIdentifier = authenticatedUser.getAuthenticationProvider().getIdentifier();
for (UserContext userContext : userContexts) {
if (userContext.getAuthenticationProvider().getIdentifier().equals(authProviderIdentifier))
return userContext;
}
// If not found, return null
return null;
} }
/** /**
* Replaces the user context associated with this session with the given * Returns a list of all UserContexts associated with this session. Each
* user context. * AuthenticationProvider currently loaded by Guacamole may provide its own
* UserContext for any successfully-authenticated user.
* *
* @param userContext * @return
* The user context to associate with this session. * An unmodifiable list of all UserContexts associated with this
* session.
*/ */
public void setUserContext(UserContext userContext) { public List<UserContext> getUserContexts() {
this.userContext = userContext; return Collections.unmodifiableList(userContexts);
}
/**
* Replaces all UserContexts associated with this session with the given
* List of UserContexts.
*
* @param userContexts
* The List of UserContexts to associate with this session.
*/
public void setUserContexts(List<UserContext> userContexts) {
this.userContexts = userContexts;
} }
/**
* Returns the ClipboardState associated with this session.
*
* @return The ClipboardState associated with this session.
*/
public ClipboardState getClipboardState() {
return clipboardState;
}
/** /**
* Returns whether this session has any associated active tunnels. * Returns whether this session has any associated active tunnels.
* *

View File

@@ -1,124 +0,0 @@
/*
* Copyright (C) 2014 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.net.basic;
import java.util.List;
import javax.xml.bind.DatatypeConverter;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.io.GuacamoleReader;
import org.glyptodon.guacamole.protocol.GuacamoleInstruction;
/**
* GuacamoleReader implementation which watches for specific instructions,
* maintaining state based on the observed instructions.
*
* @author Michael Jumper
*/
public class MonitoringGuacamoleReader implements GuacamoleReader {
/**
* The underlying GuacamoleReader.
*/
private final GuacamoleReader reader;
/**
* Collection of all listeners which will receive events.
*/
private final ClipboardState clipboard;
/**
* The index of the clipboard stream, if any.
*/
private String clipboard_stream_index = null;
/**
* Creates a new MonitoringGuacamoleReader which watches the instructions
* read by the given GuacamoleReader, firing events when specific
* instructions are seen.
*
* @param clipboard The clipboard state to maintain.
* @param reader The reader to observe.
*/
public MonitoringGuacamoleReader(ClipboardState clipboard,
GuacamoleReader reader) {
this.clipboard = clipboard;
this.reader = reader;
}
@Override
public boolean available() throws GuacamoleException {
return reader.available();
}
@Override
public char[] read() throws GuacamoleException {
// Read single instruction, handle end-of-stream
GuacamoleInstruction instruction = readInstruction();
if (instruction == null)
return null;
return instruction.toString().toCharArray();
}
@Override
public GuacamoleInstruction readInstruction() throws GuacamoleException {
// Read single instruction, handle end-of-stream
GuacamoleInstruction instruction = reader.readInstruction();
if (instruction == null)
return null;
// If clipboard changing, reset clipboard state
if (instruction.getOpcode().equals("clipboard")) {
List<String> args = instruction.getArgs();
if (args.size() >= 2) {
clipboard_stream_index = args.get(0);
clipboard.begin(args.get(1));
}
}
// Add clipboard blobs to existing streams
else if (instruction.getOpcode().equals("blob")) {
List<String> args = instruction.getArgs();
if (args.size() >= 2 && args.get(0).equals(clipboard_stream_index)) {
String base64 = args.get(1);
clipboard.append(DatatypeConverter.parseBase64Binary(base64));
}
}
// Terminate and update clipboard at end of stream
else if (instruction.getOpcode().equals("end")) {
List<String> args = instruction.getArgs();
if (args.size() >= 1 && args.get(0).equals(clipboard_stream_index)) {
clipboard.commit();
clipboard_stream_index = null;
}
}
return instruction;
}
}

View File

@@ -24,13 +24,11 @@ package org.glyptodon.guacamole.net.basic;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.glyptodon.guacamole.net.basic.rest.clipboard.ClipboardRESTService;
import java.util.List; import java.util.List;
import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleClientException;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleSecurityException;
import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.io.GuacamoleReader;
import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel; import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel;
import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleTunnel;
import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Connection;
@@ -234,28 +232,6 @@ public class TunnelRequestService {
*/ */
private final long connectionStartTime = System.currentTimeMillis(); private final long connectionStartTime = System.currentTimeMillis();
@Override
public GuacamoleReader acquireReader() {
// Monitor instructions which pertain to server-side events, if necessary
try {
if (environment.getProperty(ClipboardRESTService.INTEGRATION_ENABLED, false)) {
ClipboardState clipboard = session.getClipboardState();
return new MonitoringGuacamoleReader(clipboard, super.acquireReader());
}
}
catch (GuacamoleException e) {
logger.warn("Clipboard integration failed to initialize: {}", e.getMessage());
logger.debug("Error setting up clipboard integration.", e);
}
// Pass through by default.
return super.acquireReader();
}
@Override @Override
public void close() throws GuacamoleException { public void close() throws GuacamoleException {
@@ -268,13 +244,13 @@ public class TunnelRequestService {
// Connection identifiers // Connection identifiers
case CONNECTION: case CONNECTION:
logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds", logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds",
session.getUserContext().self().getIdentifier(), id, duration); session.getAuthenticatedUser().getIdentifier(), id, duration);
break; break;
// Connection group identifiers // Connection group identifiers
case CONNECTION_GROUP: case CONNECTION_GROUP:
logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds", logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds",
session.getUserContext().self().getIdentifier(), id, duration); session.getAuthenticatedUser().getIdentifier(), id, duration);
break; break;
// Type is guaranteed to be one of the above // Type is guaranteed to be one of the above

View File

@@ -23,7 +23,9 @@
package org.glyptodon.guacamole.net.basic.extension; package org.glyptodon.guacamole.net.basic.extension;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.UUID;
import org.glyptodon.guacamole.GuacamoleException; 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.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
@@ -52,6 +54,12 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
*/ */
private final AuthenticationProvider authProvider; private final AuthenticationProvider authProvider;
/**
* The identifier to provide for the underlying authentication provider if
* the authentication provider could not be loaded.
*/
private final String facadeIdentifier = UUID.randomUUID().toString();
/** /**
* Creates a new AuthenticationProviderFacade which delegates all function * Creates a new AuthenticationProviderFacade which delegates all function
* calls to an instance of the given AuthenticationProvider subclass. If * calls to an instance of the given AuthenticationProvider subclass. If
@@ -118,7 +126,21 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
} }
@Override @Override
public UserContext getUserContext(Credentials credentials) public String getIdentifier() {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("The authentication system could not be loaded. Please check for errors earlier in the logs.");
return facadeIdentifier;
}
// Delegate to underlying auth provider
return authProvider.getIdentifier();
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException { throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded // Ignore auth attempts if no auth provider could be loaded
@@ -128,13 +150,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
} }
// Delegate to underlying auth provider // Delegate to underlying auth provider
return authProvider.getUserContext(credentials); return authProvider.authenticateUser(credentials);
} }
@Override @Override
public UserContext updateUserContext(UserContext context, Credentials credentials) public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
throws GuacamoleException { Credentials credentials) throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded // Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) { if (authProvider == null) {
@@ -143,7 +165,38 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
} }
// Delegate to underlying auth provider // Delegate to underlying auth provider
return authProvider.updateUserContext(context, credentials); return authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("User data retrieval attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
return null;
}
// Delegate to underlying auth provider
return authProvider.getUserContext(authenticatedUser);
}
@Override
public UserContext updateUserContext(UserContext context,
AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("User data refresh attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
return null;
}
// Delegate to underlying auth provider
return authProvider.updateUserContext(context, authenticatedUser);
} }

View File

@@ -22,6 +22,7 @@
package org.glyptodon.guacamole.net.basic.extension; package org.glyptodon.guacamole.net.basic.extension;
import com.google.inject.Provides;
import com.google.inject.servlet.ServletModule; import com.google.inject.servlet.ServletModule;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
@@ -91,10 +92,10 @@ public class ExtensionModule extends ServletModule {
private final Environment environment; private final Environment environment;
/** /**
* The currently-bound authentication provider, if any. At the moment, we * All currently-bound authentication providers, if any.
* only support one authentication provider loaded at any one time.
*/ */
private Class<? extends AuthenticationProvider> boundAuthenticationProvider = null; private final List<AuthenticationProvider> boundAuthenticationProviders =
new ArrayList<AuthenticationProvider>();
/** /**
* Service for adding and retrieving language resources. * Service for adding and retrieving language resources.
@@ -179,40 +180,24 @@ public class ExtensionModule extends ServletModule {
/** /**
* Binds the given AuthenticationProvider class such that any service * Binds the given AuthenticationProvider class such that any service
* requiring access to the AuthenticationProvider can obtain it via * requiring access to the AuthenticationProvider can obtain it via
* injection. * injection, along with any other bound AuthenticationProviders.
* *
* @param authenticationProvider * @param authenticationProvider
* The AuthenticationProvider class to bind. * The AuthenticationProvider class to bind.
*/ */
private void bindAuthenticationProvider(Class<? extends AuthenticationProvider> authenticationProvider) { private void bindAuthenticationProvider(Class<? extends AuthenticationProvider> authenticationProvider) {
// Choose auth provider for binding if not already chosen
if (boundAuthenticationProvider == null)
boundAuthenticationProvider = authenticationProvider;
// If an auth provider is already chosen, skip and warn
else {
logger.debug("Ignoring AuthenticationProvider \"{}\".", authenticationProvider);
logger.warn("Only one authentication extension may be used at a time. Please "
+ "make sure that only one authentication extension is present "
+ "within the GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " "
+ "directory, and that you are not also specifying the deprecated "
+ "\"auth-provider\" property within guacamole.properties.");
return;
}
// Bind authentication provider // Bind authentication provider
logger.debug("Binding AuthenticationProvider \"{}\".", authenticationProvider); logger.debug("[{}] Binding AuthenticationProvider \"{}\".",
bind(AuthenticationProvider.class).toInstance(new AuthenticationProviderFacade(authenticationProvider)); boundAuthenticationProviders.size(), authenticationProvider.getName());
boundAuthenticationProviders.add(new AuthenticationProviderFacade(authenticationProvider));
} }
/** /**
* Binds each of the the given AuthenticationProvider classes such that any * Binds each of the the given AuthenticationProvider classes such that any
* service requiring access to the AuthenticationProvider can obtain it via * service requiring access to the AuthenticationProvider can obtain it via
* injection. Note that, as multiple simultaneous authentication providers * injection.
* are not currently supported, attempting to bind more than one
* authentication provider will result in warnings being logged.
* *
* @param authProviders * @param authProviders
* The AuthenticationProvider classes to bind. * The AuthenticationProvider classes to bind.
@@ -225,6 +210,18 @@ public class ExtensionModule extends ServletModule {
} }
/**
* Returns a list of all currently-bound AuthenticationProvider instances.
*
* @return
* A List of all currently-bound AuthenticationProvider. The List is
* not modifiable.
*/
@Provides
public List<AuthenticationProvider> getAuthenticationProviders() {
return Collections.unmodifiableList(boundAuthenticationProviders);
}
/** /**
* Serves each of the given resources as a language resource. Language * Serves each of the given resources as a language resource. Language
* resources are served from within the "/translations" directory as JSON * resources are served from within the "/translations" directory as JSON
@@ -415,11 +412,8 @@ public class ExtensionModule extends ServletModule {
// Load all extensions // Load all extensions
loadExtensions(javaScriptResources, cssResources); loadExtensions(javaScriptResources, cssResources);
// Bind basic auth if nothing else chosen/provided // Always bind basic auth last
if (boundAuthenticationProvider == null) { bindAuthenticationProvider(BasicFileAuthenticationProvider.class);
logger.info("Using default, \"basic\", XML-driven authentication.");
bindAuthenticationProvider(BasicFileAuthenticationProvider.class);
}
// Dynamically generate app.js and app.css from extensions // Dynamically generate app.js and app.css from extensions
serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources))); serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources)));

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2014 Glyptodon LLC * Copyright (C) 2015 Glyptodon LLC
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -22,6 +22,7 @@
package org.glyptodon.guacamole.net.basic.rest; package org.glyptodon.guacamole.net.basic.rest;
import java.util.List;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException;
import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Connection;
@@ -29,6 +30,7 @@ import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.basic.GuacamoleSession;
import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup;
/** /**
@@ -39,6 +41,43 @@ import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup
*/ */
public class ObjectRetrievalService { public class ObjectRetrievalService {
/**
* Retrieves a single UserContext from the given GuacamoleSession, which
* may contain multiple UserContexts.
*
* @param session
* The GuacamoleSession to retrieve the UserContext from.
*
* @param identifier
* The unique identifier of the AuthenticationProvider that created the
* UserContext being retrieved. Only one UserContext per
* AuthenticationProvider can exist.
*
* @return
* The UserContext that was created by the AuthenticationProvider
* having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the UserContext, or if the
* UserContext does not exist.
*/
public UserContext retrieveUserContext(GuacamoleSession session,
String identifier) throws GuacamoleException {
// Get list of UserContexts
List<UserContext> userContexts = session.getUserContexts();
// Locate and return the UserContext associated with the
// AuthenticationProvider having the given identifier, if any
for (UserContext userContext : userContexts) {
if (userContext.getAuthenticationProvider().getIdentifier().equals(identifier))
return userContext;
}
throw new GuacamoleResourceNotFoundException("Session not associated with authentication provider \"" + identifier + "\".");
}
/** /**
* Retrieves a single user from the given user context. * Retrieves a single user from the given user context.
* *

View File

@@ -28,7 +28,6 @@ import com.google.inject.servlet.ServletModule;
import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; import com.sun.jersey.guice.spi.container.servlet.GuiceContainer;
import org.codehaus.jackson.jaxrs.JacksonJsonProvider; import org.codehaus.jackson.jaxrs.JacksonJsonProvider;
import org.glyptodon.guacamole.net.basic.rest.auth.TokenRESTService; import org.glyptodon.guacamole.net.basic.rest.auth.TokenRESTService;
import org.glyptodon.guacamole.net.basic.rest.clipboard.ClipboardRESTService;
import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService; import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService;
import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService;
import org.glyptodon.guacamole.net.basic.rest.activeconnection.ActiveConnectionRESTService; import org.glyptodon.guacamole.net.basic.rest.activeconnection.ActiveConnectionRESTService;
@@ -58,7 +57,6 @@ public class RESTServletModule extends ServletModule {
// Set up the API endpoints // Set up the API endpoints
bind(ActiveConnectionRESTService.class); bind(ActiveConnectionRESTService.class);
bind(ClipboardRESTService.class);
bind(ConnectionGroupRESTService.class); bind(ConnectionGroupRESTService.class);
bind(ConnectionRESTService.class); bind(ConnectionRESTService.class);
bind(LanguageRESTService.class); bind(LanguageRESTService.class);

View File

@@ -0,0 +1,133 @@
/*
* 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.net.basic.rest.auth;
import java.util.Collections;
import java.util.List;
/**
* A simple object which describes the result of an authentication operation,
* including the resulting token.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class APIAuthenticationResult {
/**
* The unique token generated for the user that authenticated.
*/
private final String authToken;
/**
* The username of the user that authenticated.
*/
private final String username;
/**
* The unique identifier of the data source from which this user account
* came. Although this user account may exist across several data sources
* (AuthenticationProviders), this will be the unique identifier of the
* AuthenticationProvider that authenticated this user for the current
* session.
*/
private final String dataSource;
/**
* The identifiers of all data sources available to this user.
*/
private final List<String> availableDataSources;
/**
* Returns the unique authentication token which identifies the current
* session.
*
* @return
* The user's authentication token.
*/
public String getAuthToken() {
return authToken;
}
/**
* Returns the user identified by the authentication token associated with
* the current session.
*
* @return
* The user identified by this authentication token.
*/
public String getUsername() {
return username;
}
/**
* Returns the unique identifier of the data source associated with the user
* account associated with the current session.
*
* @return
* The unique identifier of the data source associated with the user
* account associated with the current session.
*/
public String getDataSource() {
return dataSource;
}
/**
* Returns the identifiers of all data sources available to the user
* associated with the current session.
*
* @return
* The identifiers of all data sources available to the user associated
* with the current session.
*/
public List<String> getAvailableDataSources() {
return availableDataSources;
}
/**
* Create a new APIAuthenticationResult object containing the given data.
*
* @param authToken
* The unique token generated for the user that authenticated, to be
* used for the duration of their session.
*
* @param username
* The username of the user owning the given token.
*
* @param dataSource
* The unique identifier of the AuthenticationProvider which
* authenticated the user.
*
* @param availableDataSources
* The unique identifier of all AuthenticationProviders to which the
* user now has access.
*/
public APIAuthenticationResult(String authToken, String username,
String dataSource, List<String> availableDataSources) {
this.authToken = authToken;
this.username = username;
this.dataSource = dataSource;
this.availableDataSources = Collections.unmodifiableList(availableDataSources);
}
}

View File

@@ -23,6 +23,7 @@
package org.glyptodon.guacamole.net.basic.rest.auth; package org.glyptodon.guacamole.net.basic.rest.auth;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.List;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleUnauthorizedException; import org.glyptodon.guacamole.GuacamoleUnauthorizedException;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
@@ -77,5 +78,24 @@ public class AuthenticationService {
public UserContext getUserContext(String authToken) throws GuacamoleException { public UserContext getUserContext(String authToken) throws GuacamoleException {
return getGuacamoleSession(authToken).getUserContext(); return getGuacamoleSession(authToken).getUserContext();
} }
/**
* Returns all UserContexts associated with a given auth token, if the auth
* token represents a currently logged in user. Throws an unauthorized
* error otherwise.
*
* @param authToken
* The auth token to check against the map of logged in users.
*
* @return
* A List of all UserContexts associated with the provided auth token.
*
* @throws GuacamoleException
* If the auth token does not correspond to any logged in user.
*/
public List<UserContext> getUserContexts(String authToken)
throws GuacamoleException {
return getGuacamoleSession(authToken).getUserContexts();
}
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2014 Glyptodon LLC * Copyright (C) 2015 Glyptodon LLC
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +24,8 @@ package org.glyptodon.guacamole.net.basic.rest.auth;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
@@ -37,11 +39,14 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.xml.bind.DatatypeConverter; import javax.xml.bind.DatatypeConverter;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleSecurityException;
import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.GuacamoleSession;
import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIError;
@@ -55,6 +60,7 @@ import org.slf4j.LoggerFactory;
* A service for managing auth tokens via the Guacamole REST API. * A service for managing auth tokens via the Guacamole REST API.
* *
* @author James Muehlner * @author James Muehlner
* @author Michael Jumper
*/ */
@Path("/tokens") @Path("/tokens")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@@ -67,10 +73,11 @@ public class TokenRESTService {
private Environment environment; private Environment environment;
/** /**
* The authentication provider used to authenticate this user. * All configured authentication providers which can be used to
* authenticate users or retrieve data associated with authenticated users.
*/ */
@Inject @Inject
private AuthenticationProvider authProvider; private List<AuthenticationProvider> authProviders;
/** /**
* The map of auth tokens to sessions for the REST endpoints. * The map of auth tokens to sessions for the REST endpoints.
@@ -136,53 +143,26 @@ public class TokenRESTService {
} }
/** /**
* Authenticates a user, generates an auth token, associates that auth token * Returns the credentials associated with the given request, using the
* with the user's UserContext for use by further requests. If an existing * provided username and password.
* token is provided, the authentication procedure will attempt to update *
* or reuse the provided token. * @param request
* * The request to use to derive the credentials.
*
* @param username * @param username
* The username of the user who is to be authenticated. * The username to associate with the credentials, or null if the
* username should be derived from the request.
* *
* @param password * @param password
* The password of the user who is to be authenticated. * The password to associate with the credentials, or null if the
* password should be derived from the request.
* *
* @param token * @return
* An optional existing auth token for the user who is to be * A new Credentials object whose contents have been derived from the
* authenticated. * given request, along with the provided username and password.
*
* @param consumedRequest
* The HttpServletRequest associated with the login attempt. The
* parameters of this request may not be accessible, as the request may
* have been fully consumed by JAX-RS.
*
* @param parameters
* A MultivaluedMap containing all parameters from the given HTTP
* request. All request parameters must be made available through this
* map, even if those parameters are no longer accessible within the
* now-fully-consumed HTTP request.
*
* @return The auth token for the newly logged-in user.
* @throws GuacamoleException If an error prevents successful login.
*/ */
@POST private Credentials getCredentials(HttpServletRequest request,
@AuthProviderRESTExposure String username, String password) {
public APIAuthToken createToken(@FormParam("username") String username,
@FormParam("password") String password,
@FormParam("token") String token,
@Context HttpServletRequest consumedRequest,
MultivaluedMap<String, String> parameters)
throws GuacamoleException {
// Reconstitute the HTTP request with the map of parameters
HttpServletRequest request = new APIRequest(consumedRequest, parameters);
// Pull existing session if token provided
GuacamoleSession existingSession;
if (token != null)
existingSession = tokenSessionMap.get(token);
else
existingSession = null;
// If no username/password given, try Authorization header // If no username/password given, try Authorization header
if (username == null && password == null) { if (username == null && password == null) {
@@ -215,41 +195,151 @@ public class TokenRESTService {
} }
} // end Authorization header fallback } // end Authorization header fallback
// Build credentials // Build credentials
Credentials credentials = new Credentials(); Credentials credentials = new Credentials();
credentials.setUsername(username); credentials.setUsername(username);
credentials.setPassword(password); credentials.setPassword(password);
credentials.setRequest(request); credentials.setRequest(request);
credentials.setSession(request.getSession(true)); credentials.setSession(request.getSession(true));
UserContext userContext;
try {
// Update existing user context if session already exists return credentials;
if (existingSession != null)
userContext = authProvider.updateUserContext(existingSession.getUserContext(), credentials);
/// Otherwise, generate a new user context }
else {
userContext = authProvider.getUserContext(credentials); /**
* Attempts authentication against all AuthenticationProviders, in order,
* using the provided credentials. The first authentication failure takes
* priority, but remaining AuthenticationProviders are attempted. If any
* AuthenticationProvider succeeds, the resulting AuthenticatedUser is
* returned, and no further AuthenticationProviders are tried.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* The AuthenticatedUser given by the highest-priority
* AuthenticationProvider for which the given credentials are valid.
*
* @throws GuacamoleException
* If the given credentials are not valid for any
* AuthenticationProvider, or if an error occurs while authenticating
* the user.
*/
private AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Log successful authentication GuacamoleCredentialsException authFailure = null;
if (userContext != null && logger.isInfoEnabled())
logger.info("User \"{}\" successfully authenticated from {}.",
userContext.self().getIdentifier(), getLoggableAddress(request));
// Attempt authentication against each AuthenticationProvider
for (AuthenticationProvider authProvider : authProviders) {
// Attempt authentication
try {
AuthenticatedUser authenticatedUser = authProvider.authenticateUser(credentials);
if (authenticatedUser != null)
return authenticatedUser;
} }
// Request standard username/password if no user context was produced // First failure takes priority for now
if (userContext == null) catch (GuacamoleCredentialsException e) {
throw new GuacamoleInvalidCredentialsException("Permission Denied.", if (authFailure == null)
CredentialsInfo.USERNAME_PASSWORD); authFailure = e;
}
} }
// If a specific failure occured, rethrow that
if (authFailure != null)
throw authFailure;
// Otherwise, request standard username/password
throw new GuacamoleInvalidCredentialsException(
"Permission Denied.",
CredentialsInfo.USERNAME_PASSWORD
);
}
/**
* Re-authenticates the given AuthenticatedUser against the
* AuthenticationProvider that originally created it, using the given
* Credentials.
*
* @param authenticatedUser
* The AuthenticatedUser to re-authenticate.
*
* @param credentials
* The Credentials to use to re-authenticate the user.
*
* @return
* A AuthenticatedUser which may have been updated due to re-
* authentication.
*
* @throws GuacamoleException
* If an error prevents the user from being re-authenticated.
*/
private AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException {
// Get original AuthenticationProvider
AuthenticationProvider authProvider = authenticatedUser.getAuthenticationProvider();
// Re-authenticate the AuthenticatedUser against the original AuthenticationProvider only
authenticatedUser = authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
if (authenticatedUser == null)
throw new GuacamoleSecurityException("User re-authentication failed.");
return authenticatedUser;
}
/**
* Returns the AuthenticatedUser associated with the given session and
* credentials, performing a fresh authentication and creating a new
* AuthenticatedUser if necessary.
*
* @param existingSession
* The current GuacamoleSession, or null if no session exists yet.
*
* @param credentials
* The Credentials to use to authenticate the user.
*
* @return
* The AuthenticatedUser associated with the given session and
* credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating or re-authenticating the
* user.
*/
private AuthenticatedUser getAuthenticatedUser(GuacamoleSession existingSession,
Credentials credentials) throws GuacamoleException {
try {
// Re-authenticate user if session exists
if (existingSession != null)
return updateAuthenticatedUser(existingSession.getAuthenticatedUser(), credentials);
// Otherwise, attempt authentication as a new user
AuthenticatedUser authenticatedUser = authenticateUser(credentials);
if (logger.isInfoEnabled())
logger.info("User \"{}\" successfully authenticated from {}.",
authenticatedUser.getIdentifier(),
getLoggableAddress(credentials.getRequest()));
return authenticatedUser;
}
// Log and rethrow any authentication errors
catch (GuacamoleException e) { catch (GuacamoleException e) {
// Get request and username for sake of logging
HttpServletRequest request = credentials.getRequest();
String username = credentials.getUsername();
// Log authentication failures with associated usernames // Log authentication failures with associated usernames
if (username != null) { if (username != null) {
if (logger.isWarnEnabled()) if (logger.isWarnEnabled())
@@ -260,27 +350,172 @@ public class TokenRESTService {
// Log anonymous authentication failures // Log anonymous authentication failures
else if (logger.isDebugEnabled()) else if (logger.isDebugEnabled())
logger.debug("Anonymous authentication attempt from {} failed.", logger.debug("Anonymous authentication attempt from {} failed.",
getLoggableAddress(request), username); getLoggableAddress(request));
// Rethrow exception
throw e; throw e;
} }
}
/**
* Returns all UserContexts associated with the given AuthenticatedUser,
* updating existing UserContexts, if any. If no UserContexts are yet
* associated with the given AuthenticatedUser, new UserContexts are
* generated by polling each available AuthenticationProvider.
*
* @param existingSession
* The current GuacamoleSession, or null if no session exists yet.
*
* @param authenticatedUser
* The AuthenticatedUser that has successfully authenticated or re-
* authenticated.
*
* @return
* A List of all UserContexts associated with the given
* AuthenticatedUser.
*
* @throws GuacamoleException
* If an error occurs while creating or updating any UserContext.
*/
private List<UserContext> getUserContexts(GuacamoleSession existingSession,
AuthenticatedUser authenticatedUser) throws GuacamoleException {
List<UserContext> userContexts = new ArrayList<UserContext>(authProviders.size());
// If UserContexts already exist, update them and add to the list
if (existingSession != null) {
// Update all old user contexts
List<UserContext> oldUserContexts = existingSession.getUserContexts();
for (UserContext oldUserContext : oldUserContexts) {
// Update existing UserContext
AuthenticationProvider authProvider = oldUserContext.getAuthenticationProvider();
UserContext userContext = authProvider.updateUserContext(oldUserContext, authenticatedUser);
// Add to available data, if successful
if (userContext != null)
userContexts.add(userContext);
// If unsuccessful, log that this happened, as it may be a bug
else
logger.debug("AuthenticationProvider \"{}\" retroactively destroyed its UserContext.",
authProvider.getClass().getName());
}
}
// Otherwise, create new UserContexts from available AuthenticationProviders
else {
// Get UserContexts from each available AuthenticationProvider
for (AuthenticationProvider authProvider : authProviders) {
// Generate new UserContext
UserContext userContext = authProvider.getUserContext(authenticatedUser);
// Add to available data, if successful
if (userContext != null)
userContexts.add(userContext);
}
}
return userContexts;
}
/**
* Authenticates a user, generates an auth token, associates that auth token
* with the user's UserContext for use by further requests. If an existing
* token is provided, the authentication procedure will attempt to update
* or reuse the provided token.
*
* @param username
* The username of the user who is to be authenticated.
*
* @param password
* The password of the user who is to be authenticated.
*
* @param token
* An optional existing auth token for the user who is to be
* authenticated.
*
* @param consumedRequest
* The HttpServletRequest associated with the login attempt. The
* parameters of this request may not be accessible, as the request may
* have been fully consumed by JAX-RS.
*
* @param parameters
* A MultivaluedMap containing all parameters from the given HTTP
* request. All request parameters must be made available through this
* map, even if those parameters are no longer accessible within the
* now-fully-consumed HTTP request.
*
* @return
* An authentication response object containing the possible-new auth
* token, as well as other related data.
*
* @throws GuacamoleException
* If an error prevents successful authentication.
*/
@POST
@AuthProviderRESTExposure
public APIAuthenticationResult createToken(@FormParam("username") String username,
@FormParam("password") String password,
@FormParam("token") String token,
@Context HttpServletRequest consumedRequest,
MultivaluedMap<String, String> parameters)
throws GuacamoleException {
// Reconstitute the HTTP request with the map of parameters
HttpServletRequest request = new APIRequest(consumedRequest, parameters);
// Pull existing session if token provided
GuacamoleSession existingSession;
if (token != null)
existingSession = tokenSessionMap.get(token);
else
existingSession = null;
// Build credentials from request
Credentials credentials = getCredentials(request, username, password);
// Get up-to-date AuthenticatedUser and associated UserContexts
AuthenticatedUser authenticatedUser = getAuthenticatedUser(existingSession, credentials);
List<UserContext> userContexts = getUserContexts(existingSession, authenticatedUser);
// Update existing session, if it exists // Update existing session, if it exists
String authToken; String authToken;
if (existingSession != null) { if (existingSession != null) {
authToken = token; authToken = token;
existingSession.setCredentials(credentials); existingSession.setAuthenticatedUser(authenticatedUser);
existingSession.setUserContext(userContext); existingSession.setUserContexts(userContexts);
} }
// If no existing session, generate a new token/session pair // If no existing session, generate a new token/session pair
else { else {
authToken = authTokenGenerator.getToken(); authToken = authTokenGenerator.getToken();
tokenSessionMap.put(authToken, new GuacamoleSession(environment, credentials, userContext)); tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts));
logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier());
} }
logger.debug("Login was successful for user \"{}\".", userContext.self().getIdentifier()); // Build list of all available auth providers
return new APIAuthToken(authToken, userContext.self().getIdentifier()); List<String> authProviderIdentifiers = new ArrayList<String>(userContexts.size());
for (UserContext userContext : userContexts)
authProviderIdentifiers.add(userContext.getAuthenticationProvider().getIdentifier());
// Return possibly-new auth token
return new APIAuthenticationResult(
authToken,
authenticatedUser.getIdentifier(),
authenticatedUser.getAuthenticationProvider().getIdentifier(),
authProviderIdentifiers
);
} }

View File

@@ -1,101 +0,0 @@
/*
* Copyright (C) 2013 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.net.basic.rest.clipboard;
import com.google.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleUnsupportedException;
import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.net.basic.ClipboardState;
import org.glyptodon.guacamole.net.basic.GuacamoleSession;
import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty;
/**
* A REST service for reading the current contents of the clipboard.
*
* @author Michael Jumper
*/
@Path("/clipboard")
public class ClipboardRESTService {
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* The amount of time to wait for clipboard changes, in milliseconds.
*/
private static final int CLIPBOARD_TIMEOUT = 250;
/**
* Whether clipboard integration is enabled.
*/
public static final BooleanGuacamoleProperty INTEGRATION_ENABLED = new BooleanGuacamoleProperty() {
@Override
public String getName() { return "enable-clipboard-integration"; }
};
@GET
@AuthProviderRESTExposure
public Response getClipboard(@QueryParam("token") String authToken)
throws GuacamoleException {
// Only bother if actually enabled
if (environment.getProperty(INTEGRATION_ENABLED, false)) {
// Get clipboard
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
final ClipboardState clipboard = session.getClipboardState();
// Send clipboard contents
synchronized (clipboard) {
clipboard.waitForContents(CLIPBOARD_TIMEOUT);
return Response.ok(clipboard.getContents(),
clipboard.getMimetype()).build();
}
}
// Otherwise, inform not supported
else
throw new GuacamoleUnsupportedException("Clipboard integration not supported");
}
}

View File

@@ -41,11 +41,11 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.User;
import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission;
import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet;
import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.Permission;
@@ -121,12 +121,6 @@ public class UserRESTService {
@Inject @Inject
private ObjectRetrievalService retrievalService; private ObjectRetrievalService retrievalService;
/**
* The authentication provider used to authenticating a user.
*/
@Inject
private AuthenticationProvider authProvider;
/** /**
* Gets a list of users in the system, filtering the returned list by the * Gets a list of users in the system, filtering the returned list by the
* given permission, if specified. * given permission, if specified.
@@ -337,8 +331,16 @@ public class UserRESTService {
credentials.setRequest(request); credentials.setRequest(request);
credentials.setSession(request.getSession(true)); credentials.setSession(request.getSession(true));
// Verify that the old password was correct // Verify that the old password was correct
if (authProvider.getUserContext(credentials) == null) { try {
if (userContext.getAuthenticationProvider().authenticateUser(credentials) == null) {
throw new APIException(APIError.Type.PERMISSION_DENIED,
"Permission denied.");
}
}
// Pass through any credentials exceptions as simple permission denied
catch (GuacamoleCredentialsException e) {
throw new APIException(APIError.Type.PERMISSION_DENIED, throw new APIException(APIError.Type.PERMISSION_DENIED,
"Permission denied."); "Permission denied.");
} }

View File

@@ -20,7 +20,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package org.glyptodon.guacamole.net.basic.xml.user_mapping; package org.glyptodon.guacamole.net.basic.xml.usermapping;
import org.glyptodon.guacamole.net.basic.auth.Authorization; import org.glyptodon.guacamole.net.basic.auth.Authorization;
import org.glyptodon.guacamole.net.basic.auth.UserMapping; import org.glyptodon.guacamole.net.basic.auth.UserMapping;

View File

@@ -20,7 +20,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package org.glyptodon.guacamole.net.basic.xml.user_mapping; package org.glyptodon.guacamole.net.basic.xml.usermapping;
import org.glyptodon.guacamole.net.basic.auth.Authorization; import org.glyptodon.guacamole.net.basic.auth.Authorization;
import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.xml.TagHandler;

View File

@@ -20,7 +20,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package org.glyptodon.guacamole.net.basic.xml.user_mapping; package org.glyptodon.guacamole.net.basic.xml.usermapping;
import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.xml.TagHandler;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;

View File

@@ -20,7 +20,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package org.glyptodon.guacamole.net.basic.xml.user_mapping; package org.glyptodon.guacamole.net.basic.xml.usermapping;
import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.xml.TagHandler;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;

View File

@@ -20,7 +20,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package org.glyptodon.guacamole.net.basic.xml.user_mapping; package org.glyptodon.guacamole.net.basic.xml.usermapping;
import org.glyptodon.guacamole.net.basic.auth.UserMapping; import org.glyptodon.guacamole.net.basic.auth.UserMapping;
import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.xml.TagHandler;

View File

@@ -23,5 +23,5 @@
/** /**
* Classes related to parsing the user-mapping.xml file. * Classes related to parsing the user-mapping.xml file.
*/ */
package org.glyptodon.guacamole.net.basic.xml.user_mapping; package org.glyptodon.guacamole.net.basic.xml.usermapping;

View File

@@ -57,7 +57,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
/** /**
* The unique identifier of the local cookie which stores the user's * The unique identifier of the local cookie which stores the user's
* current authentication token and user ID. * current authentication token and username.
* *
* @type String * @type String
*/ */
@@ -68,7 +68,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
* and given arbitrary parameters, returning a promise that succeeds only * and given arbitrary parameters, returning a promise that succeeds only
* if the authentication operation was successful. The resulting * if the authentication operation was successful. The resulting
* authentication data can be retrieved later via getCurrentToken() or * authentication data can be retrieved later via getCurrentToken() or
* getCurrentUserID(). * getCurrentUsername().
* *
* The provided parameters can be virtually any object, as each property * The provided parameters can be virtually any object, as each property
* will be sent as an HTTP parameter in the authentication request. * will be sent as an HTTP parameter in the authentication request.
@@ -99,8 +99,9 @@ angular.module('auth').factory('authenticationService', ['$injector',
// Store auth data // Store auth data
$cookieStore.put(AUTH_COOKIE_ID, { $cookieStore.put(AUTH_COOKIE_ID, {
authToken : data.authToken, authToken : data.authToken,
userID : data.userID username : data.username,
dataSource : data.dataSource
}); });
// Process is complete // Process is complete
@@ -174,7 +175,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
* its properties will be included as parameters in the update request. * its properties will be included as parameters in the update request.
* This function returns a promise that succeeds only if the authentication * This function returns a promise that succeeds only if the authentication
* operation was successful. The resulting authentication data can be * operation was successful. The resulting authentication data can be
* retrieved later via getCurrentToken() or getCurrentUserID(). * retrieved later via getCurrentToken() or getCurrentUsername().
* *
* If there is no current auth token, this function behaves identically to * If there is no current auth token, this function behaves identically to
* authenticate(), and makes a general authentication request. * authenticate(), and makes a general authentication request.
@@ -209,7 +210,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
* with a username and password, ignoring any currently-stored token, * with a username and password, ignoring any currently-stored token,
* returning a promise that succeeds only if the login operation was * returning a promise that succeeds only if the login operation was
* successful. The resulting authentication data can be retrieved later * successful. The resulting authentication data can be retrieved later
* via getCurrentToken() or getCurrentUserID(). * via getCurrentToken() or getCurrentUsername().
* *
* @param {String} username * @param {String} username
* The username to log in with. * The username to log in with.
@@ -254,19 +255,19 @@ angular.module('auth').factory('authenticationService', ['$injector',
}; };
/** /**
* Returns the user ID of the current user. If the current user is not * Returns the username of the current user. If the current user is not
* logged in, this ID may not be valid. * logged in, this value may not be valid.
* *
* @returns {String} * @returns {String}
* The user ID of the current user, or null if no authentication data * The username of the current user, or null if no authentication data
* is present. * is present.
*/ */
service.getCurrentUserID = function getCurrentUserID() { service.getCurrentUsername = function getCurrentUsername() {
// Return user ID, if available // Return username, if available
var authData = $cookieStore.get(AUTH_COOKIE_ID); var authData = $cookieStore.get(AUTH_COOKIE_ID);
if (authData) if (authData)
return authData.userID; return authData.username;
// No auth data present // No auth data present
return null; return null;
@@ -293,5 +294,45 @@ angular.module('auth').factory('authenticationService', ['$injector',
}; };
/**
* Returns the identifier of the data source that authenticated the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String}
* The identifier of the data source that authenticated the current
* user, or null if no authentication data is present.
*/
service.getDataSource = function getDataSource() {
// Return data source, if available
var authData = $cookieStore.get(AUTH_COOKIE_ID);
if (authData)
return authData.dataSource;
// No auth data present
return null;
};
/**
* Returns the identifiers of all data sources available to the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String[]}
* The identifiers of all data sources availble to the current user,
* or null if no authentication data is present.
*/
service.getAvailableDataSources = function getAvailableDataSources() {
// Return data sources, if available
var authData = $cookieStore.get(AUTH_COOKIE_ID);
if (authData)
return authData.availableDataSources;
// No auth data present
return null;
};
return service; return service;
}]); }]);

View File

@@ -190,7 +190,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
}); });
// Query the user's permissions for the current connection // Query the user's permissions for the current connection
permissionService.getPermissions(authenticationService.getCurrentUserID()) permissionService.getPermissions(authenticationService.getCurrentUsername())
.success(function permissionsReceived(permissions) { .success(function permissionsReceived(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;

View File

@@ -128,7 +128,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
}); });
// Query the user's permissions for the current connection group // Query the user's permissions for the current connection group
permissionService.getPermissions(authenticationService.getCurrentUserID()) permissionService.getPermissions(authenticationService.getCurrentUsername())
.success(function permissionsReceived(permissions) { .success(function permissionsReceived(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;

View File

@@ -153,7 +153,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
}); });
// Query the user's permissions for the current connection // Query the user's permissions for the current connection
permissionService.getPermissions(authenticationService.getCurrentUserID()) permissionService.getPermissions(authenticationService.getCurrentUsername())
.success(function permissionsReceived(permissions) { .success(function permissionsReceived(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;

View File

@@ -78,7 +78,7 @@ angular.module('navigation').directive('guacUserMenu', [function guacUserMenu()
* *
* @type String * @type String
*/ */
$scope.username = authenticationService.getCurrentUserID(); $scope.username = authenticationService.getCurrentUsername();
/** /**
* The available main pages for the current user. * The available main pages for the current user.

View File

@@ -156,7 +156,7 @@ angular.module('navigation').factory('userPageService', ['$injector',
PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER);
// Ignore permission to update self // Ignore permission to update self
PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUserID()); PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUsername());
// Determine whether the current user needs access to the user management UI // Determine whether the current user needs access to the user management UI
var canManageUsers = var canManageUsers =
@@ -247,7 +247,7 @@ angular.module('navigation').factory('userPageService', ['$injector',
// Retrieve current permissions, resolving main pages if possible // Retrieve current permissions, resolving main pages if possible
// Resolve promise using settings pages derived from permissions // Resolve promise using settings pages derived from permissions
permissionService.getPermissions(authenticationService.getCurrentUserID()) permissionService.getPermissions(authenticationService.getCurrentUsername())
.success(function permissionsRetrieved(permissions) { .success(function permissionsRetrieved(permissions) {
deferred.resolve(generateSettingsPages(permissions)); deferred.resolve(generateSettingsPages(permissions));
}); });
@@ -328,7 +328,7 @@ angular.module('navigation').factory('userPageService', ['$injector',
}); });
// Retrieve current permissions, resolving main pages if possible // Retrieve current permissions, resolving main pages if possible
permissionService.getPermissions(authenticationService.getCurrentUserID()) permissionService.getPermissions(authenticationService.getCurrentUsername())
.success(function permissionsRetrieved(retrievedPermissions) { .success(function permissionsRetrieved(retrievedPermissions) {
permissions = retrievedPermissions; permissions = retrievedPermissions;
resolveMainPages(); resolveMainPages();

View File

@@ -48,7 +48,7 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe
var permissionService = $injector.get('permissionService'); var permissionService = $injector.get('permissionService');
// Identifier of the current user // Identifier of the current user
var currentUserID = authenticationService.getCurrentUserID(); var currentUsername = authenticationService.getCurrentUsername();
/** /**
* An action to be provided along with the object sent to * An action to be provided along with the object sent to
@@ -120,7 +120,7 @@ angular.module('settings').directive('guacSettingsConnections', [function guacSe
}; };
// Retrieve current permissions // Retrieve current permissions
permissionService.getPermissions(currentUserID) permissionService.getPermissions(currentUsername)
.success(function permissionsRetrieved(permissions) { .success(function permissionsRetrieved(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;

View File

@@ -64,7 +64,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe
* *
* @type String * @type String
*/ */
var username = authenticationService.getCurrentUserID(); var username = authenticationService.getCurrentUsername();
/** /**
* All currently-set preferences, or their defaults if not yet set. * All currently-set preferences, or their defaults if not yet set.

View File

@@ -187,7 +187,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
}; };
// Query the user's permissions // Query the user's permissions
permissionService.getPermissions(authenticationService.getCurrentUserID()) permissionService.getPermissions(authenticationService.getCurrentUsername())
.success(function permissionsReceived(retrievedPermissions) { .success(function permissionsReceived(retrievedPermissions) {
$scope.permissions = retrievedPermissions; $scope.permissions = retrievedPermissions;
}); });

View File

@@ -48,7 +48,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
var userService = $injector.get('userService'); var userService = $injector.get('userService');
// Identifier of the current user // Identifier of the current user
var currentUserID = authenticationService.getCurrentUserID(); var currentUsername = authenticationService.getCurrentUsername();
/** /**
* An action to be provided along with the object sent to * An action to be provided along with the object sent to
@@ -118,7 +118,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
}; };
// Retrieve current permissions // Retrieve current permissions
permissionService.getPermissions(currentUserID) permissionService.getPermissions(currentUsername)
.success(function permissionsRetrieved(permissions) { .success(function permissionsRetrieved(permissions) {
$scope.permissions = permissions; $scope.permissions = permissions;
@@ -147,7 +147,7 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
// Display only other users, not self // Display only other users, not self
$scope.users = users.filter(function isNotSelf(user) { $scope.users = users.filter(function isNotSelf(user) {
return user.username !== currentUserID; return user.username !== currentUsername;
}); });
}); });