GUAC-586: Load multiple AuthenticationProviders.

This commit is contained in:
Michael Jumper
2015-08-26 13:39:17 -07:00
parent 15e948138d
commit 343c8e6cd0
7 changed files with 178 additions and 77 deletions

View File

@@ -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<UserContext> 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<UserContext> userContexts)
throws GuacamoleException {
this.lastAccessedTime = System.currentTimeMillis();
this.authenticatedUser = authenticatedUser;
this.userContext = userContext;
this.userContexts = userContexts;
}
/**
@@ -116,18 +121,56 @@ public class GuacamoleSession {
* @return The UserContext associated with this session.
*/
public UserContext getUserContext() {
// 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
* user context.
* 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.
*
* @param userContext
* The user context to associate with this session.
* @return
* An unmodifiable list of all UserContexts associated with this
* session.
*/
public void setUserContext(UserContext userContext) {
this.userContext = userContext;
public List<UserContext> getUserContexts() {
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;
}
/**

View File

@@ -244,13 +244,13 @@ public class TunnelRequestService {
// 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

View File

@@ -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<? extends AuthenticationProvider> boundAuthenticationProvider = null;
private final List<AuthenticationProvider> boundAuthenticationProviders =
new ArrayList<AuthenticationProvider>();
/**
* 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
* 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<? 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
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 @@ 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
* resources are served from within the "/translations" directory as JSON
@@ -415,11 +412,8 @@ public class ExtensionModule extends ServletModule {
// 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.");
// 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)));

View File

@@ -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.ConnectionGroup;
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 @@ import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup
*/
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<UserContext> 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.
*

View File

@@ -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;
@@ -78,4 +79,23 @@ public class AuthenticationService {
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

@@ -24,6 +24,8 @@ package org.glyptodon.guacamole.net.basic.rest.auth;
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<AuthenticationProvider> authProviders;
/**
* The map of auth tokens to sessions for the REST endpoints.
@@ -224,24 +227,32 @@ public class TokenRESTService {
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) {
// Attempt authentication
authenticatedUser = authProvider.authenticateUser(credentials);
// Log successful authentication
if (authenticatedUser != null && logger.isInfoEnabled())
// 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
if (authenticatedUser == null)
@@ -266,6 +277,10 @@ public class TokenRESTService {
throw e;
}
// Get UserContexts from each available AuthenticationProvider
List<UserContext> userContexts = new ArrayList<UserContext>(authProviders.size());
for (AuthenticationProvider authProvider : authProviders) {
// Generate or update user context
UserContext userContext;
if (existingSession != null)
@@ -273,23 +288,24 @@ public class TokenRESTService {
else
userContext = authProvider.getUserContext(authenticatedUser);
// STUB: Request standard username/password if no user context was produced
if (userContext == null)
throw new GuacamoleInvalidCredentialsException("Permission Denied.",
CredentialsInfo.USERNAME_PASSWORD);
// 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());

View File

@@ -41,7 +41,6 @@ import javax.ws.rs.core.Context;
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 class UserRESTService {
// 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.");
}