diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java index b28a0c49e..ae06013ec 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.java @@ -28,6 +28,7 @@ import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ConnectionGroupDirector import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionDirectory; import com.google.inject.Inject; import com.google.inject.Provider; +import java.util.ArrayList; import java.util.Collection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; @@ -130,9 +131,22 @@ public class UserContext extends RestrictedObject } @Override - public ConnectionRecord getActiveConnection(String tunnelUUID) + public Collection getActiveConnections(Collection tunnelUUIDs) throws GuacamoleException { - return tunnelService.getActiveConnection(getCurrentUser(), tunnelUUID); + + // Look up active connections for each given tunnel UUID + Collection records = new ArrayList(tunnelUUIDs.size()); + for (String tunnelUUID : tunnelUUIDs) { + + // Add corresponding record only if it exists + ConnectionRecord record = tunnelService.getActiveConnection(getCurrentUser(), tunnelUUID); + if (record != null) + records.add(record); + + } + + return records; + } } diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/UserContext.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/UserContext.java index 7c52834de..bad5901fe 100644 --- a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/UserContext.java +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/UserContext.java @@ -113,24 +113,24 @@ public interface UserContext { throws GuacamoleException; /** - * Returns the connection record associated with the active connection - * having the tunnel with the given UUID. The active connection will only + * Returns the connection records associated with the active connections + * having the tunnels with the given UUIDs. An active connection will only * be returned if the current user has access. * - * @param tunnelUUID - * The UUID of the tunnel whose associated connection record should be - * returned. + * @param tunnelUUIDs + * The UUIDs of the tunnels whose associated connection records should + * be returned. * * @return - * The connection record associated with the active connection having - * the tunnel with the given UUID, if any, or null if no such - * connection exists. + * A collection of all connection records associated with the active + * connections having the tunnels with the given UUIDs, if any, or an + * empty collection if no such connections exist. * * @throws GuacamoleException * If an error occurs while reading active connection records, or if * permission is denied. */ - ConnectionRecord getActiveConnection(String tunnelUUID) + Collection getActiveConnections(Collection tunnelUUIDs) throws GuacamoleException; } diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUserContext.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUserContext.java index 480c79017..3f6b45fe2 100644 --- a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUserContext.java +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUserContext.java @@ -175,9 +175,9 @@ public class SimpleUserContext implements UserContext { } @Override - public ConnectionRecord getActiveConnection(String tunnelUUID) + public Collection getActiveConnections(Collection tunnelUUID) throws GuacamoleException { - return null; + return Collections.EMPTY_LIST; } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java index 231f8586a..91641eadd 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/tunnel/TunnelRESTService.java @@ -24,22 +24,25 @@ package org.glyptodon.guacamole.net.basic.rest.tunnel; import com.google.inject.Inject; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; import javax.ws.rs.GET; 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 org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.APIPatch; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; +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; @@ -74,7 +77,8 @@ public class TunnelRESTService { * performing the operation. * * @return - * The tunnels of all active connections visible to the current user. + * A map of the tunnels of all active connections visible to the + * current user, where the key of each entry is the tunnel's UUID. * * @throws GuacamoleException * If an error occurs while retrieving the tunnels. @@ -82,19 +86,21 @@ public class TunnelRESTService { @GET @Path("/") @AuthProviderRESTExposure - public List getTunnels(@QueryParam("token") String authToken) + public Map getTunnels(@QueryParam("token") String authToken) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); // Retrieve all active tunnels - List apiTunnels = new ArrayList(); + Map apiTunnels = new HashMap(); for (ConnectionRecord record : userContext.getActiveConnections()) { // Locate associated tunnel and UUID GuacamoleTunnel tunnel = record.getTunnel(); - if (tunnel != null) - apiTunnels.add(new APITunnel(record, tunnel.getUUID().toString())); + if (tunnel != null) { + APITunnel apiTunnel = new APITunnel(record, tunnel.getUUID().toString()); + apiTunnels.put(apiTunnel.getUUID(), apiTunnel); + } } @@ -103,37 +109,58 @@ public class TunnelRESTService { } /** - * Deletes the tunnel having the given UUID, effectively closing the - * tunnel and killing the associated connection. + * Applies the given tunnel patches. This operation currently only supports + * deletion of tunnels through the "remove" patch operation. Deleting a + * tunnel effectively closing the tunnel and kills the associated + * connection. The path of each patch operation is of the form "/UUID" + * where UUID is the UUID of the tunnel being modified. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * - * @param tunnelUUID - * The UUID associated with the tunnel being deleted. + * @param patches + * The tunnel patches to apply for this request. * * @throws GuacamoleException - * If an error occurs while deleting the tunnel. + * If an error occurs while deleting the tunnels. */ - @DELETE - @Path("/{tunnelUUID}") + @PATCH + @Path("/") @AuthProviderRESTExposure - public void deleteTunnel(@QueryParam("token") String authToken, - @PathParam("tunnelUUID") String tunnelUUID) - throws GuacamoleException { + public void patchTunnels(@QueryParam("token") String authToken, + List> patches) throws GuacamoleException { + // Attempt to get all requested tunnels UserContext userContext = authenticationService.getUserContext(authToken); - // Retrieve specified tunnel - ConnectionRecord record = userContext.getActiveConnection(tunnelUUID); - if (record == null) - throw new GuacamoleResourceNotFoundException("No such tunnel: \"" + tunnelUUID + "\""); + // Build list of tunnels to delete + Collection tunnelUUIDs = new ArrayList(patches.size()); + for (APIPatch patch : patches) { - // Close tunnel, if not already closed - GuacamoleTunnel tunnel = record.getTunnel(); - if (tunnel != null && tunnel.isOpen()) - tunnel.close(); + // Only remove is supported + if (patch.getOp() != APIPatch.Operation.remove) + throw new GuacamoleUnsupportedException("Only the \"remove\" operation is supported when patching tunnels."); + + // Retrieve and validate path + String path = patch.getPath(); + if (!path.startsWith("/")) + throw new GuacamoleClientException("Patch paths must start with \"/\"."); + + // Add UUID + tunnelUUIDs.add(path.substring(1)); + + } + + // Close each tunnel, if not already closed + Collection records = userContext.getActiveConnections(tunnelUUIDs); + for (ConnectionRecord record : records) { + + GuacamoleTunnel tunnel = record.getTunnel(); + if (tunnel != null && tunnel.isOpen()) + tunnel.close(); + + } } diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js index 8aa49022f..3ae1e77e8 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSessionsController.js @@ -74,11 +74,11 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj $scope.connections = {}; /** - * The count of currently selected tunnel wrappers. + * Map of all currently-selected tunnel wrappers by UUID. * - * @type Number + * @type Object. */ - var selectedWrapperCount = 0; + var selectedWrappers = {}; /** * Adds the given connection to the internal set of visible @@ -126,9 +126,9 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj // Wrap all active tunnels for sake of display $scope.wrappers = []; - tunnels.forEach(function wrapActiveTunnel(tunnel) { - $scope.wrappers.push(new ActiveTunnelWrapper(tunnel)); - }); + for (var tunnelUUID in tunnels) { + $scope.wrappers.push(new ActiveTunnelWrapper(tunnels[tunnelUUID])); + } }); @@ -147,12 +147,24 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj }; + /** + * An action to be provided along with the object sent to showStatus which + * closes the currently-shown status dialog. + */ + var ACKNOWLEDGE_ACTION = { + name : "MANAGE_SESSION.ACTION_ACKNOWLEDGE", + // Handle action + callback : function acknowledgeCallback() { + guacNotification.showStatus(false); + } + }; + /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var CANCEL_ACTION = { - name : "MANAGE_USER.ACTION_CANCEL", + name : "MANAGE_SESSION.ACTION_CANCEL", // Handle action callback : function cancelCallback() { guacNotification.showStatus(false); @@ -178,7 +190,31 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj * confirmation. */ var deleteSessionsImmediately = function deleteSessionsImmediately() { - // TODO: Use a batch delete function to delete the sessions. + + // Perform deletion + tunnelService.deleteActiveTunnels(Object.keys(selectedWrappers)) + .success(function tunnelsDeleted() { + + // Remove deleted tunnels from wrapper array + $scope.wrappers = $scope.wrappers.filter(function tunnelStillExists(wrapper) { + return !(wrapper.tunnel.uuid in selectedWrappers); + }); + + // Clear selection + selectedWrappers = {}; + + }) + + // Notify of any errors + .error(function tunnelDeletionFailed(error) { + guacNotification.showStatus({ + 'className' : 'error', + 'title' : 'MANAGE_SESSION.DIALOG_HEADER_ERROR', + 'text' : error.message, + 'actions' : [ ACKNOWLEDGE_ACTION ] + }); + }); + }; /** @@ -201,20 +237,31 @@ angular.module('manage').controller('manageSessionsController', ['$scope', '$inj * true if selected sessions can be deleted, false otherwise. */ $scope.canDeleteSessions = function canDeleteSessions() { - return selectedWrapperCount > 0; + + // We can delete sessions if at least one is selected + for (var tunnelUUID in selectedWrappers) + return true; + + return false; + }; /** * Called whenever a tunnel wrapper changes selected status. * - * @param {Boolean} selected - * Whether the wrapper is now selected. + * @param {ActiveTunnelWrapper} wrapper + * The wrapper whose selected status has changed. */ - $scope.wrapperSelectionChange = function wrapperSelectionChange(selected) { - if (selected) - selectedWrapperCount++; + $scope.wrapperSelectionChange = function wrapperSelectionChange(wrapper) { + + // Add wrapper to map if selected + if (wrapper.checked) + selectedWrappers[wrapper.tunnel.uuid] = wrapper; + + // Otherwise, remove wrapper from map else - selectedWrapperCount--; + delete selectedWrappers[wrapper.tunnel.uuid]; + }; }]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html index 7bb34cf5d..144b09024 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html @@ -32,7 +32,11 @@ THE SOFTWARE.

{{'MANAGE_SESSION.HELP_SESSIONS' | translate}}

- + + +
+ +
@@ -56,7 +60,7 @@ THE SOFTWARE.
- + {{wrapper.tunnel.username}} diff --git a/guacamole/src/main/webapp/app/rest/services/tunnelService.js b/guacamole/src/main/webapp/app/rest/services/tunnelService.js index ca9c5a838..8ce18d867 100644 --- a/guacamole/src/main/webapp/app/rest/services/tunnelService.js +++ b/guacamole/src/main/webapp/app/rest/services/tunnelService.js @@ -30,12 +30,12 @@ angular.module('rest').factory('tunnelService', ['$http', 'authenticationService /** * Makes a request to the REST API to get the list of active tunnels, - * returning a promise that provides an array of @link{ActiveTunnel} + * returning a promise that provides a map of @link{ActiveTunnel} * objects if successful. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{ActiveTunnel} - * objects upon success. + * @returns {Promise.>} + * A promise which will resolve with a map of @link{ActiveTunnel} + * objects, where each key is the UUID of the corresponding tunnel. */ service.getActiveTunnels = function getActiveTunnels() { @@ -54,31 +54,41 @@ angular.module('rest').factory('tunnelService', ['$http', 'authenticationService }; /** - * Makes a request to the REST API to delete the tunnel having the given - * UUID, effectively disconnecting the tunnel, returning a promise that can - * be used for processing the results of the call. + * Makes a request to the REST API to delete the tunnels having the given + * UUIDs, effectively disconnecting the tunnels, returning a promise that + * can be used for processing the results of the call. * - * @param {String} uuid - * The UUID of the tunnel to delete. + * @param {String[]} uuids + * The UUIDs of the tunnels to delete. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ - service.deleteActiveTunnel = function deleteActiveTunnel(uuid) { + service.deleteActiveTunnels = function deleteActiveTunnels(uuids) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; - // Delete connection - return $http({ - method : 'DELETE', - url : 'api/tunnels/' + encodeURIComponent(uuid), - params : httpParameters + // Convert provided array of UUIDs to a patch + var tunnelPatch = []; + uuids.forEach(function addTunnelPatch(uuid) { + tunnelPatch.push({ + op : 'remove', + path : '/' + uuid + }); }); + // Perform tunnel deletion via PATCH + return $http({ + method : 'PATCH', + url : 'api/tunnels', + params : httpParameters, + data : tunnelPatch + }); + }; return service; diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index e19e4dfe1..0f85ddd09 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -236,12 +236,14 @@ "MANAGE_SESSION" : { - "ACTION_DELETE" : "Kill all selected sessions", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_DELETE" : "Kill Sessions", "DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions", "DIALOG_HEADER_ERROR" : "Error", - "HELP_SESSIONS" : "Click to kill a user session.", + "HELP_SESSIONS" : "All currently-active Guacamole sessions are listed here. If you wish to kill one or more sessions, check the box next to those sessions and click \"Kill Sessions\". Killing a session will immediately disconnect the user from the associated connection.", "SECTION_HEADER_SESSIONS" : "Sessions", @@ -250,7 +252,7 @@ "TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name", - "TEXT_CONFIRM_DELETE" : "Are you sure you want to kill these sessions?" + "TEXT_CONFIRM_DELETE" : "Are you sure you want to kill all selected sessions? The users using these sessions will be immediately disconnected." },