From b7ac434d41b0db29f5453f2d113f4668380272f9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Jun 2016 19:35:07 -0700 Subject: [PATCH 1/2] GUACAMOLE-53: Add session affinity attribute for connection groups. --- .../connectiongroup/ConnectionGroupModel.java | 30 +++++++++++++++++ .../ModeledConnectionGroup.java | 23 +++++++++++-- .../src/main/resources/translations/en.json | 1 + .../schema/001-create-schema.sql | 2 ++ .../schema/upgrade/upgrade-pre-0.9.10.sql | 7 ++++ .../connectiongroup/ConnectionGroupMapper.xml | 33 +++++++++++-------- .../schema/001-create-schema.sql | 1 + .../schema/upgrade/upgrade-pre-0.9.10.sql | 7 ++++ .../connectiongroup/ConnectionGroupMapper.xml | 33 +++++++++++-------- 9 files changed, 108 insertions(+), 29 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java index 1aedfe07a..6805ef2a5 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.java @@ -54,6 +54,12 @@ public class ConnectionGroupModel extends GroupedObjectModel { */ private Integer maxConnectionsPerUser; + /** + * Whether individual users should be consistently assigned the same + * connection within a balancing group until they log out. + */ + private boolean sessionAffinityEnabled; + /** * Creates a new, empty connection group. */ @@ -156,6 +162,30 @@ public class ConnectionGroupModel extends GroupedObjectModel { this.maxConnectionsPerUser = maxConnectionsPerUser; } + /** + * Returns whether individual users should be consistently assigned the same + * connection within a balancing group until they log out. + * + * @return + * Whether individual users should be consistently assigned the same + * connection within a balancing group until they log out. + */ + public boolean isSessionAffinityEnabled() { + return sessionAffinityEnabled; + } + + /** + * Sets whether individual users should be consistently assigned the same + * connection within a balancing group until they log out. + * + * @param sessionAffinityEnabled + * Whether individual users should be consistently assigned the same + * connection within a balancing group until they log out. + */ + public void setSessionAffinityEnabled(boolean sessionAffinityEnabled) { + this.sessionAffinityEnabled = sessionAffinityEnabled; + } + @Override public String getIdentifier() { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java index 5ee058007..85f6cec6d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java @@ -26,11 +26,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; -import org.apache.guacamole.auth.jdbc.connection.ConnectionService; -import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.base.ModeledGroupedDirectoryObject; +import org.apache.guacamole.auth.jdbc.connection.ConnectionService; +import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; +import org.apache.guacamole.form.BooleanField; import org.apache.guacamole.form.Field; import org.apache.guacamole.form.Form; import org.apache.guacamole.form.NumericField; @@ -67,13 +68,21 @@ public class ModeledConnectionGroup extends ModeledGroupedDirectoryObjectasList( new NumericField(MAX_CONNECTIONS_NAME), - new NumericField(MAX_CONNECTIONS_PER_USER_NAME) + new NumericField(MAX_CONNECTIONS_PER_USER_NAME), + new BooleanField(ENABLE_SESSION_AFFINITY, "true") )); /** @@ -168,6 +177,10 @@ public class ModeledConnectionGroup extends ModeledGroupedDirectoryObject - - - - + + + - - + + + @@ -79,7 +80,8 @@ parent_id, type, max_connections, - max_connections_per_user + max_connections_per_user, + enable_session_affinity FROM guacamole_connection_group WHERE connection_group_id IN parent_id = #{parentIdentifier,jdbcType=VARCHAR} @@ -144,14 +148,16 @@ parent_id, type, max_connections, - max_connections_per_user + max_connections_per_user, + enable_session_affinity ) VALUES ( #{object.name,jdbcType=VARCHAR}, #{object.parentIdentifier,jdbcType=VARCHAR}, #{object.type,jdbcType=VARCHAR}, #{object.maxConnections,jdbcType=INTEGER}, - #{object.maxConnectionsPerUser,jdbcType=INTEGER} + #{object.maxConnectionsPerUser,jdbcType=INTEGER}, + #{object.sessionAffinityEnabled,jdbcType=BOOLEAN} ) @@ -163,8 +169,9 @@ parent_id = #{object.parentIdentifier,jdbcType=VARCHAR}, type = #{object.type,jdbcType=VARCHAR}, max_connections = #{object.maxConnections,jdbcType=INTEGER}, - max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER} + max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER}, + enable_session_affinity = #{object.sessionAffinityEnabled,jdbcType=BOOLEAN} WHERE connection_group_id = #{object.objectID,jdbcType=INTEGER} - \ No newline at end of file + diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql index a5543b25e..3a84ba54f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql @@ -63,6 +63,7 @@ CREATE TABLE guacamole_connection_group ( -- Concurrency limits max_connections integer, max_connections_per_user integer, + enable_session_affinity boolean NOT NULL DEFAULT FALSE, PRIMARY KEY (connection_group_id), diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.10.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.10.sql index c1f0e8bc3..baaa4ff93 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.10.sql +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.10.sql @@ -88,3 +88,10 @@ ALTER TABLE guacamole_connection_history FOREIGN KEY (connection_id) REFERENCES guacamole_connection (connection_id) ON DELETE SET NULL; +-- +-- Add session affinity column +-- + +ALTER TABLE guacamole_connection_group + ADD COLUMN enable_session_affinity boolean NOT NULL DEFAULT FALSE; + diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml index 24c8a841d..b55c01592 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml @@ -25,13 +25,14 @@ - - - - + + + - - + + + @@ -79,7 +80,8 @@ parent_id, type, max_connections, - max_connections_per_user + max_connections_per_user, + enable_session_affinity FROM guacamole_connection_group WHERE connection_group_id IN parent_id = #{parentIdentifier,jdbcType=INTEGER}::integer @@ -144,14 +148,16 @@ parent_id, type, max_connections, - max_connections_per_user + max_connections_per_user, + enable_session_affinity ) VALUES ( #{object.name,jdbcType=VARCHAR}, #{object.parentIdentifier,jdbcType=INTEGER}::integer, #{object.type,jdbcType=VARCHAR}::guacamole_connection_group_type, #{object.maxConnections,jdbcType=INTEGER}, - #{object.maxConnectionsPerUser,jdbcType=INTEGER} + #{object.maxConnectionsPerUser,jdbcType=INTEGER}, + #{object.sessionAffinityEnabled,jdbcType=BOOLEAN} ) @@ -163,8 +169,9 @@ parent_id = #{object.parentIdentifier,jdbcType=INTEGER}::integer, type = #{object.type,jdbcType=VARCHAR}::guacamole_connection_group_type, max_connections = #{object.maxConnections,jdbcType=INTEGER}, - max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER} + max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER}, + enable_session_affinity = #{object.sessionAffinityEnabled,jdbcType=BOOLEAN} WHERE connection_group_id = #{object.objectID,jdbcType=INTEGER}::integer - \ No newline at end of file + From 10344aeba4ce23939e03500dc55fc662eaeb3cc5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Jun 2016 20:51:34 -0700 Subject: [PATCH 2/2] GUACAMOLE-53: Implement session affinity within the GuacamoleTunnelService. --- .../ModeledConnectionGroup.java | 11 +++++ .../AbstractGuacamoleTunnelService.java | 45 +++++++++++++++++++ .../auth/jdbc/user/AuthenticatedUser.java | 44 ++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java index 85f6cec6d..a0675f48d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java @@ -257,5 +257,16 @@ public class ModeledConnectionGroup extends ModeledGroupedDirectoryObject getPreferredConnections(AuthenticatedUser user, + Collection identifiers) { + + // Search provided identifiers for any preferred connections + for (String identifier : identifiers) { + + // If at least one prefferred connection is found, assume it is the + // only preferred connection + if (user.isPreferredConnection(identifier)) + return Collections.singletonList(identifier); + + } + + // No preferred connections were found + return identifiers; + + } + /** * Returns a list of all balanced connections within a given connection * group. If the connection group is not balancing, or it contains no @@ -440,6 +477,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS if (identifiers.isEmpty()) return Collections.emptyList(); + // Restrict to preferred connections if session affinity is enabled + if (connectionGroup.isSessionAffinityEnabled()) + identifiers = getPreferredConnections(user, identifiers); + // Retrieve all children Collection models = connectionMapper.select(identifiers); List connections = new ArrayList(models.size()); @@ -535,6 +576,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS throw e; } + // If session affinity is enabled, prefer this connection going forward + if (connectionGroup.isSessionAffinityEnabled()) + user.preferConnection(connection.getIdentifier()); + // Connect to acquired child return assignGuacamoleTunnel(new ActiveConnectionRecord(user, connectionGroup, connection), info); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/AuthenticatedUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/AuthenticatedUser.java index 3322a2080..0696c8801 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/AuthenticatedUser.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/AuthenticatedUser.java @@ -19,6 +19,9 @@ package org.apache.guacamole.auth.jdbc.user; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; @@ -73,6 +76,17 @@ public class AuthenticatedUser implements org.apache.guacamole.net.auth.Authenti */ private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + "(, " + IP_ADDRESS_REGEX + ")*$"); + /** + * The connections which have been committed for use by this user in the + * context of a balancing connection group. Balancing connection groups + * will preferentially choose connections within this set, unless those + * connections are not children of the group in question. If a group DOES + * have at least one child connection within this set, no connections that + * are not in this set will be used. + */ + private final Set preferredConnections = + Collections.newSetFromMap(new ConcurrentHashMap()); + /** * Derives the remote host of the authenticating user from the given * credentials object. The remote host is derived from X-Forwarded-For @@ -157,6 +171,36 @@ public class AuthenticatedUser implements org.apache.guacamole.net.auth.Authenti return remoteHost; } + /** + * Returns whether the connection having the given identifier has been + * marked as preferred for this user's current Guacamole session. A + * preferred connection is always chosen in favor of other connections when + * it is a child of a balancing connection group. + * + * @param identifier + * The identifier of the connection to test. + * + * @return + * true if the connection having the given identifier has been marked + * as preferred, false otherwise. + */ + public boolean isPreferredConnection(String identifier) { + return preferredConnections.contains(identifier); + } + + /** + * Marks the connection having the given identifier as preferred for this + * user's current Guacamole session. A preferred connection is always chosen + * in favor of other connections when it is a child of a balancing + * connection group. + * + * @param identifier + * The identifier of the connection to prefer. + */ + public void preferConnection(String identifier) { + preferredConnections.add(identifier); + } + @Override public AuthenticationProvider getAuthenticationProvider() { return authenticationProvider;