diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java index 763793d62..77f038734 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java @@ -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; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java index 1416b0b39..e5ad5d2ea 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java @@ -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(); + + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java index d8344ec81..9ac722624 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java @@ -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); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java index 6a5944fd3..ca17757a4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java @@ -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"; } + + }; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java index ac08e0ae0..5e0a5a1fe 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java @@ -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); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java index dfa00b68c..3f33ec44f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java @@ -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"; } + + }; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java index 0b6983691..7d88d7399 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java @@ -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); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java index 432454ed8..1f2500350 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java @@ -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"; } + + }; + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java index 4b0343181..7431fa899 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java @@ -162,4 +162,9 @@ public class DelegatingUserContext implements UserContext { return userContext.getPrivileged(); } + @Override + public boolean isValid() { + return userContext.isValid(); + } + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java index 3f75b899c..a9ea7ea0c 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java @@ -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 diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java index 3e8d488c3..0458639d1 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java @@ -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 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 24f653098..15d11eb62 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 @@ -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> 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 "