GUACAMOLE-1723: Merge automatic enforcement of access time restrictions for logged in users.

This commit is contained in:
Mike Jumper
2022-12-09 15:57:29 -08:00
committed by GitHub
12 changed files with 183 additions and 9 deletions

View File

@@ -227,4 +227,18 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment {
*/
public abstract boolean trackExternalConnectionHistory() throws GuacamoleException;
/**
* Returns a boolean value representing whether access time windows should
* be enforced for active connections - i.e. whether a currently-connected
* user should be disconnected upon the closure of an access window.
*
* @return
* true if a connected user should be disconnected upon an access time
* window closing, false otherwise.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public abstract boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException;
}

View File

@@ -29,6 +29,7 @@ import java.util.Collection;
import java.util.Date;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
import org.apache.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordSet;
@@ -50,6 +51,8 @@ import org.apache.guacamole.net.auth.SharingProfile;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* UserContext implementation which is driven by an arbitrary, underlying
@@ -58,6 +61,11 @@ import org.apache.guacamole.net.auth.UserGroup;
public class ModeledUserContext extends RestrictedObject
implements org.apache.guacamole.net.auth.UserContext {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ModeledUserContext.class);
/**
* User directory restricted by the permissions of the user associated
* with this context.
@@ -130,6 +138,12 @@ public class ModeledUserContext extends RestrictedObject
@Inject
private UserRecordMapper userRecordMapper;
/**
* The environment of the Guacamole server.
*/
@Inject
private JDBCEnvironment environment;
/**
* The activity record associated with this user's Guacamole session. If
* this user's session will not have an associated activity record, such as
@@ -296,4 +310,31 @@ public class ModeledUserContext extends RestrictedObject
}
@Override
public boolean isValid() {
try {
// If access window enforcement is disabled for active sessions,
// skip validity checks entirely
if (!environment.enforceAccessWindowsForActiveSessions())
return true;
}
catch (GuacamoleException e) {
logger.warn(
"Unable to determine if access window enforcement is"
+ " enabled for active sessions; enforcing by default: {}"
, e.getMessage());
logger.debug("Unable to determine access window enforcement policy.", e);
}
// A user context is valid if the associated user's account is valid
// for the current date, and the user is within an access time window
ModeledUser user = getCurrentUser().getUser();
return user.isAccountValid() && user.isAccountAccessible();
}
}

View File

@@ -412,4 +412,13 @@ public class MySQLEnvironment extends JDBCEnvironment {
true);
}
@Override
public boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException {
// Enforce access window restrictions for active sessions unless explicitly disabled
return getProperty(
MySQLGuacamoleProperties.MYSQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS,
true);
}
}

View File

@@ -278,4 +278,17 @@ public class MySQLGuacamoleProperties {
};
/**
* Whether or not user-specific access time windows should be enforced for active sessions,
* i.e. whether users with active sessions should be logged out immediately when an access
* window closes.
*/
public static final BooleanGuacamoleProperty MYSQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "mysql-enforce-access-windows-for-active-sessions"; }
};
}

View File

@@ -371,4 +371,13 @@ public class PostgreSQLEnvironment extends JDBCEnvironment {
true);
}
@Override
public boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException {
// Enforce access window restrictions for active sessions unless explicitly disabled
return getProperty(
PostgreSQLGuacamoleProperties.POSTGRESQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS,
true);
}
}

View File

@@ -290,4 +290,17 @@ public class PostgreSQLGuacamoleProperties {
};
/**
* Whether or not user-specific access time windows should be enforced for active sessions,
* i.e. whether users with active sessions should be logged out immediately when an access
* window closes.
*/
public static final BooleanGuacamoleProperty POSTGRESQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "postgresql-enforce-access-windows-for-active-sessions"; }
};
}

View File

@@ -268,4 +268,13 @@ public class SQLServerEnvironment extends JDBCEnvironment {
true);
}
@Override
public boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException {
// Enforce access window restrictions for active sessions unless explicitly disabled
return getProperty(
SQLServerGuacamoleProperties.SQLSERVER_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS,
true);
}
}

View File

