Merge pull request #143 from glyptodon/readable-active-connections

GUAC-1126: Calculate active connections using the active connection service
This commit is contained in:
James Muehlner
2015-04-09 15:57:06 -07:00
12 changed files with 198 additions and 93 deletions

View File

@@ -88,20 +88,22 @@ public class ActiveConnectionPermissionService
if (canReadPermissions(user, targetUser)) { if (canReadPermissions(user, targetUser)) {
// Only administrators may access active connections // Only administrators may access active connections
if (!targetUser.isAdministrator()) boolean isAdmin = targetUser.isAdministrator();
return Collections.EMPTY_SET;
// Get all active connections // Get all active connections
Collection<ActiveConnectionRecord> records = tunnelService.getActiveConnections(user); Collection<ActiveConnectionRecord> records = tunnelService.getActiveConnections(user);
// We have READ and DELETE on all active connections // We have READ, and possibly DELETE, on all active connections
Set<ObjectPermission> permissions = new HashSet<ObjectPermission>(); Set<ObjectPermission> permissions = new HashSet<ObjectPermission>();
for (ActiveConnectionRecord record : records) { for (ActiveConnectionRecord record : records) {
// Add implicit READ and DELETE // Add implicit READ
String identifier = record.getUUID().toString(); String identifier = record.getUUID().toString();
permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier)); permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier));
permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier));
// If we're and admin, then we also have DELETE
if (isAdmin)
permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier));
} }

View File

