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; 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 java.util.Date;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.base.RestrictedObject; 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.activeconnection.ActiveConnectionDirectory;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel; import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordSet; 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.User;
import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.UserGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* UserContext implementation which is driven by an arbitrary, underlying * 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 public class ModeledUserContext extends RestrictedObject
implements org.apache.guacamole.net.auth.UserContext { 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 * User directory restricted by the permissions of the user associated
* with this context. * with this context.
@@ -130,6 +138,12 @@ public class ModeledUserContext extends RestrictedObject
@Inject @Inject
private UserRecordMapper userRecordMapper; private UserRecordMapper userRecordMapper;
/**
* The environment of the Guacamole server.
*/
@Inject
private JDBCEnvironment environment;
/** /**
* The activity record associated with this user's Guacamole session. If * The activity record associated with this user's Guacamole session. If
* this user's session will not have an associated activity record, such as * 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); 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); 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); 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(); return userContext.getPrivileged();
} }
@Override
public boolean isValid() {
return userContext.isValid();
}
} }

View File

@@ -40,6 +40,27 @@ public interface UserContext {
*/ */
User self(); 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 * Returns an arbitrary REST resource representing this UserContext. The
* REST resource returned must be properly annotated with JSR-311 * REST resource returned must be properly annotated with JSR-311

View File

@@ -132,6 +132,22 @@ public class GuacamoleSession {
return Collections.unmodifiableList(userContexts); 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 * 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

View File

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