diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
index 69107ba42..1556f21da 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
@@ -90,7 +90,14 @@
guice-multibindings
3.0
-
+
+
+
+ com.google.guava
+ guava
+ 18.0
+
+
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ConfigurableGuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ConfigurableGuacamoleTunnelService.java
new file mode 100644
index 000000000..a3f5b6d0e
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ConfigurableGuacamoleTunnelService.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2015 Glyptodon LLC
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.glyptodon.guacamole.auth.jdbc.tunnel;
+
+import com.google.common.collect.ConcurrentHashMultiset;
+import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import org.glyptodon.guacamole.GuacamoleClientTooManyException;
+import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser;
+import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.GuacamoleResourceConflictException;
+import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
+
+
+/**
+ * GuacamoleTunnelService implementation which restricts concurrency for each
+ * connection and group according to a maximum number of connections and
+ * maximum number of connections per user.
+ *
+ * @author James Muehlner
+ * @author Michael Jumper
+ */
+@Singleton
+public class ConfigurableGuacamoleTunnelService
+ extends AbstractGuacamoleTunnelService {
+
+ /**
+ * Set of all currently-active user/connection pairs (seats).
+ */
+ private final ConcurrentHashMultiset activeSeats = ConcurrentHashMultiset.create();
+
+ /**
+ * Set of all currently-active connections.
+ */
+ private final ConcurrentHashMultiset activeConnections = ConcurrentHashMultiset.create();
+
+ /**
+ * Set of all currently-active user/connection group pairs (seats).
+ */
+ private final ConcurrentHashMultiset activeGroupSeats = ConcurrentHashMultiset.create();
+
+ /**
+ * Set of all currently-active connection groups.
+ */
+ private final ConcurrentHashMultiset activeGroups = ConcurrentHashMultiset.create();
+
+ /**
+ * The maximum number of connections allowed per connection by default, or
+ * zero if no default limit applies.
+ */
+ private final int connectionDefaultMaxConnections;
+
+ /**
+ * The maximum number of connections a user may have to any one connection
+ * by default, or zero if no default limit applies.
+ */
+ private final int connectionDefaultMaxConnectionsPerUser;
+
+ /**
+ * The maximum number of connections allowed per connection group by
+ * default, or zero if no default limit applies.
+ */
+ private final int connectionGroupDefaultMaxConnections;
+
+ /**
+ * The maximum number of connections a user may have to any one connection
+ * group by default, or zero if no default limit applies.
+ */
+ private final int connectionGroupDefaultMaxConnectionsPerUser;
+
+ /**
+ * Creates a new ConfigurableGuacamoleTunnelService which applies the given
+ * limitations when new connections are acquired.
+ *
+ * @param connectionDefaultMaxConnections
+ * The maximum number of connections allowed per connection by default,
+ * or zero if no default limit applies.
+ *
+ * @param connectionDefaultMaxConnectionsPerUser
+ * The maximum number of connections a user may have to any one
+ * connection by default, or zero if no default limit applies.
+ *
+ * @param connectionGroupDefaultMaxConnections
+ * The maximum number of connections allowed per connection group by
+ * default, or zero if no default limit applies.
+ *
+ * @param connectionGroupDefaultMaxConnectionsPerUser
+ * The maximum number of connections a user may have to any one
+ * connection group by default, or zero if no default limit applies.
+ */
+ public ConfigurableGuacamoleTunnelService(
+ int connectionDefaultMaxConnections,
+ int connectionDefaultMaxConnectionsPerUser,
+ int connectionGroupDefaultMaxConnections,
+ int connectionGroupDefaultMaxConnectionsPerUser) {
+
+ // Set default connection limits
+ this.connectionDefaultMaxConnections = connectionDefaultMaxConnections;
+ this.connectionDefaultMaxConnectionsPerUser = connectionDefaultMaxConnectionsPerUser;
+
+ // Set default connection group limits
+ this.connectionGroupDefaultMaxConnections = connectionGroupDefaultMaxConnections;
+ this.connectionGroupDefaultMaxConnectionsPerUser = connectionGroupDefaultMaxConnectionsPerUser;
+
+ }
+
+ /**
+ * Attempts to add a single instance of the given value to the given
+ * multiset without exceeding the specified maximum number of values. If
+ * the value cannot be added without exceeding the maximum, false is
+ * returned.
+ *
+ * @param
+ * The type of values contained within the multiset.
+ *
+ * @param multiset
+ * The multiset to attempt to add a value to.
+ *
+ * @param value
+ * The value to attempt to add.
+ *
+ * @param max
+ * The maximum number of each distinct value that the given multiset
+ * should hold, or zero if no limit applies.
+ *
+ * @return
+ * true if the value was successfully added without exceeding the
+ * specified maximum, false if the value could not be added.
+ */
+ private boolean tryAdd(ConcurrentHashMultiset multiset, T value, int max) {
+
+ // Repeatedly attempt to add a new value to the given multiset until we
+ // explicitly succeed or explicitly fail
+ while (true) {
+
+ // Get current number of values
+ int count = multiset.count(value);
+
+ // Bail out if the maximum has already been reached
+ if (count >= max || max == 0)
+ return false;
+
+ // Attempt to add one more value
+ if (multiset.setCount(value, count, count+1))
+ return true;
+
+ // Try again if unsuccessful
+
+ }
+
+ }
+
+ @Override
+ protected ModeledConnection acquire(AuthenticatedUser user,
+ List connections) throws GuacamoleException {
+
+ // Get username
+ String username = user.getUser().getIdentifier();
+
+ // Sort connections in ascending order of usage
+ ModeledConnection[] sortedConnections = connections.toArray(new ModeledConnection[connections.size()]);
+ Arrays.sort(sortedConnections, new Comparator() {
+
+ @Override
+ public int compare(ModeledConnection a, ModeledConnection b) {
+
+ return getActiveConnections(a).size()
+ - getActiveConnections(b).size();
+
+ }
+
+ });
+
+ // Track whether acquire fails due to user-specific limits
+ boolean userSpecificFailure = true;
+
+ // Return the first unreserved connection
+ for (ModeledConnection connection : sortedConnections) {
+
+ // Attempt to aquire connection according to per-user limits
+ Seat seat = new Seat(username, connection.getIdentifier());
+ if (tryAdd(activeSeats, seat,
+ connectionDefaultMaxConnectionsPerUser)) {
+
+ // Attempt to aquire connection according to overall limits
+ if (tryAdd(activeConnections, connection.getIdentifier(),
+ connectionDefaultMaxConnections))
+ return connection;
+
+ // Acquire failed - retry with next connection
+ activeSeats.remove(seat);
+
+ // Failure to acquire is not user-specific
+ userSpecificFailure = false;
+
+ }
+
+ }
+
+ // Too many connections by this user
+ if (userSpecificFailure)
+ throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user.");
+
+ // Too many connections, but not necessarily due purely to this user
+ else
+ throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use.");
+
+ }
+
+ @Override
+ protected void release(AuthenticatedUser user, ModeledConnection connection) {
+ activeSeats.remove(new Seat(user.getUser().getIdentifier(), connection.getIdentifier()));
+ activeConnections.remove(connection.getIdentifier());
+ }
+
+ @Override
+ protected void acquire(AuthenticatedUser user,
+ ModeledConnectionGroup connectionGroup) throws GuacamoleException {
+
+ // Get username
+ String username = user.getUser().getIdentifier();
+
+ // Attempt to aquire connection group according to per-user limits
+ Seat seat = new Seat(username, connectionGroup.getIdentifier());
+ if (tryAdd(activeGroupSeats, seat,
+ connectionGroupDefaultMaxConnectionsPerUser)) {
+
+ // Attempt to aquire connection group according to overall limits
+ if (tryAdd(activeGroups, connectionGroup.getIdentifier(),
+ connectionGroupDefaultMaxConnections))
+ return;
+
+ // Acquire failed
+ activeGroupSeats.remove(seat);
+
+ // Failure to acquire is not user-specific
+ throw new GuacamoleResourceConflictException("Cannot connect. This connection group is in use.");
+
+ }
+
+ // Already in use by this user
+ throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user.");
+
+ }
+
+ @Override
+ protected void release(AuthenticatedUser user,
+ ModeledConnectionGroup connectionGroup) {
+ activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()));
+ activeGroups.remove(connectionGroup.getIdentifier());
+ }
+
+}