@@ -26,6 +26,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser;
@@ -62,35 +63,43 @@ public class ActiveConnectionService
public TrackedActiveConnection retrieveObject(AuthenticatedUser user, public TrackedActiveConnection retrieveObject(AuthenticatedUser user,
String identifier) throws GuacamoleException { String identifier) throws GuacamoleException {
// Only administrators may retrieve active connections // Pull objects having given identifier
if (!user.getUser().isAdministrator()) Collection<TrackedActiveConnection> objects = retrieveObjects(user, Collections.singleton(identifier));
throw new GuacamoleSecurityException("Permission denied.");
// Retrieve record associated with requested connection // If no such object, return null
ActiveConnectionRecord record = tunnelService.getActiveConnection(user, identifier); if (objects.isEmpty())
if (record == null)
return null; return null;
// Return tracked active connection using retrieved record // The object collection will have exactly one element unless the
TrackedActiveConnection activeConnection = trackedActiveConnectionProvider.get(); // database has seriously lost integrity
activeConnection.init(user, record); assert(objects.size() == 1);
return activeConnection;
// Return first and only object
return objects.iterator().next();
} }
@Override @Override
public Collection<TrackedActiveConnection> retrieveObjects(AuthenticatedUser user, public Collection<TrackedActiveConnection> retrieveObjects(AuthenticatedUser user,
Collection<String> identifiers) throws GuacamoleException { Collection<String> identifiers) throws GuacamoleException {
// Build list of all active connections with given identifiers boolean isAdmin = user.getUser().isAdministrator();
Collection<TrackedActiveConnection> activeConnections = new ArrayList<TrackedActiveConnection>(identifiers.size()); Set<String> identifierSet = new HashSet<String>(identifiers);
for (String identifier : identifiers) {
// Add connection to list if it exists // Retrieve all visible connections (permissions enforced by tunnel service)
TrackedActiveConnection activeConnection = retrieveObject(user, identifier); Collection<ActiveConnectionRecord> records = tunnelService.getActiveConnections(user);
if (activeConnection != null)
// Restrict to subset of records which match given identifiers
Collection<TrackedActiveConnection> activeConnections = new ArrayList<TrackedActiveConnection>(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); activeConnections.add(activeConnection);
}
} }
return activeConnections; return activeConnections;
@@ -101,6 +110,10 @@ public class ActiveConnectionService
public void deleteObject(AuthenticatedUser user, String identifier) public void deleteObject(AuthenticatedUser user, String identifier)
throws GuacamoleException { 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) // Close connection, if it exists (and we have permission)
ActiveConnection activeConnection = retrieveObject(user, identifier); ActiveConnection activeConnection = retrieveObject(user, identifier);
if (activeConnection != null) { if (activeConnection != null) {

View File

@@ -69,26 +69,40 @@ public class TrackedActiveConnection extends RestrictedObject implements ActiveC
/** /**
* Initializes this TrackedActiveConnection, copying the data associated * 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 * @param currentUser
* The user that created or retrieved this object. * The user that created or retrieved this object.
* *
* @param activeConnectionRecord * @param activeConnectionRecord
* The active connection record to copy. * 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, public void init(AuthenticatedUser currentUser,
ActiveConnectionRecord activeConnectionRecord) { ActiveConnectionRecord activeConnectionRecord,
boolean includeSensitiveInformation) {
super.init(currentUser); super.init(currentUser);
// Copy all data from given record // Copy all non-sensitive data from given record
this.connectionIdentifier = activeConnectionRecord.getConnection().getIdentifier(); this.connectionIdentifier = activeConnectionRecord.getConnection().getIdentifier();
this.identifier = activeConnectionRecord.getUUID().toString(); this.identifier = activeConnectionRecord.getUUID().toString();
this.remoteHost = activeConnectionRecord.getRemoteHost();
this.startDate = activeConnectionRecord.getStartDate(); 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();
}
} }

View File

@@ -28,8 +28,10 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser;
@@ -448,24 +450,33 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
public Collection<ActiveConnectionRecord> getActiveConnections(AuthenticatedUser user) public Collection<ActiveConnectionRecord> getActiveConnections(AuthenticatedUser user)
throws GuacamoleException { throws GuacamoleException {
// Only administrators may see all active connections // Simply return empty list if there are no active tunnels
if (!user.getUser().isAdministrator()) Collection<ActiveConnectionRecord> records = activeTunnels.values();
if (records.isEmpty())
return Collections.EMPTY_LIST; return Collections.EMPTY_LIST;
return Collections.unmodifiableCollection(activeTunnels.values()); // Build set of all connection identifiers associated with active tunnels
Set<String> identifiers = new HashSet<String>(records.size());
for (ActiveConnectionRecord record : records)
identifiers.add(record.getConnection().getIdentifier());
} // Produce collection of readable connection identifiers
Collection<ConnectionModel> connections = connectionMapper.selectReadable(user.getUser().getModel(), identifiers);
@Override // Ensure set contains only identifiers of readable connections
public ActiveConnectionRecord getActiveConnection(AuthenticatedUser user, identifiers.clear();
String tunnelUUID) throws GuacamoleException { for (ConnectionModel connection : connections)
identifiers.add(connection.getIdentifier());
// Only administrators may see all active connections // Produce readable subset of records
if (!user.getUser().isAdministrator()) Collection<ActiveConnectionRecord> visibleRecords = new ArrayList<ActiveConnectionRecord>(records.size());
return null; for (ActiveConnectionRecord record : records) {
if (identifiers.contains(record.getConnection().getIdentifier()))
visibleRecords.add(record);
}
return visibleRecords;
return activeTunnels.get(tunnelUUID);
} }
@Override @Override

View File

@@ -59,30 +59,6 @@ public interface GuacamoleTunnelService {
public Collection<ActiveConnectionRecord> getActiveConnections(AuthenticatedUser user) public Collection<ActiveConnectionRecord> getActiveConnections(AuthenticatedUser user)
throws GuacamoleException; 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 * Creates a socket for the given user which connects to the given
* connection. The given client information will be passed to guacd when * connection. The given client information will be passed to guacd when

View File

@@ -34,7 +34,9 @@ import org.glyptodon.guacamole.net.GuacamoleTunnel;
public interface ActiveConnection extends Identifiable { 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 * @return
* The identifier of the connection being actively used. * 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. * Returns the date and time the connection began.
* *
* @return * @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(); Date getStartDate();
@@ -61,7 +64,8 @@ public interface ActiveConnection extends Identifiable {
* Sets the date and time the connection began. * Sets the date and time the connection began.
* *
* @param startDate * @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); void setStartDate(Date startDate);
@@ -90,7 +94,8 @@ public interface ActiveConnection extends Identifiable {
* Returns the name of the user who is using this connection. * Returns the name of the user who is using this connection.
* *
* @return * @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(); String getUsername();
@@ -98,7 +103,8 @@ public interface ActiveConnection extends Identifiable {
* Sets the name of the user who is using this connection. * Sets the name of the user who is using this connection.
* *
* @param username * @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); void setUsername(String username);

View File

@@ -106,14 +106,14 @@ public class ActiveConnectionRESTService {
if (permissions != null && permissions.isEmpty()) if (permissions != null && permissions.isEmpty())
permissions = null; permissions = null;
// An admin user has access to any user // An admin user has access to any connection
SystemPermissionSet systemPermissions = self.getSystemPermissions(); SystemPermissionSet systemPermissions = self.getSystemPermissions();
boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER);
// Get the directory // Get the directory
Directory<ActiveConnection> activeConnectionDirectory = userContext.getActiveConnectionDirectory(); Directory<ActiveConnection> activeConnectionDirectory = userContext.getActiveConnectionDirectory();
// Filter users, if requested // Filter connections, if requested
Collection<String> activeConnectionIdentifiers = activeConnectionDirectory.getIdentifiers(); Collection<String> activeConnectionIdentifiers = activeConnectionDirectory.getIdentifiers();
if (!isAdmin && permissions != null) { if (!isAdmin && permissions != null) {
ObjectPermissionSet activeConnectionPermissions = self.getActiveConnectionPermissions(); ObjectPermissionSet activeConnectionPermissions = self.getActiveConnectionPermissions();

View File

@@ -88,11 +88,58 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
}, },
templateUrl: 'app/groupList/templates/guacGroupList.html', 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'); 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.<String, Number>
*/
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 * Returns whether the given item represents a connection that can
* be displayed. If there is no connection template, then no * be displayed. If there is no connection template, then no
@@ -131,7 +178,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
if (connectionGroup) { if (connectionGroup) {
// Create item hierarchy, including connections only if they will be visible // 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 root group is to be shown, wrap that group as the child of a fake root group
if ($scope.showRootGroup) if ($scope.showRootGroup)
@@ -161,7 +209,7 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
$scope.toggleExpanded = function toggleExpanded(groupListItem) { $scope.toggleExpanded = function toggleExpanded(groupListItem) {
groupListItem.isExpanded = !groupListItem.isExpanded; groupListItem.isExpanded = !groupListItem.isExpanded;
}; };
}] }]
}; };

