diff --git a/.gitignore b/.gitignore index d26be9c0e..b82b2caa7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *~ target/ nb-configuration.xml +guacamole/customs.json diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleConfiguration.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleConfiguration.java index e43841236..1a034b39e 100644 --- a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleConfiguration.java +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleConfiguration.java @@ -162,4 +162,31 @@ public class GuacamoleConfiguration implements Serializable { return Collections.unmodifiableSet(parameters.keySet()); } + /** + * Returns a map which contains parameter name/value pairs as key/value + * pairs. Changes to this map will affect the parameters stored within + * this configuration. + * + * @return + * A map which contains all parameter name/value pairs as key/value + * pairs. + */ + public Map getParameters() { + return parameters; + } + + /** + * Replaces all current parameters with the parameters defined within the + * given map. Key/value pairs within the map represent parameter name/value + * pairs. + * + * @param parameters + * A map which contains all parameter name/value pairs as key/value + * pairs. + */ + public void setParameters(Map parameters) { + this.parameters.clear(); + this.parameters.putAll(parameters); + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java index 55e473755..70fde2592 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java @@ -23,11 +23,7 @@ package org.glyptodon.guacamole.net.basic.rest; import com.google.inject.AbstractModule; -import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionService; -import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupService; -import org.glyptodon.guacamole.net.basic.rest.permission.PermissionService; import org.glyptodon.guacamole.net.basic.rest.protocol.ProtocolRetrievalService; -import org.glyptodon.guacamole.net.basic.rest.user.UserService; /** * A Guice Module for setting up dependency injection for the @@ -41,10 +37,6 @@ public class RESTModule extends AbstractModule { protected void configure() { // Bind generic low-level services - bind(ConnectionService.class); - bind(ConnectionGroupService.class); - bind(PermissionService.class); - bind(UserService.class); bind(ProtocolRetrievalService.class); } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java index 6d8487fdf..cc4e90e6d 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java @@ -30,7 +30,6 @@ 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.connectiongroup.ConnectionGroupRESTService; -import org.glyptodon.guacamole.net.basic.rest.permission.PermissionRESTService; import org.glyptodon.guacamole.net.basic.rest.protocol.ProtocolRESTService; import org.glyptodon.guacamole.net.basic.rest.user.UserRESTService; @@ -48,7 +47,6 @@ public class RESTServletModule extends ServletModule { bind(ClipboardRESTService.class); bind(ConnectionRESTService.class); bind(ConnectionGroupRESTService.class); - bind(PermissionRESTService.class); bind(ProtocolRESTService.class); bind(UserRESTService.class); bind(TokenRESTService.class); 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 95a8e28b7..6db943908 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 @@ -48,7 +48,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/token") +@Path("/tokens") @Produces(MediaType.APPLICATION_JSON) public class TokenRESTService { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java index dab0c3884..57bf4a9c8 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.java @@ -22,14 +22,11 @@ package org.glyptodon.guacamole.net.basic.rest.connection; -import java.util.HashMap; -import java.util.List; import java.util.Map; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.ConnectionRecord; -import org.glyptodon.guacamole.net.basic.rest.APIConstants; +import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** @@ -60,15 +57,10 @@ public class APIConnection { */ private String protocol; - /** - * The history records associated with this connection. - */ - private List history; - /** * Map of all associated parameter values, indexed by parameter name. */ - private Map parameters = new HashMap(); + private Map parameters; /** * Create an empty APIConnection. @@ -76,7 +68,9 @@ public class APIConnection { public APIConnection() {} /** - * Create an APIConnection from a Connection record. + * Create an APIConnection from a Connection record. Parameters for the + * connection will not be included. + * * @param connection The connection to create this APIConnection from. * @throws GuacamoleException If a problem is encountered while * instantiating this new APIConnection. @@ -84,21 +78,18 @@ public class APIConnection { public APIConnection(Connection connection) throws GuacamoleException { + // Set identifying information this.name = connection.getName(); this.identifier = connection.getIdentifier(); + + // Set proper parent identifier, using root identifier if needed this.parentIdentifier = connection.getParentIdentifier(); - this.history = connection.getHistory(); - - // Use the explicit ROOT group ID if (this.parentIdentifier == null) - this.parentIdentifier = APIConstants.ROOT_CONNECTION_GROUP_IDENTIFIER; - + this.parentIdentifier = APIConnectionGroup.ROOT_IDENTIFIER; + + // Set protocol from configuration GuacamoleConfiguration configuration = connection.getConfiguration(); - this.protocol = configuration.getProtocol(); - - for (String key: configuration.getParameterNames()) - this.parameters.put(key, configuration.getParameter(key)); } @@ -151,14 +142,6 @@ public class APIConnection { this.parentIdentifier = parentIdentifier; } - /** - * Returns the history records associated with this connection. - * @return The history records associated with this connection. - */ - public List getHistory() { - return history; - } - /** * Returns the parameter map for this connection. * @return The parameter map for this connection. diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java index aca765c40..94bb5c0ed 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.java @@ -22,6 +22,7 @@ package org.glyptodon.guacamole.net.basic.rest.connection; +import java.util.Collections; import java.util.List; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; @@ -78,15 +79,14 @@ public class APIConnectionWrapper implements Connection { @Override public GuacamoleConfiguration getConfiguration() { - // Create the GuacamoleConfiguration from the parameter map + // Create the GuacamoleConfiguration with current protocol GuacamoleConfiguration configuration = new GuacamoleConfiguration(); - - Map parameters = apiConnection.getParameters(); - - for(Map.Entry entry : parameters.entrySet()) - configuration.setParameter(entry.getKey(), entry.getValue()); - configuration.setProtocol(apiConnection.getProtocol()); + + // Add parameters, if available + Map parameters = apiConnection.getParameters(); + if (parameters != null) + configuration.setParameters(parameters); return configuration; } @@ -94,13 +94,10 @@ public class APIConnectionWrapper implements Connection { @Override public void setConfiguration(GuacamoleConfiguration config) { - // Create a parameter map from the GuacamoleConfiguration - Map parameters = apiConnection.getParameters(); - for(String key : config.getParameterNames()) - parameters.put(key, config.getParameter(key)); - - // Set the protocol + // Set protocol and parameters apiConnection.setProtocol(config.getProtocol()); + apiConnection.setParameters(config.getParameters()); + } @Override @@ -110,7 +107,7 @@ public class APIConnectionWrapper implements Connection { @Override public List getHistory() throws GuacamoleException { - return apiConnection.getHistory(); + return Collections.EMPTY_LIST; } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java index 103b56643..95efdd54b 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java @@ -24,6 +24,7 @@ package org.glyptodon.guacamole.net.basic.rest.connection; import com.google.inject.Inject; import java.util.List; +import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -34,16 +35,18 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response.Status; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; +import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; -import org.glyptodon.guacamole.net.basic.rest.HTTPException; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup; +import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +55,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/connection") +@Path("/connections") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionRESTService { @@ -69,59 +72,20 @@ public class ConnectionRESTService { private AuthenticationService authenticationService; /** - * A service for managing the REST endpoint APIConnection objects. - */ - @Inject - private ConnectionService connectionService; - - /** - * Gets a list of connections with the given ConnectionGroup parentID. - * If no parentID is provided, returns the connections from the root group. + * Retrieves an individual connection. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param parentID The ID of the ConnectionGroup the connections - * belong to. If null, the root connection group will be used. - * @return The connection list. - * @throws GuacamoleException If a problem is encountered while listing connections. - */ - @GET - @AuthProviderRESTExposure - public List getConnections(@QueryParam("token") String authToken, @QueryParam("parentID") String parentID) - throws GuacamoleException { - - UserContext userContext = authenticationService.getUserContext(authToken); - - // If the parent connection group is passed in, try to find it. - ConnectionGroup parentConnectionGroup; - if (parentID == null) - parentConnectionGroup = userContext.getRootConnectionGroup(); - - else { - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); - parentConnectionGroup = connectionGroupDirectory.get(parentID); - } - - if (parentConnectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); - - Directory connectionDirectory = - parentConnectionGroup.getConnectionDirectory(); - - // Return the converted connection directory - return connectionService.convertConnectionList(connectionDirectory); - - } - - /** - * Gets an individual connection. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionID The ID of the Connection.. - * @return The connection. - * @throws GuacamoleException If a problem is encountered while retrieving the connection. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionID + * The identifier of the connection to retrieve. + * + * @return + * The connection having the given identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection. */ @GET @Path("/{connectionID}") @@ -139,19 +103,105 @@ public class ConnectionRESTService { // Get the connection Connection connection = connectionDirectory.get(connectionID); if (connection == null) - throw new HTTPException(Status.NOT_FOUND, "No Connection found with the provided ID."); + throw new GuacamoleResourceNotFoundException("No such connection: \"" + connectionID + "\""); return new APIConnection(connection); } - + + /** + * Retrieves the parameters associated with a single connection. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionID + * The identifier of the connection. + * + * @return + * A map of parameter name/value pairs. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection parameters. + */ + @GET + @Path("/{connectionID}/parameters") + @AuthProviderRESTExposure + public Map getConnectionParameters(@QueryParam("token") String authToken, + @PathParam("connectionID") String connectionID) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionDirectory = + rootGroup.getConnectionDirectory(); + + // Get the connection + Connection connection = connectionDirectory.get(connectionID); + if (connection == null) + throw new GuacamoleResourceNotFoundException("No such connection: \"" + connectionID + "\""); + + // Retrieve connection configuration + GuacamoleConfiguration config = connection.getConfiguration(); + + // Return parameter map + return config.getParameters(); + + } + + /** + * Retrieves the usage history of a single connection. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionID + * The identifier of the connection. + * + * @return + * A list of connection records, describing the start and end times of + * various usages of this connection. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection history. + */ + @GET + @Path("/{connectionID}/history") + @AuthProviderRESTExposure + public List getConnectionHistory(@QueryParam("token") String authToken, + @PathParam("connectionID") String connectionID) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the connection directory + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + Directory connectionDirectory = + rootGroup.getConnectionDirectory(); + + // Get the connection + Connection connection = connectionDirectory.get(connectionID); + if (connection == null) + throw new GuacamoleResourceNotFoundException("No such connection: \"" + connectionID + "\""); + + return connection.getHistory(); + + } + /** * Deletes an individual connection. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionID The ID of the Connection to delete. - * @throws GuacamoleException If a problem is encountered while deleting the connection. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionID + * The identifier of the connection to delete. + * + * @throws GuacamoleException + * If an error occurs while deleting the connection. */ @DELETE @Path("/{connectionID}") @@ -168,72 +218,112 @@ public class ConnectionRESTService { // Make sure the connection is there before trying to delete if (connectionDirectory.get(connectionID) == null) - throw new HTTPException(Status.NOT_FOUND, "No Connection found with the provided ID."); + throw new GuacamoleResourceNotFoundException("No such connection: \"" + connectionID + "\""); // Delete the connection connectionDirectory.remove(connectionID); } - + /** - * Creates a new connection and returns the identifier of the new connection. - * If a parentID is provided, the connection will be created in the - * connection group with the parentID. Otherwise, the root connection group - * will be used. + * Retrieves a single connection group from the given user context. If + * the given identifier is null or the root identifier, the root connection + * group will be returned. + * + * @param userContext + * The user context to retrieve the connection group from. + * + * @param identifier + * The identifier of the connection group to retrieve. + * + * @return + * The connection group having the given identifier, or the root + * connection group if the identifier is null or the root identifier. + * + * @throws GuacamoleException + * If an error occurs while retrieving the connection group, or if the + * connection group does not exist. + */ + private ConnectionGroup retrieveConnectionGroup(UserContext userContext, + String identifier) throws GuacamoleException { + + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + + // Use root group if identifier is null (or the standard root identifier) + if (identifier == null || identifier.equals(APIConnectionGroup.ROOT_IDENTIFIER)) + return rootGroup; + + // Pull specified connection group otherwise + Directory directory = rootGroup.getConnectionGroupDirectory(); + ConnectionGroup connectionGroup = directory.get(identifier); + + if (connectionGroup == null) + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + identifier + "\""); + + return connectionGroup; + + } + + /** + * Creates a new connection and returns the identifier of the new + * connection. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param parentID The ID of the ConnectionGroup the connections - * belong to. If null, the root connection group will be used. - * @param connection The connection to create. - * @return The identifier of the new connection. - * @throws GuacamoleException If a problem is encountered while creating the connection. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connection + * The connection to create. + * + * @return + * The identifier of the new connection. + * + * @throws GuacamoleException + * If an error occurs while creating the connection. */ @POST @AuthProviderRESTExposure - public String createConnection(@QueryParam("token") String authToken, - @QueryParam("parentID") String parentID, APIConnection connection) throws GuacamoleException { + public String createConnection(@QueryParam("token") String authToken, + APIConnection connection) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); + // Validate that connection data was provided if (connection == null) - throw new GuacamoleClientException("A connection is required for this request."); + throw new GuacamoleClientException("Connection JSON must be submitted when creating connections."); - // If the parent connection group is passed in, try to find it. - ConnectionGroup parentConnectionGroup; - if (parentID == null) - parentConnectionGroup = userContext.getRootConnectionGroup(); + // Retrieve parent group + String parentID = connection.getParentIdentifier(); + ConnectionGroup parentConnectionGroup = retrieveConnectionGroup(userContext, parentID); - else { - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); - parentConnectionGroup = connectionGroupDirectory.get(parentID); - } - - if (parentConnectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); - - Directory connectionDirectory = - parentConnectionGroup.getConnectionDirectory(); - - // Create the connection + // Add the new connection + Directory connectionDirectory = parentConnectionGroup.getConnectionDirectory(); connectionDirectory.add(new APIConnectionWrapper(connection)); // Return the new connection identifier return connection.getIdentifier(); } - + /** - * Updates a connection. + * Updates an existing connection. If the parent identifier of the + * connection is changed, the connection will also be moved to the new + * parent group. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionID The ID of the Connection to move. - * @param connection The connection to update. - * @throws GuacamoleException If a problem is encountered while updating the connection. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionID + * The identifier of the connection to update. + * + * @param connection + * The connection data to update the specified connection with. + * + * @throws GuacamoleException + * If an error occurs while updating the connection. */ - @POST + @PUT @Path("/{connectionID}") @AuthProviderRESTExposure public void updateConnection(@QueryParam("token") String authToken, @@ -241,62 +331,30 @@ public class ConnectionRESTService { UserContext userContext = authenticationService.getUserContext(authToken); + // Validate that connection data was provided if (connection == null) - throw new GuacamoleClientException("A connection is required for this request."); + throw new GuacamoleClientException("Connection JSON must be submitted when updating connections."); // Get the connection directory ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); Directory connectionDirectory = rootGroup.getConnectionDirectory(); - Connection connectionFromAuthProvider = connectionDirectory.get(connectionID); - // Make sure the connection is there before trying to update - if (connectionFromAuthProvider == null) - throw new HTTPException(Status.NOT_FOUND, "No Connection found with the provided ID."); + Connection existingConnection = connectionDirectory.get(connectionID); + if (existingConnection == null) + throw new GuacamoleResourceNotFoundException("No such connection: \"" + connectionID + "\""); - // Copy the information from this connection over to an object from the Auth Provider - APIConnectionWrapper wrappedConnection = new APIConnectionWrapper(connection); - connectionFromAuthProvider.setConfiguration(wrappedConnection.getConfiguration()); - connectionFromAuthProvider.setName(wrappedConnection.getName()); + // Retrieve connection configuration + GuacamoleConfiguration config = new GuacamoleConfiguration(); + config.setProtocol(connection.getProtocol()); + config.setParameters(connection.getParameters()); // Update the connection - connectionDirectory.update(connectionFromAuthProvider); + existingConnection.setConfiguration(config); + existingConnection.setName(connection.getName()); + connectionDirectory.update(existingConnection); } - /** - * Moves an individual connection to a different connection group. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionID The ID of the Connection to move. - * @param parentID The ID of the ConnectionGroup the connection is to be moved to. - * @throws GuacamoleException If a problem is encountered while moving the connection. - */ - @PUT - @Path("/{connectionID}") - @AuthProviderRESTExposure - public void moveConnection(@QueryParam("token") String authToken, - @PathParam("connectionID") String connectionID, @QueryParam("parentID") String parentID) - throws GuacamoleException { - - UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the connection directory - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - Directory connectionDirectory = - rootGroup.getConnectionDirectory(); - - // Find the new parent connection group - Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); - ConnectionGroup parentConnectionGroup = connectionGroupDirectory.get(parentID); - - if (parentConnectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); - - // Move the connection - connectionDirectory.move(connectionID, parentConnectionGroup.getConnectionDirectory()); - } - } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionService.java deleted file mode 100644 index 44449818d..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionService.java +++ /dev/null @@ -1,59 +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.rest.connection; - -import java.util.ArrayList; -import java.util.List; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.Connection; -import org.glyptodon.guacamole.net.auth.Directory; - -/** - * A service for performing useful manipulations on REST Connections. - * - * @author James Muehlner - */ -public class ConnectionService { - - /** - * Converts a Connection Directory to a list of APIConnection objects for - * exposing with the REST endpoints. - * - * @param connectionDirectory The Connection Directory to convert for REST endpoint use. - * @return A List of APIConnection objects for use with the REST endpoint. - * @throws GuacamoleException If an error occurs while converting the - * connection directory. - */ - public List convertConnectionList(Directory connectionDirectory) - throws GuacamoleException { - - List restConnections = new ArrayList(); - - for (String connectionID : connectionDirectory.getIdentifiers()) - restConnections.add(new APIConnection(connectionDirectory.get(connectionID))); - - return restConnections; - - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java index 4ac155d39..3f166f41c 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java @@ -22,10 +22,11 @@ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; +import java.util.Collection; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup.Type; -import org.glyptodon.guacamole.net.basic.rest.APIConstants; +import org.glyptodon.guacamole.net.basic.rest.connection.APIConnection; /** * A simple connection group to expose through the REST endpoints. @@ -35,6 +36,11 @@ import org.glyptodon.guacamole.net.basic.rest.APIConstants; @JsonIgnoreProperties(ignoreUnknown = true) public class APIConnectionGroup { + /** + * The identifier of the root connection group. + */ + public static final String ROOT_IDENTIFIER = "ROOT"; + /** * The name of this connection group. */ @@ -54,6 +60,18 @@ public class APIConnectionGroup { * The type of this connection group. */ private Type type; + + /** + * All child connection groups. If children are not being queried, this may + * be omitted. + */ + private Collection childConnectionGroups; + + /** + * All child connections. If children are not being queried, this may be + * omitted. + */ + private Collection childConnections; /** * Create an empty APIConnectionGroup. @@ -73,7 +91,7 @@ public class APIConnectionGroup { // Use the explicit ROOT group ID if (this.parentIdentifier == null) - this.parentIdentifier = APIConstants.ROOT_CONNECTION_GROUP_IDENTIFIER; + this.parentIdentifier = ROOT_IDENTIFIER; this.name = connectionGroup.getName(); this.type = connectionGroup.getType(); @@ -144,4 +162,52 @@ public class APIConnectionGroup { this.type = type; } + /** + * Returns a collection of all child connection groups, or null if children + * have not been queried. + * + * @return + * A collection of all child connection groups, or null if children + * have not been queried. + */ + public Collection getChildConnectionGroups() { + return childConnectionGroups; + } + + /** + * Sets the collection of all child connection groups to the given + * collection, which may be null if children have not been queried. + * + * @param childConnectionGroups + * The collection containing all child connection groups of this + * connection group, or null if children have not been queried. + */ + public void setChildConnectionGroups(Collection childConnectionGroups) { + this.childConnectionGroups = childConnectionGroups; + } + + /** + * Returns a collection of all child connections, or null if children have + * not been queried. + * + * @return + * A collection of all child connections, or null if children have not + * been queried. + */ + public Collection getChildConnections() { + return childConnections; + } + + /** + * Sets the collection of all child connections to the given collection, + * which may be null if children have not been queried. + * + * @param childConnections + * The collection containing all child connections of this connection + * group, or null if children have not been queried. + */ + public void setChildConnections(Collection childConnections) { + this.childConnections = childConnections; + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java index aa04a5f60..46a37c5e0 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java @@ -23,7 +23,8 @@ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; import com.google.inject.Inject; -import java.util.List; +import java.util.ArrayList; +import java.util.Collection; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -34,15 +35,19 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response.Status; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; +import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; +import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; -import org.glyptodon.guacamole.net.basic.rest.HTTPException; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.glyptodon.guacamole.net.basic.rest.connection.APIConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +56,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/connectionGroup") +@Path("/connectionGroups") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionGroupRESTService { @@ -68,63 +73,124 @@ public class ConnectionGroupRESTService { private AuthenticationService authenticationService; /** - * A service for managing the REST endpoint APIConnection objects. - */ - @Inject - private ConnectionGroupService connectionGroupService; - - /** - * The ID that will be guaranteed to refer to the root connection group. - */ - private static final String ROOT_CONNECTION_GROUP_ID = "ROOT"; - - /** - * Gets a list of connection groups with the given ConnectionGroup parentID. - * If no parentID is provided, returns the connection groups from the root group. + * Retrieves the given connection group from the user context, including + * all descendant connections and groups if requested. + * + * @param userContext + * The user context from which to retrieve the connection group. + * + * @param identifier + * The unique identifier of the connection group to retrieve. + * + * @param includeDescendants + * Whether the descendant connections and groups of the given + * connection group should also be retrieved. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param parentID The ID of the ConnectionGroup the connection groups - * belong to. If null, the root connection group will be used. - * @return The connection list. - * @throws GuacamoleException If a problem is encountered while listing connection groups. + * @param permission + * The permission the current user must have for a connection or + * connection group to be returned in the results, if any. If null + * is specified, no filtering by permission will be performed. + * + * @return + * The requested connection group, or null if no such connection group + * exists. + * + * @throws GuacamoleException + * If an error occurs while retrieving the requested connection group + * or any of its descendants. */ - @GET - @AuthProviderRESTExposure - public List getConnectionGroups(@QueryParam("token") String authToken, @QueryParam("parentID") String parentID) + private APIConnectionGroup retrieveConnectionGroup(UserContext userContext, + String identifier, boolean includeDescendants, ObjectPermission.Type permission) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + User self = userContext.self(); + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - // If the parent connection group is passed in, try to find it. - ConnectionGroup parentConnectionGroup; - if (parentID == null) - parentConnectionGroup = userContext.getRootConnectionGroup(); + ConnectionGroup connectionGroup; + + // Use root group if requested + if (identifier == null || identifier.equals(APIConnectionGroup.ROOT_IDENTIFIER)) + connectionGroup = rootGroup; + // Otherwise, query requested group using root group directory else { - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); - parentConnectionGroup = connectionGroupDirectory.get(parentID); + + Directory connectionGroupDirectory = + rootGroup.getConnectionGroupDirectory(); + + // Get the connection group from root directory + connectionGroup = connectionGroupDirectory.get(identifier); + if (connectionGroup == null) + return null; + } - if (parentConnectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No connection group found with the provided parentID."); + // Wrap queried connection group + APIConnectionGroup apiConnectionGroup = new APIConnectionGroup(connectionGroup); - Directory connectionGroupDirectory = - parentConnectionGroup.getConnectionGroupDirectory(); + // Recursively query all descendants if necessary + if (includeDescendants) { + + // Query all child connections + Collection apiConnections = new ArrayList(); + Directory connectionDirectory = connectionGroup.getConnectionDirectory(); + + for (String childIdentifier : connectionDirectory.getIdentifiers()) { + + // Pull current connection - silently ignore if connection was removed prior to read + Connection childConnection = connectionDirectory.get(childIdentifier); + if (childConnection == null) + continue; + + // Filter based on permission, if requested + if (permission == null || self.hasPermission(new ConnectionPermission(permission, childIdentifier))) + apiConnections.add(new APIConnection(childConnection)); + + } + + // Associate child connections with current connection group + apiConnectionGroup.setChildConnections(apiConnections); + + // Query all child connection groups + Collection apiConnectionGroups = new ArrayList(); + Directory groupDirectory = connectionGroup.getConnectionGroupDirectory(); + + for (String childIdentifier : groupDirectory.getIdentifiers()) { + + // Pull current connection group - silently ignore if connection group was removed prior to read + APIConnectionGroup childConnectionGroup = retrieveConnectionGroup(userContext, childIdentifier, true, permission); + if (childConnectionGroup == null) + continue; + + apiConnectionGroups.add(childConnectionGroup); + + } + + // Associate child groups with current connection group + apiConnectionGroup.setChildConnectionGroups(apiConnectionGroups); + + } + + // Return the connectiion group + return apiConnectionGroup; - // Return the converted connection group list - return connectionGroupService.convertConnectionGroupList(connectionGroupDirectory); } - + /** * Gets an individual connection group. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionGroupID The ID of the ConnectionGroup. - * @return The connection group. - * @throws GuacamoleException If a problem is encountered while retrieving the connection group. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionGroupID + * The ID of the connection group to retrieve. + * + * @return + * The connection group, without any descendants. + * + * @throws GuacamoleException + * If a problem is encountered while retrieving the connection group. */ @GET @Path("/{connectionGroupID}") @@ -133,34 +199,70 @@ public class ConnectionGroupRESTService { @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the connection group directory - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - - // Return the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) - return new APIConnectionGroup(rootGroup); - - Directory connectionGroupDirectory = - rootGroup.getConnectionGroupDirectory(); - // Get the connection group - ConnectionGroup connectionGroup = connectionGroupDirectory.get(connectionGroupID); + // Retrieve requested connection group only + APIConnectionGroup connectionGroup = retrieveConnectionGroup(userContext, connectionGroupID, false, null); if (connectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided ID."); + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + connectionGroupID + "\""); - // Return the connectiion group - return new APIConnectionGroup(connectionGroup); + return connectionGroup; } - + + /** + * Gets an individual connection group and all children. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionGroupID + * The ID of the connection group to retrieve. + * + * @param permission + * If specified, limit the returned list to only those connections for + * which the current user has the given permission. Otherwise, all + * visible connections are returned. Connection groups are unaffected + * by this parameter. + * + * @return + * The requested connection group, including all descendants. + * + * @throws GuacamoleException + * If a problem is encountered while retrieving the connection group or + * its descendants. + */ + @GET + @Path("/{connectionGroupID}/tree") + @AuthProviderRESTExposure + public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken, + @PathParam("connectionGroupID") String connectionGroupID, + @QueryParam("permission") ObjectPermission.Type permission) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Retrieve requested connection group and all descendants + APIConnectionGroup connectionGroup = retrieveConnectionGroup(userContext, connectionGroupID, true, permission); + if (connectionGroup == null) + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + connectionGroupID + "\""); + + return connectionGroup; + + } + /** * Deletes an individual connection group. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionGroupID The ID of the ConnectionGroup to delete. - * @throws GuacamoleException If a problem is encountered while deleting the connection group. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionGroupID + * The identifier of the connection group to delete. + * + * @throws GuacamoleException + * If an error occurs while deleting the connection group. */ @DELETE @Path("/{connectionGroupID}") @@ -174,7 +276,7 @@ public class ConnectionGroupRESTService { ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); // Use the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + if (connectionGroupID != null && connectionGroupID.equals(APIConnectionGroup.ROOT_IDENTIFIER)) connectionGroupID = rootGroup.getIdentifier(); Directory connectionGroupDirectory = @@ -182,7 +284,7 @@ public class ConnectionGroupRESTService { // Make sure the connection is there before trying to delete if (connectionGroupDirectory.get(connectionGroupID) == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided ID."); + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + connectionGroupID + "\""); // Delete the connection group connectionGroupDirectory.remove(connectionGroupID); @@ -195,42 +297,49 @@ public class ConnectionGroupRESTService { * connection group with the parentID. Otherwise, the root connection group * will be used. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param parentID The ID of the ConnectionGroup the connection groups - * belong to. If null, the root connection group will be used. - * @param connectionGroup The connection group to create. - * @return The identifier of the new connection group. - * @throws GuacamoleException If a problem is encountered while creating the connection group. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionGroup + * The connection group to create. + * + * @return + * The identifier of the new connection group. + * + * @throws GuacamoleException + * If an error occurs while creating the connection group. */ @POST @AuthProviderRESTExposure - public String createConnectionGroup(@QueryParam("token") String authToken, - @QueryParam("parentID") String parentID, APIConnectionGroup connectionGroup) throws GuacamoleException { + public String createConnectionGroup(@QueryParam("token") String authToken, + APIConnectionGroup connectionGroup) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); + // Validate that connection group data was provided if (connectionGroup == null) - throw new GuacamoleClientException("A connection group is required for this request."); + throw new GuacamoleClientException("Connection group JSON must be submitted when creating connections groups."); - // If the parent connection group is passed in, try to find it. + String parentID = connectionGroup.getParentIdentifier(); + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); + + // Use root group if no parent is specified ConnectionGroup parentConnectionGroup; if (parentID == null) - parentConnectionGroup = userContext.getRootConnectionGroup(); + parentConnectionGroup = rootGroup; + // Pull specified connection group otherwise else { - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); parentConnectionGroup = connectionGroupDirectory.get(parentID); + + if (parentConnectionGroup == null) + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + parentID + "\""); } - if (parentConnectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); - - Directory connectionGroupDirectory = - parentConnectionGroup.getConnectionGroupDirectory(); - - // Create the connection group + // Add the new connection group + Directory connectionGroupDirectory = parentConnectionGroup.getConnectionGroupDirectory(); connectionGroupDirectory.add(new APIConnectionGroupWrapper(connectionGroup)); // Return the new connection group identifier @@ -239,15 +348,24 @@ public class ConnectionGroupRESTService { } /** - * Updates a connection group. + * Updates a connection group. If the parent identifier of the + * connection group is changed, the connection group will also be moved to + * the new parent group. * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionGroupID The ID of the ConnectionGroup to update. - * @param connectionGroup The connection group to update. - * @throws GuacamoleException If a problem is encountered while updating the connection group. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionGroupID + * The identifier of the existing connection group to update. + * + * @param connectionGroup + * The data to update the existing connection group with. + * + * @throws GuacamoleException + * If an error occurs while updating the connection group. */ - @POST + @PUT @Path("/{connectionGroupID}") @AuthProviderRESTExposure public void updateConnectionGroup(@QueryParam("token") String authToken, @@ -256,14 +374,15 @@ public class ConnectionGroupRESTService { UserContext userContext = authenticationService.getUserContext(authToken); + // Validate that connection group data was provided if (connectionGroup == null) - throw new GuacamoleClientException("A connection group is required for this request."); + throw new GuacamoleClientException("Connection group JSON must be submitted when updating connection groups."); // Get the connection directory ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); // Use the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + if (connectionGroupID != null && connectionGroupID.equals(APIConnectionGroup.ROOT_IDENTIFIER)) connectionGroupID = rootGroup.getIdentifier(); Directory connectionGroupDirectory = @@ -271,51 +390,11 @@ public class ConnectionGroupRESTService { // Make sure the connection group is there before trying to update if (connectionGroupDirectory.get(connectionGroupID) == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided ID."); + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + connectionGroupID + "\""); // Update the connection group connectionGroupDirectory.update(new APIConnectionGroupWrapper(connectionGroup)); } - /** - * Moves an individual connection group to a different connection group. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param connectionGroupID The ID of the ConnectionGroup to move. - * @param parentID The ID of the ConnectionGroup the connection group is to be moved to. - * @throws GuacamoleException If a problem is encountered while moving the connection group. - */ - @PUT - @Path("/{connectionGroupID}") - @AuthProviderRESTExposure - public void moveConnectionGroup(@QueryParam("token") String authToken, - @PathParam("connectionGroupID") String connectionGroupID, - @QueryParam("parentID") String parentID) throws GuacamoleException { - - UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the connection group directory - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - - // Use the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) - connectionGroupID = rootGroup.getIdentifier(); - - Directory connectionGroupDirectory = - rootGroup.getConnectionGroupDirectory(); - - // Find the new parent connection group - Directory newConnectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); - ConnectionGroup parentConnectionGroup = newConnectionGroupDirectory.get(parentID); - - if (parentConnectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided parentID."); - - // Move the connection group - connectionGroupDirectory.move(connectionGroupID, parentConnectionGroup.getConnectionGroupDirectory()); - - } - } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java deleted file mode 100644 index ecbe5fffd..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java +++ /dev/null @@ -1,59 +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.rest.connectiongroup; - -import java.util.ArrayList; -import java.util.List; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; - -/** - * A service for performing useful manipulations on REST ConnectionGroups. - * - * @author James Muehlner - */ -public class ConnectionGroupService { - - /** - * Converts a ConnectionGroup directory to a list of APIConnectionGroup - * objects for exposing with the REST endpoints. - * - * @param connectionGroupDirectory The ConnectionGroup Directory to convert for REST endpoint use. - * @return A List of APIConnectionGroup objects for use with the REST endpoint. - * @throws GuacamoleException If an error occurs while converting the - * connection group directory. - */ - public List convertConnectionGroupList( - Directory connectionGroupDirectory) throws GuacamoleException { - - List restConnectionGroups = new ArrayList(); - - for (String connectionGroupID : connectionGroupDirectory.getIdentifiers()) - restConnectionGroups.add(new APIConnectionGroup(connectionGroupDirectory.get(connectionGroupID))); - - return restConnectionGroups; - - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermission.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermission.java deleted file mode 100644 index ee97de0fd..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermission.java +++ /dev/null @@ -1,228 +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.rest.permission; - -import org.codehaus.jackson.annotate.JsonIgnoreProperties; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.glyptodon.guacamole.net.auth.permission.ConnectionGroupPermission; -import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; -import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; -import org.glyptodon.guacamole.net.auth.permission.Permission; -import org.glyptodon.guacamole.net.auth.permission.SystemPermission; -import org.glyptodon.guacamole.net.auth.permission.UserPermission; - -/** - * A simple user permission to expose through the REST endpoints. - * - * @author James Muehlner - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) -public class APIPermission { - - /** - * Create an empty APIPermission. - */ - public APIPermission() {} - - /** - * The type of object that this permission refers to. - */ - private ObjectType objectType; - - /** - * The type of object that a permission can refer to. - */ - public enum ObjectType { - - /** - * A normal connection. - */ - CONNECTION, - - /** - * A connection group. - */ - CONNECTION_GROUP, - - /** - * A Guacamole user. - */ - USER, - - /** - * The Guacamole system itself. - */ - SYSTEM - - } - - /** - * The identifier of the object that this permission refers to. - */ - private String objectIdentifier; - - /** - * The object permission type for this APIPermission, if relevant. This is - * only used if this.objectType is CONNECTION, CONNECTION_GROUP, or USER. - */ - private ObjectPermission.Type objectPermissionType; - - /** - * The system permission type for this APIPermission, if relevant. This is - * only used if this.objectType is SYSTEM. - */ - private SystemPermission.Type systemPermissionType; - - /** - * Create an APIConnection from a Connection record. - * - * @param permission The permission to create this APIPermission from. - */ - public APIPermission(Permission permission) { - - // Connection permission - if (permission instanceof ConnectionPermission) { - this.objectType = ObjectType.CONNECTION; - this.objectPermissionType = ((ConnectionPermission) permission).getType(); - this.objectIdentifier = ((ConnectionPermission) permission).getObjectIdentifier(); - } - - // Connection group permission - else if (permission instanceof ConnectionGroupPermission) { - this.objectType = ObjectType.CONNECTION_GROUP; - this.objectPermissionType = ((ConnectionGroupPermission) permission).getType(); - this.objectIdentifier = ((ConnectionGroupPermission) permission).getObjectIdentifier(); - } - - // User permission - else if (permission instanceof UserPermission) { - this.objectType = ObjectType.USER; - this.objectPermissionType = ((UserPermission) permission).getType(); - this.objectIdentifier = ((UserPermission) permission).getObjectIdentifier(); - } - - // System permission - else if (permission instanceof SystemPermission) { - this.objectType = ObjectType.SYSTEM; - this.systemPermissionType = ((SystemPermission) permission).getType(); - } - - } - - /** - * Returns the type of object that this permission refers to. - * - * @return The type of object that this permission refers to. - */ - public ObjectType getObjectType() { - return objectType; - } - - /** - * Set the type of object that this permission refers to. - * @param objectType The type of object that this permission refers to. - */ - public void setObjectType(ObjectType objectType) { - this.objectType = objectType; - } - - /** - * Returns a string representation of the permission type. - * - * @return A string representation of the permission type. - */ - public String getPermissionType() { - switch(this.objectType) { - case CONNECTION: - case CONNECTION_GROUP: - case USER: - return this.objectPermissionType.toString(); - case SYSTEM: - return this.systemPermissionType.toString(); - default: - return null; - } - } - - /** - * Set the permission type from a string representation of that type. - * Since it's not clear at this point whether this is an object permission or - * system permission, try to set both of them. - * - * @param permissionType The string representation of the permission type. - */ - public void setPermissionType(String permissionType) { - try { - this.objectPermissionType = ObjectPermission.Type.valueOf(permissionType); - } catch(IllegalArgumentException e) {} - - try { - this.systemPermissionType = SystemPermission.Type.valueOf(permissionType); - } catch(IllegalArgumentException e) {} - } - - /** - * Returns the identifier of the object that this permission refers to. - * - * @return The identifier of the object that this permission refers to. - */ - public String getObjectIdentifier() { - return objectIdentifier; - } - - /** - * Set the identifier of the object that this permission refers to. - * - * @param objectIdentifier The identifier of the object that this permission refers to. - */ - public void setObjectIdentifier(String objectIdentifier) { - this.objectIdentifier = objectIdentifier; - } - - /** - * Returns an org.glyptodon.guacamole.net.auth.permission.Permission - * representation of this APIPermission. - * - * @return An org.glyptodon.guacamole.net.auth.permission.Permission - * representation of this APIPermission. - */ - public Permission toPermission() { - switch(this.objectType) { - case CONNECTION: - return new ConnectionPermission - (this.objectPermissionType, this.objectIdentifier); - case CONNECTION_GROUP: - return new ConnectionGroupPermission - (this.objectPermissionType, this.objectIdentifier); - case USER: - return new UserPermission - (this.objectPermissionType, this.objectIdentifier); - case SYSTEM: - return new SystemPermission(this.systemPermissionType); - default: - return null; - } - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermissionSet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermissionSet.java new file mode 100644 index 000000000..450cbc823 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermissionSet.java @@ -0,0 +1,293 @@ +/* + * 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.rest.permission; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleServerException; +import org.glyptodon.guacamole.net.auth.permission.ConnectionGroupPermission; +import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; +import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; +import org.glyptodon.guacamole.net.auth.permission.Permission; +import org.glyptodon.guacamole.net.auth.permission.SystemPermission; +import org.glyptodon.guacamole.net.auth.permission.UserPermission; + +/** + * The set of permissions which are granted to a specific user, organized by + * object type and, if applicable, identifier. This object can be constructed + * with arbitrary permissions present, or manipulated after creation through + * the manipulation or replacement of its collections of permissions, but is + * otherwise not intended for internal use as a data structure for permissions. + * Its primary purpose is as a hierarchical format for exchanging granted + * permissions with REST clients. + */ +public class APIPermissionSet { + + /** + * Map of connection ID to the set of granted permissions. + */ + private Map> connectionPermissions = new HashMap>(); + + /** + * Map of connection group ID to the set of granted permissions. + */ + private Map> connectionGroupPermissions = new HashMap>(); + + /** + * Map of user ID to the set of granted permissions. + */ + private Map> userPermissions = new HashMap>(); + + /** + * Set of all granted system-level permissions. + */ + private EnumSet systemPermissions = EnumSet.noneOf(SystemPermission.Type.class); + + /** + * Adds the given object permission to the given map of object identifier + * to permission set. + * + * @param permissions + * The map to add the given permission to. + * + * @param permission + * The permission to add. + */ + private void addPermission(Map> permissions, ObjectPermission permission) { + + // Pull set of permissions for given object + String id = permission.getObjectIdentifier(); + EnumSet types = permissions.get(id); + + // If set does not yet exist, create it + if (types == null) { + types = EnumSet.of(permission.getType()); + permissions.put(id, types); + } + + // Otherwise, add the specified permission + else + types.add(permission.getType()); + + } + + /** + * Adds the given system-level permission to the given set of granted + * system permissions. + * + * @param permissions + * The set of system permissions to add the given permission to. + * + * @param permission + * The permission to add. + */ + private void addPermission(EnumSet permissions, SystemPermission permission) { + permissions.add(permission.getType()); + } + + /** + * Adds the given permission to the appropriate type-specific set or map of + * permissions based on the permission class. Only connection, connection + * group, user, and system permissions are supported. Unsupported + * permission types will result in a GuacamoleException being thrown. + * + * @param permission The permission to add. + * @throws GuacamoleException If the permission is of an unsupported type. + */ + private void addPermission(Permission permission) throws GuacamoleException { + + // Connection permissions + if (permission instanceof ConnectionPermission) + addPermission(connectionPermissions, (ConnectionPermission) permission); + + // Connection group permissions + else if (permission instanceof ConnectionGroupPermission) + addPermission(connectionGroupPermissions, (ConnectionGroupPermission) permission); + + // User permissions + else if (permission instanceof UserPermission) + addPermission(userPermissions, (UserPermission) permission); + + // System permissions + else if (permission instanceof SystemPermission) + addPermission(systemPermissions, (SystemPermission) permission); + + // Unknown / unsupported permission type + else + throw new GuacamoleServerException("Serialization of permission type \"" + permission.getClass() + "\" not implemented."); + + } + + /** + * Creates a new permission set which contains no granted permissions. Any + * permissions must be added by manipulating or replacing the applicable + * permission collection. + */ + public APIPermissionSet() { + } + + /** + * Creates a new permission set having the given permissions. + * + * @param permissions + * The permissions to initially store within the permission set. + * + * @throws GuacamoleException + * If any of the given permissions are of an unsupported type. + */ + public APIPermissionSet(Iterable permissions) throws GuacamoleException { + + // Add all provided permissions + for (Permission permission : permissions) + addPermission(permission); + + } + + /** + * Creates a new permission set having the given permissions. + * + * @param permissions + * The permissions to initially store within the permission set. + * + * @throws GuacamoleException + * If any of the given permissions are of an unsupported type. + */ + public APIPermissionSet(Permission... permissions) throws GuacamoleException { + + // Add all provided permissions + for (Permission permission : permissions) + addPermission(permission); + + } + + /** + * Returns a map of connection IDs to the set of permissions granted for + * that connection. If no permissions are granted to a particular + * connection, its ID will not be present as a key in the map. This map is + * mutable, and changes to this map will affect the permission set + * directly. + * + * @return + * A map of connection IDs to the set of permissions granted for that + * connection. + */ + public Map> getConnectionPermissions() { + return connectionPermissions; + } + + /** + * Returns a map of connection group IDs to the set of permissions granted + * for that connection group. If no permissions are granted to a particular + * connection group, its ID will not be present as a key in the map. This + * map is mutable, and changes to this map will affect the permission set + * directly. + * + * @return + * A map of connection group IDs to the set of permissions granted for + * that connection group. + */ + public Map> getConnectionGroupPermissions() { + return connectionGroupPermissions; + } + + /** + * Returns a map of user IDs to the set of permissions granted for that + * user. If no permissions are granted to a particular user, its ID will + * not be present as a key in the map. This map is mutable, and changes to + * to this map will affect the permission set directly. + * + * @return + * A map of user IDs to the set of permissions granted for that user. + */ + public Map> getUserPermissions() { + return userPermissions; + } + + /** + * Returns the set of granted system-level permissions. If no permissions + * are granted at the system level, this will be an empty set. This set is + * mutable, and changes to this set will affect the permission set + * directly. + * + * @return + * The set of granted system-level permissions. + */ + public EnumSet getSystemPermissions() { + return systemPermissions; + } + + /** + * Replaces the current map of connection permissions with the given map, + * which must map connection ID to its corresponding set of granted + * permissions. If a connection has no permissions, its ID must not be + * present as a key in the map. + * + * @param connectionPermissions + * The map which must replace the currently-stored map of permissions. + */ + public void setConnectionPermissions(Map> connectionPermissions) { + this.connectionPermissions = connectionPermissions; + } + + /** + * Replaces the current map of connection group permissions with the given + * map, which must map connection group ID to its corresponding set of + * granted permissions. If a connection group has no permissions, its ID + * must not be present as a key in the map. + * + * @param connectionGroupPermissions + * The map which must replace the currently-stored map of permissions. + */ + public void setConnectionGroupPermissions(Map> connectionGroupPermissions) { + this.connectionGroupPermissions = connectionGroupPermissions; + } + + /** + * Replaces the current map of user permissions with the given map, which + * must map user ID to its corresponding set of granted permissions. If a + * user has no permissions, its ID must not be present as a key in the map. + * + * @param userPermissions + * The map which must replace the currently-stored map of permissions. + */ + public void setUserPermissions(Map> userPermissions) { + this.userPermissions = userPermissions; + } + + /** + * Replaces the current set of system-level permissions with the given set. + * If no system-level permissions are granted, the empty set must be + * specified. + * + * @param systemPermissions + * The set which must replace the currently-stored set of permissions. + */ + public void setSystemPermissions(EnumSet systemPermissions) { + this.systemPermissions = systemPermissions; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java deleted file mode 100644 index 08e49812b..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java +++ /dev/null @@ -1,217 +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.rest.permission; - -import com.google.inject.Inject; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response.Status; -import org.glyptodon.guacamole.GuacamoleException; -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.auth.permission.Permission; -import org.glyptodon.guacamole.net.basic.rest.APIPatch; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; -import org.glyptodon.guacamole.net.basic.rest.HTTPException; -import org.glyptodon.guacamole.net.basic.rest.PATCH; -import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A REST Service for handling connection CRUD operations. - * - * @author James Muehlner - */ -@Path("/permission") -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -public class PermissionRESTService { - - /** - * Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger(PermissionRESTService.class); - - /** - * A service for authenticating users from auth tokens. - */ - @Inject - private AuthenticationService authenticationService; - - /** - * A service for managing the REST endpoint APIPermission objects. - */ - @Inject - private PermissionService permissionService; - - /** - * Gets a list of permissions for the user with the given userID. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param userID The ID of the user to retrieve permissions for. - * @return The permission list. - * @throws GuacamoleException If a problem is encountered while listing permissions. - */ - @GET - @Path("/{userID}") - @AuthProviderRESTExposure - public List getPermissions(@QueryParam("token") String authToken, @PathParam("userID") String userID) - throws GuacamoleException { - - UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the user - User user = userContext.getUserDirectory().get(userID); - if (user == null) - throw new HTTPException(Status.NOT_FOUND, "User not found with the provided userID."); - - return permissionService.convertPermissionList(user.getPermissions()); - - } - - /** - * Adds a permissions for a user with the given userID. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param userID The user ID to add the permission for. - * @param permission The permission to add for the user with the given userID. - * @throws GuacamoleException If a problem is encountered while adding the permission. - */ - @POST - @Path("/{userID}") - @AuthProviderRESTExposure - public void addPermission(@QueryParam("token") String authToken, - @PathParam("userID") String userID, APIPermission permission) - throws GuacamoleException { - - UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the user - User user = userContext.getUserDirectory().get(userID); - if (user == null) - throw new HTTPException(Status.NOT_FOUND, "User not found with the provided userID."); - - // Add the new permission - user.addPermission(permission.toPermission()); - userContext.getUserDirectory().update(user); - - } - - /** - * Removes a permissions for a user with the given userID. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param userID The user ID to remove the permission for. - * @param permission The permission to remove for the user with the given userID. - * @throws GuacamoleException If a problem is encountered while removing the permission. - */ - @POST - @Path("/remove/{userID}/") - @AuthProviderRESTExposure - public void removePermission(@QueryParam("token") String authToken, - @PathParam("userID") String userID, APIPermission permission) - throws GuacamoleException { - - UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the user - User user = userContext.getUserDirectory().get(userID); - if (user == null) - throw new HTTPException(Status.NOT_FOUND, "User not found with the provided userID."); - - // Remove the permission - user.removePermission(permission.toPermission()); - userContext.getUserDirectory().update(user); - - } - - - /** - * Applies a given list of permission patches. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param patches The permission patches to apply for this request. - * @throws GuacamoleException If a problem is encountered while removing the permission. - */ - @PATCH - @AuthProviderRESTExposure - public void patchPermissions(@QueryParam("token") String authToken, - List> patches) throws GuacamoleException { - - UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the user directory - Directory userDirectory = userContext.getUserDirectory(); - - // All users who have had permissions added or removed - Map modifiedUsers = new HashMap(); - - for (APIPatch patch : patches) { - - String userID = patch.getPath(); - Permission permission = patch.getValue().toPermission(); - - // See if we've already modified this user in this request - User user = modifiedUsers.get(userID); - if (user == null) - user = userDirectory.get(userID); - - if (user == null) - throw new HTTPException(Status.NOT_FOUND, "User not found with userID " + userID + "."); - - // Only the add and remove operations are supported for permissions - switch(patch.getOp()) { - case add: - user.addPermission(permission); - modifiedUsers.put(userID, user); - break; - case remove: - user.removePermission(permission); - modifiedUsers.put(userID, user); - break; - } - - } - - // Save the permission changes for all modified users - for (User user : modifiedUsers.values()) - userDirectory.update(user); - - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java deleted file mode 100644 index bdff3d84b..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java +++ /dev/null @@ -1,74 +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.rest.permission; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import org.glyptodon.guacamole.net.auth.permission.Permission; - -/** - * A service for performing useful manipulations on REST Permissions. - * - * @author James Muehlner - */ -public class PermissionService { - - /** - * Converts a list of Permission to a list of APIPermission objects for - * exposing with the REST endpoints. - * - * @param permissions The Connections to convert for REST endpoint use. - * @return A List of APIPermission objects for use with the REST endpoint. - */ - public List convertPermissionList(Iterable permissions) { - - List restPermissions = new ArrayList(); - - for(Permission permission : permissions) - restPermissions.add(new APIPermission(permission)); - - return restPermissions; - - } - - /** - * Converts a list of APIPermission to a set of Permission objects for internal - * Guacamole use. - * - * @param restPermissions The APIPermission objects from the REST endpoints. - * @return a List of Permission objects for internal Guacamole use. - */ - public Set convertAPIPermissionList(Iterable restPermissions) { - - Set permissions = new HashSet(); - - for(APIPermission restPermission : restPermissions) - permissions.add(restPermission.toPermission()); - - return permissions; - - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java index dacdf8f56..537e53c4d 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/protocol/ProtocolRESTService.java @@ -24,14 +24,15 @@ package org.glyptodon.guacamole.net.basic.rest.protocol; import com.google.inject.Inject; import java.util.Map; -import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.basic.ProtocolInfo; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,9 +41,8 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/protocol") +@Path("/protocols") @Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) public class ProtocolRESTService { /** @@ -50,6 +50,12 @@ public class ProtocolRESTService { */ private static final Logger logger = LoggerFactory.getLogger(ProtocolRESTService.class); + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + /** * Service for retrieving protocol definitions. */ @@ -59,15 +65,27 @@ public class ProtocolRESTService { /** * Gets a map of protocols defined in the system - protocol name to protocol. * - * @return The protocol map. - * @throws GuacamoleException If a problem is encountered while listing protocols. + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @return + * A map of protocol information, where each key is the unique name + * associated with that protocol. + * + * @throws GuacamoleException + * If an error occurs while retrieving the available protocols. */ @GET @AuthProviderRESTExposure - public Map getProtocols() throws GuacamoleException { + public Map getProtocols(@QueryParam("token") String authToken) throws GuacamoleException { + // Verify the given auth token is valid + authenticationService.getUserContext(authToken); + // Get and return a map of all protocols. return protocolRetrievalservice.getProtocolMap(); + } } 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 176615b1d..c9e6a6fde 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 @@ -23,25 +23,40 @@ package org.glyptodon.guacamole.net.basic.rest.user; import com.google.inject.Inject; +import java.util.ArrayList; import java.util.List; import java.util.UUID; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; 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.auth.permission.ConnectionGroupPermission; +import org.glyptodon.guacamole.net.auth.permission.ConnectionPermission; +import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; +import org.glyptodon.guacamole.net.auth.permission.Permission; +import org.glyptodon.guacamole.net.auth.permission.SystemPermission; +import org.glyptodon.guacamole.net.auth.permission.UserPermission; +import org.glyptodon.guacamole.net.basic.rest.APIPatch; +import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.add; +import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.remove; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.HTTPException; +import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.glyptodon.guacamole.net.basic.rest.permission.APIPermissionSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,7 +65,7 @@ import org.slf4j.LoggerFactory; * * @author James Muehlner */ -@Path("/user") +@Path("/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class UserRESTService { @@ -59,6 +74,30 @@ public class UserRESTService { * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(UserRESTService.class); + + /** + * The prefix of any path within an operation of a JSON patch which + * modifies the permissions of a user regarding a specific connection. + */ + private static final String CONNECTION_PERMISSION_PATCH_PATH_PREFIX = "/connectionPermissions/"; + + /** + * The prefix of any path within an operation of a JSON patch which + * modifies the permissions of a user regarding a specific connection group. + */ + private static final String CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX = "/connectionGroupPermissions/"; + + /** + * The prefix of any path within an operation of a JSON patch which + * modifies the permissions of a user regarding another, specific user. + */ + private static final String USER_PERMISSION_PATCH_PATH_PREFIX = "/userPermissions/"; + + /** + * The path of any operation within a JSON patch which modifies the + * permissions of a user regarding the entire system. + */ + private static final String SYSTEM_PERMISSION_PATCH_PATH = "/systemPermissions"; /** * A service for authenticating users from auth tokens. @@ -67,44 +106,73 @@ public class UserRESTService { private AuthenticationService authenticationService; /** - * A service for managing the REST endpoint APIPermission objects. - */ - @Inject - private UserService userService; - - /** - * Gets a list of users in the system. - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @return The user list. - * @throws GuacamoleException If a problem is encountered while listing users. + * Gets a list of users in the system, filtering the returned list by the + * given permission, if specified. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param permission + * If specified, limit the returned list to only those users for whom + * the current user has the given permission. Otherwise, all visible + * users are returned. + * + * @return + * A list of all visible users. If a permission was specified, this + * list will contain only those users for whom the current user has + * that permission. + * + * @throws GuacamoleException + * If an error is encountered while retrieving users. */ @GET @AuthProviderRESTExposure - public List getUsers(@QueryParam("token") String authToken) throws GuacamoleException { + public List getUsers(@QueryParam("token") String authToken, + @QueryParam("permission") UserPermission.Type permission) + throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); + User self = userContext.self(); // Get the directory Directory userDirectory = userContext.getUserDirectory(); - // Convert and return the user directory listing - return userService.convertUserList(userDirectory); + List users = new ArrayList(); + + // Add all users matching the given permission filter + for (String username : userDirectory.getIdentifiers()) { + + if (permission == null || self.hasPermission(new UserPermission(permission, username))) + users.add(new APIUser(userDirectory.get(username))); + + } + + // Return the user directory listing + return users; } /** - * Gets an individual user. - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param userID The ID of the user to retrieve. - * @return user The user. - * @throws GuacamoleException If a problem is encountered while retrieving the user. + * Retrieves an individual user. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param username + * The username of the user to retrieve. + * + * @return user + * The user having the given username. + * + * @throws GuacamoleException + * If an error occurs while retrieving the user. */ @GET - @Path("/{userID}") + @Path("/{username}") @AuthProviderRESTExposure - public APIUser getUser(@QueryParam("token") String authToken, @PathParam("userID") String userID) + public APIUser getUser(@QueryParam("token") String authToken, @PathParam("username") String username) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); @@ -113,9 +181,9 @@ public class UserRESTService { Directory userDirectory = userContext.getUserDirectory(); // Get the user - User user = userDirectory.get(userID); + User user = userDirectory.get(username); if (user == null) - throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); // Return the user return new APIUser(user); @@ -154,16 +222,25 @@ public class UserRESTService { /** * Updates an individual existing user. - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param userID The unique identifier of the user to update. - * @param user The updated user. - * @throws GuacamoleException If a problem is encountered while updating the user. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param username + * The username of the user to update. + * + * @param user + * The data to update the user with. + * + * @throws GuacamoleException + * If an error occurs while updating the user. */ - @POST - @Path("/{userID}") + @PUT + @Path("/{username}") @AuthProviderRESTExposure - public void updateUser(@QueryParam("token") String authToken, @PathParam("userID") String userID, APIUser user) + public void updateUser(@QueryParam("token") String authToken, + @PathParam("username") String username, APIUser user) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); @@ -171,37 +248,43 @@ public class UserRESTService { // Get the directory Directory userDirectory = userContext.getUserDirectory(); - if (!user.getUsername().equals(userID)) - throw new HTTPException(Response.Status.BAD_REQUEST, "Username does not match provided userID."); + // Validate data and path are sane + if (!user.getUsername().equals(username)) + throw new HTTPException(Response.Status.BAD_REQUEST, + "Username in path does not match username provided JSON data."); // Get the user - User existingUser = userDirectory.get(userID); + User existingUser = userDirectory.get(username); if (existingUser == null) - throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); // Do not update the user password if no password was provided - if (user.getPassword() != null) { - /* - * Update the user with the permission set from the existing user - * since the user REST endpoints do not expose permissions. - */ + if (user.getPassword() != null) existingUser.setPassword(user.getPassword()); - userDirectory.update(existingUser); - } + + // Update the user + userDirectory.update(existingUser); } /** * Deletes an individual existing user. - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param userID The unique identifier of the user to delete. - * @throws GuacamoleException If a problem is encountered while deleting the user. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param username + * The username of the user to delete. + * + * @throws GuacamoleException + * If an error occurs while deleting the user. */ @DELETE - @Path("/{userID}") + @Path("/{username}") @AuthProviderRESTExposure - public void deleteUser(@QueryParam("token") String authToken, @PathParam("userID") String userID) + public void deleteUser(@QueryParam("token") String authToken, + @PathParam("username") String username) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); @@ -210,12 +293,168 @@ public class UserRESTService { Directory userDirectory = userContext.getUserDirectory(); // Get the user - User existingUser = userDirectory.get(userID); + User existingUser = userDirectory.get(username); if (existingUser == null) - throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); // Delete the user - userDirectory.remove(userID); + userDirectory.remove(username); + + } + + /** + * Gets a list of permissions for the user with the given username. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param username + * The username of the user to retrieve permissions for. + * + * @return + * A list of all permissions granted to the specified user. + * + * @throws GuacamoleException + * If an error occurs while retrieving permissions. + */ + @GET + @Path("/{username}/permissions") + @AuthProviderRESTExposure + public APIPermissionSet getPermissions(@QueryParam("token") String authToken, + @PathParam("username") String username) + throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the user + User user = userContext.getUserDirectory().get(username); + if (user == null) + throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); + + return new APIPermissionSet(user.getPermissions()); + + } + + /** + * Applies a given list of permission patches. Each patch specifies either + * an "add" or a "remove" operation for a permission type, represented by + * a string. Valid permission types depend on the path of each patch + * operation, as the path dictates the permission being modified, such as + * "/connectionPermissions/42" or "/systemPermissions". + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param username + * The username of the user to modify the permissions of. + * + * @param patches + * The permission patches to apply for this request. + * + * @throws GuacamoleException + * If a problem is encountered while modifying permissions. + */ + @PATCH + @Path("/{username}/permissions") + @AuthProviderRESTExposure + public void patchPermissions(@QueryParam("token") String authToken, + @PathParam("username") String username, + List> patches) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Get the user directory + Directory userDirectory = userContext.getUserDirectory(); + + // Get the user + User user = userContext.getUserDirectory().get(username); + if (user == null) + throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); + + // Apply all patch operations individually + for (APIPatch patch : patches) { + + Permission permission; + + String path = patch.getPath(); + + // Create connection permission if path has connection prefix + if (path.startsWith(CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) { + + // Get identifier and type from patch operation + String identifier = path.substring(CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length()); + ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); + + // Create corresponding permission + permission = new ConnectionPermission(type, identifier); + + } + + // Create connection group permission if path has connection group prefix + else if (path.startsWith(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX)) { + + // Get identifier and type from patch operation + String identifier = path.substring(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX.length()); + ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); + + // Create corresponding permission + permission = new ConnectionGroupPermission(type, identifier); + + } + + // Create user permission if path has user prefix + else if (path.startsWith(USER_PERMISSION_PATCH_PATH_PREFIX)) { + + // Get identifier and type from patch operation + String identifier = path.substring(USER_PERMISSION_PATCH_PATH_PREFIX.length()); + ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); + + // Create corresponding permission + permission = new UserPermission(type, identifier); + + } + + // Create system permission if path is system path + else if (path.startsWith(SYSTEM_PERMISSION_PATCH_PATH)) { + + // Get identifier and type from patch operation + SystemPermission.Type type = SystemPermission.Type.valueOf(patch.getValue()); + + // Create corresponding permission + permission = new SystemPermission(type); + + } + + // Otherwise, the path is not supported + else + throw new HTTPException(Status.BAD_REQUEST, "Unsupported patch path: \"" + path + "\""); + + // Add or remove permission based on operation + switch (patch.getOp()) { + + // Add permission + case add: + user.addPermission(permission); + break; + + // Remove permission + case remove: + user.removePermission(permission); + break; + + // Unsupported patch operation + default: + throw new HTTPException(Status.BAD_REQUEST, + "Unsupported patch operation: \"" + patch.getOp() + "\""); + + } + + } // end for each patch operation + + // Save the permission changes + userDirectory.update(user); } diff --git a/guacamole/src/main/webapp/app/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js index aafe92d2d..2070b25aa 100644 --- a/guacamole/src/main/webapp/app/auth/service/authenticationService.js +++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js @@ -28,20 +28,33 @@ angular.module('auth').factory('authenticationService', ['$http', '$cookieStore' var service = {}; + /** + * The unique identifier of the local cookie which stores the user's + * current authentication token and user ID. + * + * @type String + */ var AUTH_COOKIE_ID = "GUAC_AUTH"; /** * Makes a request to authenticate a user using the token REST API endpoint, - * returning a promise that can be used for processing the results of the call. + * returning a promise that succeeds only if the login operation was + * successful. The resulting authentication data can be retrieved later + * via getCurrentToken() or getCurrentUserID(). * - * @param {String} username The username to log in with. - * @param {String} password The password to log in with. - * @returns {Promise} A promise for the HTTP call. + * @param {String} username + * The username to log in with. + * + * @param {String} password + * The password to log in with. + * + * @returns {Promise} + * A promise which succeeds only if the login operation was successful. */ service.login = function login(username, password) { return $http({ method: 'POST', - url: 'api/token', + url: 'api/tokens', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -59,14 +72,17 @@ angular.module('auth').factory('authenticationService', ['$http', '$cookieStore' /** * Makes a request to logout a user using the login REST API endpoint, - * returning a promise that can be used for processing the results of the call. + * returning a promise succeeds only if the logout operation was + * successful. * - * @returns {Promise} A promise for the HTTP call. + * @returns {Promise} + * A promise which succeeds only if the logout operation was + * successful. */ service.logout = function logout() { return $http({ method: 'DELETE', - url: 'api/token/' + encodeURIComponent(service.getCurrentToken()) + url: 'api/tokens/' + encodeURIComponent(service.getCurrentToken()) }); }; diff --git a/guacamole/src/main/webapp/app/client/clientModule.js b/guacamole/src/main/webapp/app/client/clientModule.js index dec05e580..c27957115 100644 --- a/guacamole/src/main/webapp/app/client/clientModule.js +++ b/guacamole/src/main/webapp/app/client/clientModule.js @@ -23,4 +23,4 @@ /** * The module for code used to connect to a connection or balancing group. */ -angular.module('client', ['auth', 'history']); +angular.module('client', ['auth', 'history', 'rest']); diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index be93a559b..5e134f64d 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -132,10 +132,10 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', remaining: 15 }; - // Get DAO for reading connections and groups - var connectionGroupDAO = $injector.get('connectionGroupDAO'); - var connectionService = $injector.get('connectionService'); - var ClientProperties = $injector.get('ClientProperties'); + // Get services for reading connections and groups + var connectionGroupService = $injector.get('connectionGroupService'); + var connectionService = $injector.get('connectionService'); + var ClientProperties = $injector.get('ClientProperties'); // Client settings and state $scope.clientProperties = new ClientProperties(); @@ -176,7 +176,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', // Connection group case 'g': - connectionGroupDAO.getConnectionGroup($routeParams.id).success(function (group) { + connectionGroupService.getConnectionGroup($routeParams.id).success(function (group) { $scope.connectionName = $scope.page.title = group.name; }); break; diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 2fb05307a..476c5ae11 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -315,15 +315,18 @@ angular.module('client').directive('guacClient', [function guacClient() { * CONNECT / RECONNECT */ - // Connect to given ID whenever ID changes - $scope.$watch('id', function(id, previousID) { - - // If a client is already attached, ensure it is disconnected - if (client) - client.disconnect(); + /** + * Store the thumbnail of the currently connected client within + * the connection history under the given ID. If the client is not + * connected, or if no ID is given, this function has no effect. + * + * @param {String} id + * The ID of the history entry to update. + */ + var updateHistoryEntry = function updateHistoryEntry(id) { // Update stored thumbnail of previous connection - if (previousID && display && display.getWidth() > 0 && display.getHeight() > 0) { + if (id && display && display.getWidth() > 0 && display.getHeight() > 0) { // Get screenshot var canvas = display.flatten(); @@ -343,10 +346,22 @@ angular.module('client').directive('guacClient', [function guacClient() { 0, 0, thumbnail.width, thumbnail.height ); - guacHistory.updateThumbnail(previousID, thumbnail.toDataURL("image/png")); + guacHistory.updateThumbnail(id, thumbnail.toDataURL("image/png")); } + }; + + // Connect to given ID whenever ID changes + $scope.$watch('id', function(id, previousID) { + + // If a client is already attached, ensure it is disconnected + if (client) + client.disconnect(); + + // Update stored thumbnail of previous connection + updateHistoryEntry(previousID); + // Only proceed if a new client is attached if (!id) return; @@ -385,6 +400,14 @@ angular.module('client').directive('guacClient', [function guacClient() { }); + // Clean up when client directive is destroyed + $scope.$on('$destroy', function destroyClient() { + + // Update stored thumbnail of current connection + updateHistoryEntry($scope.id); + + }); + /* * MOUSE EMULATION */ diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 74eeb4b7b..4402a0bbc 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -33,10 +33,7 @@
-
- - -
+