diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java index e57afe67c..0816ae2c5 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java @@ -22,6 +22,8 @@ package org.glyptodon.guacamole.net.basic; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.glyptodon.guacamole.GuacamoleException; @@ -51,9 +53,10 @@ public class GuacamoleSession { 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 userContexts; /** * All currently-active tunnels, indexed by tunnel UUID. @@ -66,7 +69,8 @@ public class GuacamoleSession { 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 * The environment of the Guacamole server associated with this new @@ -75,18 +79,19 @@ public class GuacamoleSession { * @param authenticatedUser * The authenticated user to associate this session with. * - * @param userContext - * The user context to associate this session with. + * @param userContexts + * The List of UserContexts to associate with this session. * * @throws GuacamoleException * If an error prevents the session from being created. */ public GuacamoleSession(Environment environment, - AuthenticatedUser authenticatedUser, UserContext userContext) + AuthenticatedUser authenticatedUser, + List userContexts) throws GuacamoleException { this.lastAccessedTime = System.currentTimeMillis(); this.authenticatedUser = authenticatedUser; - this.userContext = userContext; + this.userContexts = userContexts; } /** @@ -116,18 +121,56 @@ public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) { * @return The UserContext associated with this session. */ 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; + + } + + /** + * Returns a list of all UserContexts associated with this session. Each + * AuthenticationProvider currently loaded by Guacamole may provide its own + * UserContext for any successfully-authenticated user. + * + * @return + * An unmodifiable list of all UserContexts associated with this + * session. + */ + public List getUserContexts() { + return Collections.unmodifiableList(userContexts); } /** - * Replaces the user context associated with this session with the given - * user context. + * Replaces all UserContexts associated with this session with the given + * List of UserContexts. * - * @param userContext - * The user context to associate with this session. + * @param userContexts + * The List of UserContexts to associate with this session. */ - public void setUserContext(UserContext userContext) { - this.userContext = userContext; + public void setUserContexts(List userContexts) { + this.userContexts = userContexts; } /** diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java index 23402c8fa..9c20eedeb 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java @@ -244,13 +244,13 @@ public void close() throws GuacamoleException { // Connection identifiers case CONNECTION: logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds", - session.getUserContext().self().getIdentifier(), id, duration); + session.getAuthenticatedUser().getIdentifier(), id, duration); break; // Connection group identifiers case CONNECTION_GROUP: logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds", - session.getUserContext().self().getIdentifier(), id, duration); + session.getAuthenticatedUser().getIdentifier(), id, duration); break; // Type is guaranteed to be one of the above diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java index 49d7a54ad..ac72a34ae 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java @@ -22,6 +22,7 @@ package org.glyptodon.guacamole.net.basic.extension; +import com.google.inject.Provides; import com.google.inject.servlet.ServletModule; import java.io.File; import java.io.FileFilter; @@ -91,10 +92,10 @@ public class ExtensionModule extends ServletModule { private final Environment environment; /** - * The currently-bound authentication provider, if any. At the moment, we - * only support one authentication provider loaded at any one time. + * All currently-bound authentication providers, if any. */ - private Class boundAuthenticationProvider = null; + private final List boundAuthenticationProviders = + new ArrayList(); /** * Service for adding and retrieving language resources. @@ -179,40 +180,24 @@ private Class getAuthProviderProperty() { /** * Binds the given AuthenticationProvider class such that any service * requiring access to the AuthenticationProvider can obtain it via - * injection. + * injection, along with any other bound AuthenticationProviders. * * @param authenticationProvider * The AuthenticationProvider class to bind. */ private void bindAuthenticationProvider(Class 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 - logger.debug("Binding AuthenticationProvider \"{}\".", authenticationProvider); - bind(AuthenticationProvider.class).toInstance(new AuthenticationProviderFacade(authenticationProvider)); + logger.debug("[{}] Binding AuthenticationProvider \"{}\".", + boundAuthenticationProviders.size(), authenticationProvider.getName()); + boundAuthenticationProviders.add(new AuthenticationProviderFacade(authenticationProvider)); } /** * Binds each of the the given AuthenticationProvider classes such that any * service requiring access to the AuthenticationProvider can obtain it via - * injection. Note that, as multiple simultaneous authentication providers - * are not currently supported, attempting to bind more than one - * authentication provider will result in warnings being logged. + * injection. * * @param authProviders * The AuthenticationProvider classes to bind. @@ -225,6 +210,18 @@ private void bindAuthenticationProviders(Collection getAuthenticationProviders() { + return Collections.unmodifiableList(boundAuthenticationProviders); + } + /** * Serves each of the given resources as a language resource. Language * resources are served from within the "/translations" directory as JSON @@ -415,11 +412,8 @@ protected void configureServlets() { // Load all extensions loadExtensions(javaScriptResources, cssResources); - // Bind basic auth if nothing else chosen/provided - if (boundAuthenticationProvider == null) { - logger.info("Using default, \"basic\", XML-driven authentication."); - bindAuthenticationProvider(BasicFileAuthenticationProvider.class); - } + // Always bind basic auth last + bindAuthenticationProvider(BasicFileAuthenticationProvider.class); // Dynamically generate app.js and app.css from extensions serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources))); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java index c72040493..f012d2757 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.java @@ -22,6 +22,7 @@ package org.glyptodon.guacamole.net.basic.rest; +import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.net.auth.Connection; @@ -29,6 +30,7 @@ import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup; /** @@ -39,6 +41,39 @@ */ 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 id + * The numeric ID of the UserContext to retrieve. This ID is the index + * of the UserContext within the overall list of UserContexts + * associated with the user's session. + * + * @return + * The user having the given identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the user, or if the + * user does not exist. + */ + public UserContext retrieveUserContext(GuacamoleSession session, + int id) throws GuacamoleException { + + // Get list of UserContexts + List userContexts = session.getUserContexts(); + + // Verify context exists + if (id < 0 || id >= userContexts.size()) + throw new GuacamoleResourceNotFoundException("No such user context: \"" + id + "\""); + + return userContexts.get(id); + + } + /** * Retrieves a single user from the given user context. * diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java index 46eb082c4..b730a7163 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.java @@ -23,6 +23,7 @@ package org.glyptodon.guacamole.net.basic.rest.auth; import com.google.inject.Inject; +import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleUnauthorizedException; import org.glyptodon.guacamole.net.auth.UserContext; @@ -77,5 +78,24 @@ public GuacamoleSession getGuacamoleSession(String authToken) public UserContext getUserContext(String authToken) throws GuacamoleException { 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 getUserContexts(String authToken) + throws GuacamoleException { + return getGuacamoleSession(authToken).getUserContexts(); + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java index 6d9a90923..f0130905a 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java @@ -24,6 +24,8 @@ import com.google.inject.Inject; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; @@ -68,10 +70,11 @@ public class TokenRESTService { 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 - private AuthenticationProvider authProvider; + private List authProviders; /** * The map of auth tokens to sessions for the REST endpoints. @@ -224,23 +227,31 @@ public APIAuthToken createToken(@FormParam("username") String username, credentials.setRequest(request); credentials.setSession(request.getSession(true)); - AuthenticatedUser authenticatedUser; + AuthenticatedUser authenticatedUser = null; try { // Re-authenticate user if session exists - if (existingSession != null) - authenticatedUser = authProvider.updateAuthenticatedUser(existingSession.getAuthenticatedUser(), credentials); + if (existingSession != null) { + authenticatedUser = existingSession.getAuthenticatedUser(); + authenticatedUser = authenticatedUser.getAuthenticationProvider().updateAuthenticatedUser(authenticatedUser, credentials); + } - /// Otherwise, authenticate as a new user + // Otherwise, attempt authentication as a new user against each + // AuthenticationProvider, in deterministic order else { + for (AuthenticationProvider authProvider : authProviders) { - authenticatedUser = authProvider.authenticateUser(credentials); + // Attempt authentication + authenticatedUser = authProvider.authenticateUser(credentials); - // Log successful authentication - if (authenticatedUser != null && logger.isInfoEnabled()) - logger.info("User \"{}\" successfully authenticated from {}.", - authenticatedUser.getIdentifier(), getLoggableAddress(request)); + // Stop after successful authentication + if (authenticatedUser != null && logger.isInfoEnabled()) { + logger.info("User \"{}\" successfully authenticated from {}.", + authenticatedUser.getIdentifier(), getLoggableAddress(request)); + break; + } + } } // Request standard username/password if no user was produced @@ -266,30 +277,35 @@ else if (logger.isDebugEnabled()) throw e; } - // Generate or update user context - UserContext userContext; - if (existingSession != null) - userContext = authProvider.updateUserContext(existingSession.getUserContext(), authenticatedUser); - else - userContext = authProvider.getUserContext(authenticatedUser); + // Get UserContexts from each available AuthenticationProvider + List userContexts = new ArrayList(authProviders.size()); + for (AuthenticationProvider authProvider : authProviders) { - // STUB: Request standard username/password if no user context was produced - if (userContext == null) - throw new GuacamoleInvalidCredentialsException("Permission Denied.", - CredentialsInfo.USERNAME_PASSWORD); + // Generate or update user context + UserContext userContext; + if (existingSession != null) + userContext = authProvider.updateUserContext(existingSession.getUserContext(), authenticatedUser); + else + userContext = authProvider.getUserContext(authenticatedUser); + + // Add to available data, if successful + if (userContext != null) + userContexts.add(userContext); + + } // Update existing session, if it exists String authToken; if (existingSession != null) { authToken = token; existingSession.setAuthenticatedUser(authenticatedUser); - existingSession.setUserContext(userContext); + existingSession.setUserContexts(userContexts); } // If no existing session, generate a new token/session pair else { authToken = authTokenGenerator.getToken(); - tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContext)); + tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts)); } logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier()); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java index 03696b7e6..37fda2bff 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -41,7 +41,6 @@ import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleException; 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.Directory; import org.glyptodon.guacamole.net.auth.User; @@ -122,12 +121,6 @@ public class UserRESTService { @Inject 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 * given permission, if specified. @@ -340,7 +333,7 @@ public void updatePassword(@QueryParam("token") String authToken, // Verify that the old password was correct try { - if (authProvider.authenticateUser(credentials) == null) { + if (userContext.getAuthenticationProvider().authenticateUser(credentials) == null) { throw new APIException(APIError.Type.PERMISSION_DENIED, "Permission denied."); }