View File

@@ -102,12 +102,14 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
this.isExpanded = template.isExpanded; 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. * connection group, if known.
* *
* @type Number * @type Number
*/ */
this.activeConnections = template.activeConnections; this.getActiveConnections = template.getActiveConnections || (function getActiveConnections() {
return null;
});
/** /**
* The connection or connection group whose data is exposed within * 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 * The connection whose contents should be represented by the new
* GroupListItem. * 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} * @returns {GroupListItem}
* A new GroupListItem which represents the given connection. * 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 item representing the given connection
return new GroupListItem({ return new GroupListItem({
@@ -144,7 +151,15 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
isConnectionGroup : false, isConnectionGroup : false,
// Count of currently active connections using this connection // 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 // Wrapped item
wrappedItem : connection wrappedItem : connection
@@ -165,26 +180,37 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio
* Whether connections should be included in the contents of the * Whether connections should be included in the contents of the
* resulting GroupListItem. By default, connections are included. * 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} * @returns {GroupListItem}
* A new GroupListItem which represents the given connection group, * A new GroupListItem which represents the given connection group,
* including all descendants. * including all descendants.
*/ */
GroupListItem.fromConnectionGroup = function fromConnectionGroup(connectionGroup, GroupListItem.fromConnectionGroup = function fromConnectionGroup(connectionGroup,
includeConnections) { includeConnections, countActiveConnections, countActiveConnectionGroups) {
var children = []; var children = [];
// Add any child connections // Add any child connections
if (connectionGroup.childConnections && includeConnections !== false) { if (connectionGroup.childConnections && includeConnections !== false) {
connectionGroup.childConnections.forEach(function addChildConnection(child) { connectionGroup.childConnections.forEach(function addChildConnection(child) {
children.push(GroupListItem.fromConnection(child)); children.push(GroupListItem.fromConnection(child, countActiveConnections));
}); });
} }
// Add any child groups // Add any child groups
if (connectionGroup.childConnectionGroups) { if (connectionGroup.childConnectionGroups) {
connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { 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, children : children,
// Count of currently active connection groups using this connection // 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 // Wrapped item
wrappedItem : connectionGroup wrappedItem : connectionGroup

View File

@@ -21,7 +21,7 @@
THE SOFTWARE. THE SOFTWARE.
--> -->
<div class="caption" ng-class="{active: item.activeConnections}"> <div class="caption" ng-class="{active: item.getActiveConnections()}">
<!-- Connection icon --> <!-- Connection icon -->
<div class="protocol"> <div class="protocol">
@@ -32,8 +32,8 @@
<span class="name">{{item.name}}</span> <span class="name">{{item.name}}</span>
<!-- Active user count --> <!-- Active user count -->
<span class="activeUserCount" ng-show="item.activeConnections"> <span class="activeUserCount" ng-show="item.getActiveConnections()">
{{'HOME.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.activeConnections}'}} {{'HOME.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.getActiveConnections()}'}}
</span> </span>
</div> </div>

View File

@@ -21,7 +21,7 @@
THE SOFTWARE. THE SOFTWARE.
--> -->
<div class="caption" ng-class="{active: item.activeConnections}"> <div class="caption" ng-class="{active: item.getActiveConnections()}">
<!-- Connection icon --> <!-- Connection icon -->
<div class="protocol"> <div class="protocol">
@@ -32,8 +32,8 @@
<span class="name">{{item.name}}</span> <span class="name">{{item.name}}</span>
<!-- Active user count --> <!-- Active user count -->
<span class="activeUserCount" ng-show="item.activeConnections"> <span class="activeUserCount" ng-show="item.getActiveConnections()">
{{'MANAGE_CONNECTION.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.activeConnections}'}} {{'MANAGE_CONNECTION.INFO_ACTIVE_USER_COUNT' | translate:'{USERS: item.getActiveConnections()}'}}
</span> </span>
</div> </div>

View File

@@ -59,7 +59,7 @@ angular.module('rest').factory('ActiveConnection', [function defineActiveConnect
/** /**
* The time that the connection began, in seconds since * 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 * @type Number
*/ */
@@ -73,7 +73,7 @@ angular.module('rest').factory('ActiveConnection', [function defineActiveConnect
this.remoteHost = template.remoteHost; 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 * @type String
*/ */