GUACAMOLE-1744: Merge automatically clean up UI if session has expired in the background.

This commit is contained in:
Virtually Nick
2023-02-27 17:56:18 -05:00
committed by GitHub
5 changed files with 157 additions and 25 deletions

View File

@@ -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.<!boolean>}
* 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 * Makes a request to revoke an authentication token using the token REST
* API endpoint, returning a promise that succeeds only if the token was * API endpoint, returning a promise that succeeds only if the token was

View File

@@ -23,14 +23,30 @@
angular.module('index').controller('indexController', ['$scope', '$injector', angular.module('index').controller('indexController', ['$scope', '$injector',
function 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 // Required services
const $document = $injector.get('$document'); const $document = $injector.get('$document');
const $location = $injector.get('$location'); const $interval = $injector.get('$interval');
const $route = $injector.get('$route'); const $location = $injector.get('$location');
const $window = $injector.get('$window'); const $route = $injector.get('$route');
const clipboardService = $injector.get('clipboardService'); const $window = $injector.get('$window');
const guacNotification = $injector.get('guacNotification'); const authenticationService = $injector.get('authenticationService');
const guacClientManager = $injector.get('guacClientManager'); 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 * 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(); 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 // Release all keys upon form submission (there may not be corresponding
// keyup events for key presses involved in submitting a form) // keyup events for key presses involved in submitting a form)
$document.on('submit', function formSubmitted() { $document.on('submit', function formSubmitted() {

View File

@@ -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 * @return
* The authenticated user associated with this session. * The authenticated user associated with this session.
*/ */
public AuthenticatedUser getAuthenticatedUser() { public AuthenticatedUser getAuthenticatedUser() {
this.access();
return authenticatedUser; return authenticatedUser;
} }
/** /**
* Replaces the authenticated user associated with this session with the * 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 * @param authenticatedUser
* The authenticated user to associated with this session. * The authenticated user to associated with this session.
*/ */
public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) { public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) {
this.access();
this.authenticatedUser = authenticatedUser; this.authenticatedUser = authenticatedUser;
} }
/** /**
* Returns a list of all UserContexts associated with this session. Each * Returns a list of all UserContexts associated with this session. Each
* AuthenticationProvider currently loaded by Guacamole may provide its own * 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 * @return
* An unmodifiable list of all UserContexts associated with this * An unmodifiable list of all UserContexts associated with this
* session. * session.
*/ */
public List<DecoratedUserContext> getUserContexts() { public List<DecoratedUserContext> getUserContexts() {
this.access();
return Collections.unmodifiableList(userContexts); return Collections.unmodifiableList(userContexts);
} }
@@ -136,6 +142,8 @@ public class GuacamoleSession {
* Returns true if all user contexts associated with this session are * 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, 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. * 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 * @return
* true if all user contexts associated with this session are * 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 * Returns the UserContext associated with this session that originated
* from the AuthenticationProvider with the given identifier. If no such * 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 * @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider that created the * 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 * 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 * @param userContexts
* The List of UserContexts to associate with this session. * The List of UserContexts to associate with this session.
*/ */
public void setUserContexts(List<DecoratedUserContext> userContexts) { public void setUserContexts(List<DecoratedUserContext> userContexts) {
this.access();
this.userContexts = userContexts; 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, * @return
* false otherwise. * true if this session has any associated active tunnels, false
* otherwise.
*/ */
public boolean hasTunnels() { public boolean hasTunnels() {
return !tunnels.isEmpty(); return !tunnels.isEmpty();
@@ -214,10 +227,14 @@ public class GuacamoleSession {
* session. A tunnel need not be present here to be used by the user * 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 * associated with this session, but tunnels not in this set will not
* be taken into account when determining whether a session is in use. * 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<String, UserTunnel> getTunnels() { public Map<String, UserTunnel> getTunnels() {
this.access();
return tunnels; return tunnels;
} }
@@ -228,16 +245,23 @@ public class GuacamoleSession {
* @param tunnel The tunnel to associate with this session. * @param tunnel The tunnel to associate with this session.
*/ */
public void addTunnel(UserTunnel tunnel) { public void addTunnel(UserTunnel tunnel) {
this.access();
tunnels.put(tunnel.getUUID().toString(), tunnel); tunnels.put(tunnel.getUUID().toString(), tunnel);
} }
/** /**
* Disassociates the tunnel having the given UUID from this session. * 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. * @param uuid
* @return true if the tunnel existed and was removed, false otherwise. * 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) { public boolean removeTunnel(String uuid) {
this.access();
return tunnels.remove(uuid) != null; return tunnels.remove(uuid) != null;
} }
@@ -251,7 +275,8 @@ public class GuacamoleSession {
/** /**
* Returns the time this session was last accessed, as the number of * Returns the time this session was last accessed, as the number of
* milliseconds since midnight January 1, 1970 GMT. Session access must * 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. * @return The time this session was last accessed.
*/ */

View File

@@ -208,12 +208,12 @@ public class HashTokenSessionMap implements TokenSessionMap {
if (authToken == null) if (authToken == null)
return null; return null;
// Update the last access time and return the GuacamoleSession // Return the GuacamoleSession having the given auth token (NOTE: We
GuacamoleSession session = sessionMap.get(authToken); // do not update the access time here, as it is necessary to be able
if (session != null) // to retrieve and check the session without causing that session to
session.access(); // be marked as active. Instead, those updates occur as needed when
// functions within the GuacamoleSession are invoked.)
return session; return sessionMap.get(authToken);
} }

View File

@@ -24,6 +24,7 @@ import com.google.inject.assistedinject.AssistedInject;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.HEAD;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; 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
}
} }