@@ -220,4 +220,17 @@ public class SQLServerGuacamoleProperties {
};
/**
* Whether or not user-specific access time windows should be enforced for active sessions,
* i.e. whether users with active sessions should be logged out immediately when an access
* window closes.
*/
public static final BooleanGuacamoleProperty SQLSERVER_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "sqlserver-enforce-access-windows-for-active-sessions"; }
};
}

View File

@@ -162,4 +162,9 @@ public class DelegatingUserContext implements UserContext {
return userContext.getPrivileged();
}
@Override
public boolean isValid() {
return userContext.isValid();
}
}

View File

@@ -40,6 +40,27 @@ public interface UserContext {
*/
User self();
/**
* Returns true if the session for the User associated with this user
* context is valid, or false otherwise. If the session is not valid,
* the webapp can be expected to terminate the session within a short
* period of time.
*
* NOTE: The webapp currently checks once a minute, and terminates any
* session marked as invalid.
*
* @return
* true if the session for the User associated with this user
* context is valid, or false otherwise.
*/
default boolean isValid() {
// A user context is always valid unless explicitly updated by an
// implementation
return true;
}
/**
* Returns an arbitrary REST resource representing this UserContext. The
* REST resource returned must be properly annotated with JSR-311

View File

@@ -132,6 +132,22 @@ public class GuacamoleSession {
return Collections.unmodifiableList(userContexts);
}
/**
* 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.
*
* @return
* true if all user contexts associated with this session are
* valid, or false if any user context is not valid.
*/
public boolean isValid() {
// Immediately return false if any user context is not valid
return !userContexts.stream().anyMatch(
userContext -> !userContext.isValid());
}
/**
* Returns the UserContext associated with this session that originated
* from the AuthenticationProvider with the given identifier. If no such

View File

@@ -94,7 +94,8 @@ public class HashTokenSessionMap implements TokenSessionMap {
/**
* Task which iterates through all active sessions, evicting those sessions
* which are beyond the session timeout.
* which are beyond the session timeout, or are marked as invalid by an
* extension.
*/
private class SessionEvictionTask implements Runnable {
@@ -105,7 +106,8 @@ public class HashTokenSessionMap implements TokenSessionMap {
/**
* Creates a new task which automatically evicts sessions which are
* older than the specified timeout.
* older than the specified timeout, or are marked as invalid by an
* extension.
*
* @param sessionTimeout The maximum age of any session, in
* milliseconds.
@@ -116,16 +118,16 @@ public class HashTokenSessionMap implements TokenSessionMap {
/**
* Iterates through all active sessions, evicting those sessions which
* are beyond the session timeout. Internal errors which would
* otherwise stop the session eviction process are caught, logged, and
* the process is allowed to proceed.
* are beyond the session timeout, or are marked as invalid. Internal
* errors which would otherwise stop the session eviction process are
* caught, logged, and the process is allowed to proceed.
*/
private void evictExpiredSessions() {
private void evictExpiredOrInvalidSessions() {
// Get start time of session check time
long sessionCheckStart = System.currentTimeMillis();
logger.debug("Checking for expired sessions...");
logger.debug("Checking for expired or invalid sessions...");
// For each session, remove sesions which have expired
Iterator<Map.Entry<String, GuacamoleSession>> entries = sessionMap.entrySet().iterator();
@@ -136,6 +138,15 @@ public class HashTokenSessionMap implements TokenSessionMap {
try {
// Invalidate any sessions which have been flagged as invalid by extensions
if (!session.isValid()) {
logger.debug(
"Session \"{}\" has been invalidated by an extension.",
entry.getKey());
entries.remove();
session.invalidate();
}
// Do not expire sessions which are active
if (session.hasTunnels())
continue;
@@ -170,13 +181,13 @@ public class HashTokenSessionMap implements TokenSessionMap {
@Override
public void run() {
// The evictExpiredSessions() function should already
// The evictExpiredOrInvalidSessions() function should already
// automatically handle and log all unexpected internal errors,
// but wrap the entire call in a try/catch plus additional logging
// to ensure that absolutely no errors can result in the entire
// thread dying
try {
evictExpiredSessions();
evictExpiredOrInvalidSessions();
}
catch (Throwable t) {
logger.error("An unexpected internal error prevented the "