From f61f016fdf2207a3be9ccda405dc1cba664099a7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 27 Feb 2023 12:30:05 -0800 Subject: [PATCH 1/2] GUACAMOLE-1744: Provide REST API mechanism for testing session validity. NOTE: This test must not have the effect of resetting a session's last accessed time, or periodic validity tests will erroneously act as session keep-alives. --- .../app/auth/service/authenticationService.js | 37 ++++++++++++++ .../apache/guacamole/GuacamoleSession.java | 49 ++++++++++++++----- .../rest/auth/HashTokenSessionMap.java | 12 ++--- .../rest/session/SessionResource.java | 12 +++++ 4 files changed, 92 insertions(+), 18 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index ce44b184c..8a867cea5 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -261,6 +261,43 @@ angular.module('auth').factory('authenticationService', ['$injector', }; + /** + * Determines whether the session associated with a particular token is + * still valid, without performing an operation that would result in that + * session being marked as active. If no token is provided, the session of + * the current user is checked. + * + * @param {string} [token] + * The authentication token to pass with the "Guacamole-Token" header. + * If omitted, and the user is logged in, the user's current + * authentication token will be used. + * + * @returns {Promise.} + * A promise that resolves with the boolean value "true" if the session + * is valid, and resolves with the boolean value "false" otherwise, + * including if an error prevents session validity from being + * determined. The promise is never rejected. + */ + service.getValidity = function getValidity(token) { + + // NOTE: Because this is a HEAD request, we will not receive a JSON + // response body. We will only have a simple yes/no regarding whether + // the auth token can be expected to be usable. + return service.request({ + method: 'HEAD', + url: 'api/session' + }, token) + + .then(function sessionIsValid() { + return true; + }) + + ['catch'](function sessionIsNotValid() { + return false; + }); + + }; + /** * Makes a request to revoke an authentication token using the token REST * API endpoint, returning a promise that succeeds only if the token was diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java index 0458639d1..5bf0371b3 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java @@ -99,36 +99,42 @@ public class GuacamoleSession { } /** - * Returns the authenticated user associated with this session. + * Returns the authenticated user associated with this session. Invoking + * this function automatically updates this session's last access time. * * @return * The authenticated user associated with this session. */ public AuthenticatedUser getAuthenticatedUser() { + this.access(); return authenticatedUser; } /** * Replaces the authenticated user associated with this session with the - * given authenticated user. + * given authenticated user. Invoking this function automatically updates + * this session's last access time. * * @param authenticatedUser * The authenticated user to associated with this session. */ public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) { + this.access(); this.authenticatedUser = authenticatedUser; } /** * Returns a list of all UserContexts associated with this session. Each * AuthenticationProvider currently loaded by Guacamole may provide its own - * UserContext for any successfully-authenticated user. + * UserContext for any successfully-authenticated user. Invoking this + * function automatically updates this session's last access time. * * @return * An unmodifiable list of all UserContexts associated with this * session. */ public List getUserContexts() { + this.access(); return Collections.unmodifiableList(userContexts); } @@ -136,6 +142,8 @@ public class GuacamoleSession { * Returns true if all user contexts associated with this session are * valid, or false if any user context is not valid. If a session is not * valid, it may no longer be used, and invalidate() should be invoked. + * Invoking this function does not affect the last access time of this + * session. * * @return * true if all user contexts associated with this session are @@ -151,7 +159,8 @@ public class GuacamoleSession { /** * Returns the UserContext associated with this session that originated * from the AuthenticationProvider with the given identifier. If no such - * UserContext exists, an exception is thrown. + * UserContext exists, an exception is thrown. Invoking this function + * automatically updates this session's last access time. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider that created the @@ -188,20 +197,24 @@ public class GuacamoleSession { /** * Replaces all UserContexts associated with this session with the given - * List of UserContexts. + * List of UserContexts. Invoking this function automatically updates this + * session's last access time. * * @param userContexts * The List of UserContexts to associate with this session. */ public void setUserContexts(List userContexts) { + this.access(); this.userContexts = userContexts; } /** - * Returns whether this session has any associated active tunnels. + * Returns whether this session has any associated active tunnels. Invoking + * this function does not affect the last access time of this session. * - * @return true if this session has any associated active tunnels, - * false otherwise. + * @return + * true if this session has any associated active tunnels, false + * otherwise. */ public boolean hasTunnels() { return !tunnels.isEmpty(); @@ -214,10 +227,14 @@ public class GuacamoleSession { * session. A tunnel need not be present here to be used by the user * associated with this session, but tunnels not in this set will not * be taken into account when determining whether a session is in use. + * Invoking this function automatically updates this session's last access + * time. * - * @return A map of all active tunnels associated with this session. + * @return + * A map of all active tunnels associated with this session. */ public Map getTunnels() { + this.access(); return tunnels; } @@ -228,16 +245,23 @@ public class GuacamoleSession { * @param tunnel The tunnel to associate with this session. */ public void addTunnel(UserTunnel tunnel) { + this.access(); tunnels.put(tunnel.getUUID().toString(), tunnel); } /** * Disassociates the tunnel having the given UUID from this session. + * Invoking this function automatically updates this session's last access + * time. * - * @param uuid The UUID of the tunnel to disassociate from this session. - * @return true if the tunnel existed and was removed, false otherwise. + * @param uuid + * The UUID of the tunnel to disassociate from this session. + * + * @return + * true if the tunnel existed and was removed, false otherwise. */ public boolean removeTunnel(String uuid) { + this.access(); return tunnels.remove(uuid) != null; } @@ -251,7 +275,8 @@ public class GuacamoleSession { /** * Returns the time this session was last accessed, as the number of * milliseconds since midnight January 1, 1970 GMT. Session access must - * be explicitly marked through calls to the access() function. + * be explicitly marked through calls to the access() function. Invoking + * this function does not affect the last access time of this session. * * @return The time this session was last accessed. */ diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java index 15d11eb62..cabbb78ee 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java @@ -208,12 +208,12 @@ public class HashTokenSessionMap implements TokenSessionMap { if (authToken == null) return null; - // Update the last access time and return the GuacamoleSession - GuacamoleSession session = sessionMap.get(authToken); - if (session != null) - session.access(); - - return session; + // Return the GuacamoleSession having the given auth token (NOTE: We + // do not update the access time here, as it is necessary to be able + // to retrieve and check the session without causing that session to + // be marked as active. Instead, those updates occur as needed when + // functions within the GuacamoleSession are invoked.) + return sessionMap.get(authToken); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java index 6116c3785..adf8ed687 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java @@ -24,6 +24,7 @@ import com.google.inject.assistedinject.AssistedInject; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.HEAD; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -184,4 +185,15 @@ public class SessionResource { } + /** + * Tests whether this session resource represented a valid session at the + * time it was created. This function always succeeds. It is possible for + * an HTTP request aimed at this operation to fail, but that failure occurs + * further up the chain when locating the session. + */ + @HEAD + public void checkValidity() { + // Do nothing + } + } From d10c35396e97f5603aafacc7eb9f738ba0eb951b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 27 Feb 2023 12:31:51 -0800 Subject: [PATCH 2/2] GUACAMOLE-1744: Automatically reset UI if session is no longer valid. --- .../app/index/controllers/indexController.js | 72 +++++++++++++++++-- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js index 77f67db2f..232e0495d 100644 --- a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js +++ b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js @@ -23,14 +23,30 @@ angular.module('index').controller('indexController', ['$scope', '$injector', function indexController($scope, $injector) { + /** + * The number of milliseconds that should elapse between client-side + * session checks. This DOES NOT impact whether a session expires at all; + * such checks will always be server-side. This only affects how quickly + * the client-side view can recognize that a user's session has expired + * absent any action taken by the user. + * + * @type {!number} + */ + const SESSION_VALIDITY_RECHECK_INTERVAL = 15000; + + // Required types + const ManagedClientState = $injector.get('ManagedClientState'); + // Required services - const $document = $injector.get('$document'); - const $location = $injector.get('$location'); - const $route = $injector.get('$route'); - const $window = $injector.get('$window'); - const clipboardService = $injector.get('clipboardService'); - const guacNotification = $injector.get('guacNotification'); - const guacClientManager = $injector.get('guacClientManager'); + const $document = $injector.get('$document'); + const $interval = $injector.get('$interval'); + const $location = $injector.get('$location'); + const $route = $injector.get('$route'); + const $window = $injector.get('$window'); + const authenticationService = $injector.get('authenticationService'); + const clipboardService = $injector.get('clipboardService'); + const guacNotification = $injector.get('guacNotification'); + const guacClientManager = $injector.get('guacClientManager'); /** * The error that prevents the current page from rendering at all. If no @@ -202,6 +218,48 @@ angular.module('index').controller('indexController', ['$scope', '$injector', keyboard.reset(); }; + /** + * Returns whether the current user has at least one active connection + * running within the current tab. + * + * @returns {!boolean} + * true if the current user has at least one active connection running + * in the current browser tab, false otherwise. + */ + var hasActiveTunnel = function hasActiveTunnel() { + + var clients = guacClientManager.getManagedClients(); + for (var id in clients) { + + switch (clients[id].clientState.connectionState) { + case ManagedClientState.ConnectionState.CONNECTING: + case ManagedClientState.ConnectionState.WAITING: + case ManagedClientState.ConnectionState.CONNECTED: + return true; + } + + } + + return false; + + }; + + // If we're logged in and not connected to anything, periodically check + // whether the current session is still valid. If the session has expired, + // refresh the auth state to reshow the login screen (rather than wait for + // the user to take some action and discover that they are not logged in + // after all). There is no need to do this if a connection is active as + // that connection activity will already automatically check session + // validity. + $interval(function cleanUpViewIfSessionInvalid() { + if ($scope.applicationState === ApplicationState.READY && !hasActiveTunnel()) { + authenticationService.getValidity().then(function validityDetermined(valid) { + if (!valid) + $scope.reAuthenticate(); + }); + } + }, SESSION_VALIDITY_RECHECK_INTERVAL); + // Release all keys upon form submission (there may not be corresponding // keyup events for key presses involved in submitting a form) $document.on('submit', function formSubmitted() {