From ea7e88279effab70a21b57f6fde504fa57ed3c9c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Mar 2015 11:57:08 -0700 Subject: [PATCH 1/4] GUAC-1132: Allow batch retrieval/deletion of tunnels. --- .../guacamole/auth/jdbc/user/UserContext.java | 18 ++++++++-- .../guacamole/net/auth/UserContext.java | 18 +++++----- .../net/auth/simple/SimpleUserContext.java | 4 +-- .../basic/rest/tunnel/TunnelRESTService.java | 36 +++++++++---------- 4 files changed, 45 insertions(+), 31 deletions(-) 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..4713d1166 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,6 +24,7 @@ package org.glyptodon.guacamole.net.basic.rest.tunnel; import com.google.inject.Inject; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -34,8 +35,6 @@ 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.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; @@ -103,37 +102,38 @@ public class TunnelRESTService { } /** - * Deletes the tunnel having the given UUID, effectively closing the - * tunnel and killing the associated connection. + * Deletes the tunnels having the given UUIDs, effectively closing the + * tunnels and killing the associated connections. * * @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 tunnelUUIDs + * The UUIDs associated with the tunnels being deleted. * * @throws GuacamoleException - * If an error occurs while deleting the tunnel. + * If an error occurs while deleting the tunnels. */ @DELETE - @Path("/{tunnelUUID}") + @Path("/") @AuthProviderRESTExposure - public void deleteTunnel(@QueryParam("token") String authToken, - @PathParam("tunnelUUID") String tunnelUUID) + public void deleteTunnels(@QueryParam("token") String authToken, + @QueryParam("tunnelUUID") Collection tunnelUUIDs) throws GuacamoleException { + // Attempt to get all requested tunnels UserContext userContext = authenticationService.getUserContext(authToken); + Collection records = userContext.getActiveConnections(tunnelUUIDs); - // Retrieve specified tunnel - ConnectionRecord record = userContext.getActiveConnection(tunnelUUID); - if (record == null) - throw new GuacamoleResourceNotFoundException("No such tunnel: \"" + tunnelUUID + "\""); + // Close each tunnel, if not already closed + for (ConnectionRecord record : records) { - // Close tunnel, if not already closed - GuacamoleTunnel tunnel = record.getTunnel(); - if (tunnel != null && tunnel.isOpen()) - tunnel.close(); + GuacamoleTunnel tunnel = record.getTunnel(); + if (tunnel != null && tunnel.isOpen()) + tunnel.close(); + + } } From dd78341cbd3da169056a54fbef8a7414638f4cff Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Mar 2015 12:49:03 -0700 Subject: [PATCH 2/4] GUAC-1132: Implement session deletion within management UI. --- .../basic/rest/tunnel/TunnelRESTService.java | 52 +++++++++---- .../controllers/manageSessionsController.js | 77 +++++++++++++++---- .../app/manage/templates/manageSessions.html | 2 +- .../webapp/app/rest/services/tunnelService.js | 40 ++++++---- .../src/main/webapp/translations/en_US.json | 4 +- 5 files changed, 129 insertions(+), 46 deletions(-) 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 4713d1166..1d4c68d25 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 @@ -25,20 +25,24 @@ 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.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; @@ -73,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. @@ -81,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); + } } @@ -109,24 +116,41 @@ public class TunnelRESTService { * The authentication token that is used to authenticate the user * performing the operation. * - * @param tunnelUUIDs - * The UUIDs associated with the tunnels being deleted. + * @param patches + * The tunnel patches to apply for this request. * * @throws GuacamoleException * If an error occurs while deleting the tunnels. */ - @DELETE + @PATCH @Path("/") @AuthProviderRESTExposure - public void deleteTunnels(@QueryParam("token") String authToken, - @QueryParam("tunnelUUID") Collection tunnelUUIDs) - throws GuacamoleException { + public void patchTunnels(@QueryParam("token") String authToken, + List> patches) throws GuacamoleException { // Attempt to get all requested tunnels UserContext userContext = authenticationService.getUserContext(authToken); - Collection records = userContext.getActiveConnections(tunnelUUIDs); + // Build list of tunnels to delete + Collection tunnelUUIDs = new ArrayList(patches.size()); + for (APIPatch patch : patches) { + + // 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(); 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..98c86ab19 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html @@ -56,7 +56,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..b8e5f8039 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -236,7 +236,9 @@ "MANAGE_SESSION" : { - "ACTION_DELETE" : "Kill all selected sessions", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_DELETE" : "Kill all selected sessions", "DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions", "DIALOG_HEADER_ERROR" : "Error", From 3273cd2645fc626456945d1050547b6b5b1629bc Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Mar 2015 13:00:51 -0700 Subject: [PATCH 3/4] GUAC-1132: Flesh out stubbed session-related translation text. Mark "Kill Sessions" button as dangerous. --- .../main/webapp/app/manage/templates/manageSessions.html | 6 +++++- guacamole/src/main/webapp/translations/en_US.json | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/templates/manageSessions.html b/guacamole/src/main/webapp/app/manage/templates/manageSessions.html index 98c86ab19..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}}

- + + +
+ +
diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index b8e5f8039..0f85ddd09 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -238,12 +238,12 @@ "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_DELETE" : "Kill all selected sessions", + "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", @@ -252,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." }, From b0bfc80ab7ebf5f47d28930ad5f0a5d6d0c17405 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Mar 2015 13:05:00 -0700 Subject: [PATCH 4/4] GUAC-1132: Fix patchTunnels() documentation - it's not technically purely deletion anymore. --- .../guacamole/net/basic/rest/tunnel/TunnelRESTService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 1d4c68d25..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 @@ -109,8 +109,11 @@ public class TunnelRESTService { } /** - * Deletes the tunnels having the given UUIDs, effectively closing the - * tunnels and killing the associated connections. + * 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