diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java index d5af87794..386df9bb9 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java @@ -88,20 +88,22 @@ public class ActiveConnectionPermissionService if (canReadPermissions(user, targetUser)) { // Only administrators may access active connections - if (!targetUser.isAdministrator()) - return Collections.EMPTY_SET; + boolean isAdmin = targetUser.isAdministrator(); // Get all active connections Collection records = tunnelService.getActiveConnections(user); - // We have READ and DELETE on all active connections + // We have READ, and possibly DELETE, on all active connections Set permissions = new HashSet(); for (ActiveConnectionRecord record : records) { - // Add implicit READ and DELETE + // Add implicit READ String identifier = record.getUUID().toString(); - permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier)); - permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier)); + permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier)); + + // If we're and admin, then we also have DELETE + if (isAdmin) + permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier)); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java index 41eceef60..c423885fd 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java @@ -26,6 +26,7 @@ import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; @@ -62,35 +63,43 @@ public class ActiveConnectionService public TrackedActiveConnection retrieveObject(AuthenticatedUser user, String identifier) throws GuacamoleException { - // Only administrators may retrieve active connections - if (!user.getUser().isAdministrator()) - throw new GuacamoleSecurityException("Permission denied."); + // Pull objects having given identifier + Collection objects = retrieveObjects(user, Collections.singleton(identifier)); - // Retrieve record associated with requested connection - ActiveConnectionRecord record = tunnelService.getActiveConnection(user, identifier); - if (record == null) + // If no such object, return null + if (objects.isEmpty()) return null; - // Return tracked active connection using retrieved record - TrackedActiveConnection activeConnection = trackedActiveConnectionProvider.get(); - activeConnection.init(user, record); - return activeConnection; - + // The object collection will have exactly one element unless the + // database has seriously lost integrity + assert(objects.size() == 1); + + // Return first and only object + return objects.iterator().next(); + } @Override public Collection retrieveObjects(AuthenticatedUser user, Collection identifiers) throws GuacamoleException { - // Build list of all active connections with given identifiers - Collection activeConnections = new ArrayList(identifiers.size()); - for (String identifier : identifiers) { + boolean isAdmin = user.getUser().isAdministrator(); + Set identifierSet = new HashSet(identifiers); - // Add connection to list if it exists - TrackedActiveConnection activeConnection = retrieveObject(user, identifier); - if (activeConnection != null) + // Retrieve all visible connections (permissions enforced by tunnel service) + Collection records = tunnelService.getActiveConnections(user); + + // Restrict to subset of records which match given identifiers + Collection activeConnections = new ArrayList(identifiers.size()); + for (ActiveConnectionRecord record : records) { + + // Add connection if within requested identifiers + if (identifierSet.contains(record.getUUID().toString())) { + TrackedActiveConnection activeConnection = trackedActiveConnectionProvider.get(); + activeConnection.init(user, record, isAdmin); activeConnections.add(activeConnection); - + } + } return activeConnections; @@ -101,6 +110,10 @@ public class ActiveConnectionService public void deleteObject(AuthenticatedUser user, String identifier) throws GuacamoleException { + // Only administrators may delete active connections + if (!user.getUser().isAdministrator()) + throw new GuacamoleSecurityException("Permission denied."); + // Close connection, if it exists (and we have permission) ActiveConnection activeConnection = retrieveObject(user, identifier); if (activeConnection != null) { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java index 8dc00fe27..f9e6ed30b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java @@ -69,26 +69,40 @@ public class TrackedActiveConnection extends RestrictedObject implements ActiveC /** * Initializes this TrackedActiveConnection, copying the data associated - * with the given active connection record. + * with the given active connection record. At a minimum, the identifier + * of this active connection will be set, the start date, and the + * identifier of the associated connection will be copied. If requested, + * sensitive information like the associated username will be copied, as + * well. * * @param currentUser * The user that created or retrieved this object. * * @param activeConnectionRecord * The active connection record to copy. + * + * @param includeSensitiveInformation + * Whether sensitive data should be copied from the connection record + * as well. This includes the remote host, associated tunnel, and + * username. */ public void init(AuthenticatedUser currentUser, - ActiveConnectionRecord activeConnectionRecord) { + ActiveConnectionRecord activeConnectionRecord, + boolean includeSensitiveInformation) { super.init(currentUser); - // Copy all data from given record + // Copy all non-sensitive data from given record this.connectionIdentifier = activeConnectionRecord.getConnection().getIdentifier(); this.identifier = activeConnectionRecord.getUUID().toString(); - this.remoteHost = activeConnectionRecord.getRemoteHost(); this.startDate = activeConnectionRecord.getStartDate(); - this.tunnel = activeConnectionRecord.getTunnel(); - this.username = activeConnectionRecord.getUsername(); + + // Include sensitive data, too, if requested + if (includeSensitiveInformation) { + this.remoteHost = activeConnectionRecord.getRemoteHost(); + this.tunnel = activeConnectionRecord.getTunnel(); + this.username = activeConnectionRecord.getUsername(); + } } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java index 10adde197..bfa6231e3 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java @@ -28,8 +28,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; @@ -448,24 +450,33 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS public Collection getActiveConnections(AuthenticatedUser user) throws GuacamoleException { - // Only administrators may see all active connections - if (!user.getUser().isAdministrator()) + // Simply return empty list if there are no active tunnels + Collection records = activeTunnels.values(); + if (records.isEmpty()) return Collections.EMPTY_LIST; - return Collections.unmodifiableCollection(activeTunnels.values()); + // Build set of all connection identifiers associated with active tunnels + Set identifiers = new HashSet(records.size()); + for (ActiveConnectionRecord record : records) + identifiers.add(record.getConnection().getIdentifier()); - } + // Produce collection of readable connection identifiers + Collection connections = connectionMapper.selectReadable(user.getUser().getModel(), identifiers); - @Override - public ActiveConnectionRecord getActiveConnection(AuthenticatedUser user, - String tunnelUUID) throws GuacamoleException { + // Ensure set contains only identifiers of readable connections + identifiers.clear(); + for (ConnectionModel connection : connections) + identifiers.add(connection.getIdentifier()); - // Only administrators may see all active connections - if (!user.getUser().isAdministrator()) - return null; + // Produce readable subset of records + Collection visibleRecords = new ArrayList(records.size()); + for (ActiveConnectionRecord record : records) { + if (identifiers.contains(record.getConnection().getIdentifier())) + visibleRecords.add(record); + } + + return visibleRecords; - return activeTunnels.get(tunnelUUID); - } @Override diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.java index 0bb78419d..c965a823b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.java @@ -59,30 +59,6 @@ public interface GuacamoleTunnelService { public Collection getActiveConnections(AuthenticatedUser user) throws GuacamoleException; - /** - * Returns the connection records representing the connection associated - * with the tunnel having the given UUID, if that connection is visible to - * the given user. - * - * @param user - * The user retrieving the active connection. - * - * @param tunnelUUID - * The UUID of the tunnel associated with the active connection being - * retrieved. - * - * @return - * The active connection associated with the tunnel having the given - * UUID, or null if no such connection exists. - * - * @throws GuacamoleException - * If an error occurs while retrieving all active connections, or if - * permission is denied. - */ - public ActiveConnectionRecord getActiveConnection(AuthenticatedUser user, - String tunnelUUID) - throws GuacamoleException; - /** * Creates a socket for the given user which connects to the given * connection. The given client information will be passed to guacd when diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ActiveConnection.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ActiveConnection.java index 8258e59db..1d20ded1d 100644 --- a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ActiveConnection.java +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ActiveConnection.java @@ -34,7 +34,9 @@ import org.glyptodon.guacamole.net.GuacamoleTunnel; public interface ActiveConnection extends Identifiable { /** - * Returns the identifier of the connection being actively used. + * Returns the identifier of the connection being actively used. Unlike the + * other information stored in this object, the connection identifier must + * be present and MAY NOT be null. * * @return * The identifier of the connection being actively used. @@ -53,7 +55,8 @@ public interface ActiveConnection extends Identifiable { * Returns the date and time the connection began. * * @return - * The date and time the connection began. + * The date and time the connection began, or null if this + * information is not available. */ Date getStartDate(); @@ -61,7 +64,8 @@ public interface ActiveConnection extends Identifiable { * Sets the date and time the connection began. * * @param startDate - * The date and time the connection began. + * The date and time the connection began, or null if this + * information is not available. */ void setStartDate(Date startDate); @@ -90,7 +94,8 @@ public interface ActiveConnection extends Identifiable { * Returns the name of the user who is using this connection. * * @return - * The name of the user who is using this connection. + * The name of the user who is using this connection, or null if this + * information is not available. */ String getUsername(); @@ -98,7 +103,8 @@ public interface ActiveConnection extends Identifiable { * Sets the name of the user who is using this connection. * * @param username - * The name of the user who is using this connection. + * The name of the user who is using this connection, or null if this + * information is not available. */ void setUsername(String username); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java index 7553db7ad..1a47582bc 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java @@ -106,14 +106,14 @@ public class ActiveConnectionRESTService { if (permissions != null && permissions.isEmpty()) permissions = null; - // An admin user has access to any user + // An admin user has access to any connection SystemPermissionSet systemPermissions = self.getSystemPermissions(); boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); // Get the directory Directory activeConnectionDirectory = userContext.getActiveConnectionDirectory(); - // Filter users, if requested + // Filter connections, if requested Collection activeConnectionIdentifiers = activeConnectionDirectory.getIdentifiers(); if (!isAdmin && permissions != null) { ObjectPermissionSet activeConnectionPermissions = self.getActiveConnectionPermissions(); diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js index 1ba9e8dc1..d5ba313d4 100644 --- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js +++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js @@ -88,11 +88,58 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() }, templateUrl: 'app/groupList/templates/guacGroupList.html', - controller: ['$scope', '$injector', '$interval', function guacGroupListController($scope, $injector, $interval) { + controller: ['$scope', '$injector', function guacGroupListController($scope, $injector) { - // Get required types + // Required services + var activeConnectionService = $injector.get('activeConnectionService'); + + // Required types var GroupListItem = $injector.get('GroupListItem'); + /** + * The number of active connections associated with a given + * connection identifier. If this information is unknown, or there + * are no active connections for a given identifier, no number will + * be stored. + * + * @type Object. + */ + var connectionCount = {}; + + // Count active connections by connection identifier + activeConnectionService.getActiveConnections() + .success(function activeConnectionsRetrieved(activeConnections) { + + // Count each active connection by identifier + angular.forEach(activeConnections, function addActiveConnection(activeConnection) { + + // If counter already exists, increment + var identifier = activeConnection.connectionIdentifier; + if (connectionCount[identifier]) + connectionCount[identifier]++; + + // Otherwise, initialize counter to 1 + else + connectionCount[identifier] = 1; + + }); + + }); + + /** + * Returns the number of active usages of a given connection. + * + * @param {Connection} connection + * The connection whose active connections should be counted. + * + * @returns {Number} + * The number of currently-active usages of the given + * connection. + */ + var countActiveConnections = function countActiveConnections(connection) { + return connectionCount[connection.identifier]; + }; + /** * Returns whether the given item represents a connection that can * be displayed. If there is no connection template, then no @@ -131,7 +178,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() if (connectionGroup) { // Create item hierarchy, including connections only if they will be visible - var rootItem = GroupListItem.fromConnectionGroup(connectionGroup, !!$scope.connectionTemplate); + var rootItem = GroupListItem.fromConnectionGroup(connectionGroup, + !!$scope.connectionTemplate, countActiveConnections); // If root group is to be shown, wrap that group as the child of a fake root group if ($scope.showRootGroup) @@ -161,7 +209,7 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() $scope.toggleExpanded = function toggleExpanded(groupListItem) { groupListItem.isExpanded = !groupListItem.isExpanded; }; - + }] }; diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js index 4e70dc245..aa9a0f0de 100644 --- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js @@ -102,12 +102,14 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio this.isExpanded = template.isExpanded; /** - * The number of currently active users for this connection or + * Returns the number of currently active users for this connection or * connection group, if known. * * @type Number */ - this.activeConnections = template.activeConnections; + this.getActiveConnections = template.getActiveConnections || (function getActiveConnections() { + return null; + }); /** * The connection or connection group whose data is exposed within @@ -126,10 +128,15 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * The connection whose contents should be represented by the new * GroupListItem. * + * @param {Function} [countActiveConnections] + * A getter which returns the current number of active connections for + * the given connection. If omitted, the number of active connections + * known at the time this function was called is used instead. + * * @returns {GroupListItem} * A new GroupListItem which represents the given connection. */ - GroupListItem.fromConnection = function fromConnection(connection) { + GroupListItem.fromConnection = function fromConnection(connection, countActiveConnections) { // Return item representing the given connection return new GroupListItem({ @@ -144,7 +151,15 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio isConnectionGroup : false, // Count of currently active connections using this connection - activeConnections : connection.activeConnections, + getActiveConnections : function getActiveConnections() { + + // Use getter, if provided + if (countActiveConnections) + return countActiveConnections(connection); + + return connection.activeConnections; + + }, // Wrapped item wrappedItem : connection @@ -165,26 +180,37 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * Whether connections should be included in the contents of the * resulting GroupListItem. By default, connections are included. * + * @param {Function} [countActiveConnections] + * A getter which returns the current number of active connections for + * the given connection. If omitted, the number of active connections + * known at the time this function was called is used instead. + * + * @param {Function} [countActiveConnectionGroups] + * A getter which returns the current number of active connections for + * the given connection group. If omitted, the number of active + * connections known at the time this function was called is used + * instead. + * * @returns {GroupListItem} * A new GroupListItem which represents the given connection group, * including all descendants. */ GroupListItem.fromConnectionGroup = function fromConnectionGroup(connectionGroup, - includeConnections) { + includeConnections, countActiveConnections, countActiveConnectionGroups) { var children = []; // Add any child connections if (connectionGroup.childConnections && includeConnections !== false) { connectionGroup.childConnections.forEach(function addChildConnection(child) { - children.push(GroupListItem.fromConnection(child)); + children.push(GroupListItem.fromConnection(child, countActiveConnections)); }); } // Add any child groups if (connectionGroup.childConnectionGroups) { connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { - children.push(GroupListItem.fromConnectionGroup(child, includeConnections)); + children.push(GroupListItem.fromConnectionGroup(child, includeConnections, countActiveConnections, countActiveConnectionGroups)); }); } @@ -204,7 +230,16 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio children : children, // Count of currently active connection groups using this connection - activeConnections : connectionGroup.activeConnections, + getActiveConnections : function getActiveConnections() { + + // Use getter, if provided + if (countActiveConnectionGroups) + return countActiveConnectionGroups(connectionGroup); + + return connectionGroup.activeConnections; + + }, + // Wrapped item wrappedItem : connectionGroup diff --git a/guacamole/src/main/webapp/app/home/templates/connection.html b/guacamole/src/main/webapp/app/home/templates/connection.html index 4e91fd62d..9187c4f25 100644 --- a/guacamole/src/main/webapp/app/home/templates/connection.html +++ b/guacamole/src/main/webapp/app/home/templates/connection.html @@ -21,7 +21,7 @@ THE SOFTWARE. --> -
+
@@ -32,8 +32,8 @@ {{item.name}} - - {{'HOME.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.activeConnections}'}} + + {{'HOME.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.getActiveConnections()}'}}
diff --git a/guacamole/src/main/webapp/app/manage/templates/connection.html b/guacamole/src/main/webapp/app/manage/templates/connection.html index cd0950b92..b96e3dfa7 100644 --- a/guacamole/src/main/webapp/app/manage/templates/connection.html +++ b/guacamole/src/main/webapp/app/manage/templates/connection.html @@ -21,7 +21,7 @@ THE SOFTWARE. --> -
+
@@ -32,8 +32,8 @@ {{item.name}} - - {{'MANAGE_CONNECTION.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.activeConnections}'}} + + {{'MANAGE_CONNECTION.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.getActiveConnections()}'}}
diff --git a/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js b/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js index b51e26245..d44b27d31 100644 --- a/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js +++ b/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js @@ -59,7 +59,7 @@ angular.module('rest').factory('ActiveConnection', [function defineActiveConnect /** * The time that the connection began, in seconds since - * 1970-01-01 00:00:00 UTC. + * 1970-01-01 00:00:00 UTC, if known. * * @type Number */ @@ -73,7 +73,7 @@ angular.module('rest').factory('ActiveConnection', [function defineActiveConnect this.remoteHost = template.remoteHost; /** - * The username of the user associated with the connection. + * The username of the user associated with the connection, if known. * * @type String */