From 60152e38411e40fc9d059dda9bf9f8c33eb91773 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 14:05:32 -0700 Subject: [PATCH 1/8] GUACAMOLE-5: Include sharing profiles within connection tree. --- .../rest/connection/APIConnection.java | 32 ++++ .../connectiongroup/ConnectionGroupTree.java | 172 ++++++++++++++++-- .../main/webapp/app/rest/types/Connection.js | 9 + 3 files changed, 193 insertions(+), 20 deletions(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnection.java b/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnection.java index 639318ff0..cd0441331 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnection.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnection.java @@ -19,12 +19,14 @@ package org.apache.guacamole.rest.connection; +import java.util.Collection; import java.util.Map; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.protocol.GuacamoleConfiguration; +import org.apache.guacamole.rest.sharingprofile.APISharingProfile; /** * A simple connection to expose through the REST endpoints. @@ -65,6 +67,12 @@ public class APIConnection { */ private Map attributes; + /** + * All associated sharing profiles. If sharing profiles are not being + * queried, this may be omitted. + */ + private Collection sharingProfiles; + /** * The count of currently active connections using this connection. */ @@ -227,4 +235,28 @@ public class APIConnection { this.attributes = attributes; } + /** + * Returns a collection of all associated sharing profiles, or null if + * sharing profiles have not been queried. + * + * @return + * A collection of all associated sharing profiles, or null if sharing + * profiles have not been queried. + */ + public Collection getSharingProfiles() { + return sharingProfiles; + } + + /** + * Sets the collection of all associated sharing profiles to the given + * collection, which may be null if sharing profiles have not been queried. + * + * @param sharingProfiles + * The collection containing all sharing profiles associated with this + * connection, or null if sharing profiles have not been queried. + */ + public void setSharingProfiles(Collection sharingProfiles) { + this.sharingProfiles = sharingProfiles; + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupTree.java b/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupTree.java index d2f6f6fb3..2541fa2db 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupTree.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupTree.java @@ -28,16 +28,20 @@ import java.util.Map; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.Directory; +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.permission.ObjectPermission; import org.apache.guacamole.net.auth.permission.ObjectPermissionSet; import org.apache.guacamole.rest.connection.APIConnection; +import org.apache.guacamole.rest.sharingprofile.APISharingProfile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Provides access to the entire tree of connection groups and their - * connections. + * Provides access to the entire tree of connection groups, their + * connections, and any associated sharing profiles. * * @author Michael Jumper */ @@ -48,16 +52,38 @@ public class ConnectionGroupTree { */ private static final Logger logger = LoggerFactory.getLogger(ConnectionGroupTree.class); - /** - * The context of the user obtaining this tree. - */ - private final UserContext userContext; - /** * The root connection group as an APIConnectionGroup. */ private final APIConnectionGroup rootAPIGroup; + /** + * All connection permissions granted to the user obtaining this tree. + */ + private final ObjectPermissionSet connectionPermissions; + + /** + * All sharing profile permissions granted to the user obtaining this tree. + */ + private final ObjectPermissionSet sharingProfilePermissions; + + /** + * The directory of all connections visible to the user obtaining this tree. + */ + private final Directory connectionDirectory; + + /** + * The directory of all connection groups visible to the user obtaining this + * tree. + */ + private final Directory connectionGroupDirectory; + + /** + * The directory of all sharing profiles visible to the user obtaining this + * tree. + */ + private final Directory sharingProfileDirectory; + /** * All connection groups that have been retrieved, stored by their * identifiers. @@ -65,6 +91,12 @@ public class ConnectionGroupTree { private final Map retrievedGroups = new HashMap(); + /** + * All connections that have been retrieved, stored by their identifiers. + */ + private final Map retrievedConnections = + new HashMap(); + /** * Adds each of the provided connections to the current tree as children * of their respective parents. The parent connection groups must already @@ -95,7 +127,9 @@ public class ConnectionGroupTree { } // Add child - children.add(new APIConnection(connection)); + APIConnection apiConnection = new APIConnection(connection); + retrievedConnections.put(connection.getIdentifier(), apiConnection); + children.add(apiConnection); } @@ -150,7 +184,55 @@ public class ConnectionGroupTree { } // end for each connection group } - + + /** + * Adds each of the provided sharing profiles to the current tree as + * children of their respective primary connections. The primary connections + * must already be added. + * + * @param sharingProfiles + * The sharing profiles to add to the tree. + * + * @throws GuacamoleException + * If an error occurs while adding the sharing profiles to the tree. + */ + private void addSharingProfiles(Collection sharingProfiles) + throws GuacamoleException { + + // Add each sharing profile to the tree + for (SharingProfile sharingProfile : sharingProfiles) { + + // Retrieve the sharing profile's associated connection + String primaryConnectionIdentifier = sharingProfile.getPrimaryConnectionIdentifier(); + APIConnection primaryConnection = retrievedConnections.get(primaryConnectionIdentifier); + + // Add the sharing profile as a child of the primary connection + if (primaryConnection != null) { + + Collection children = primaryConnection.getSharingProfiles(); + + // Create child collection if it does not yet exist + if (children == null) { + children = new ArrayList(); + primaryConnection.setSharingProfiles(children); + } + + // Add child + children.add(new APISharingProfile(sharingProfile)); + + } + + // Warn of internal consistency issues + else + logger.debug("Sharing profile \"{}\" cannot be added to the " + + "tree: primary connection \"{}\" does not actually " + + "exist.", sharingProfile.getIdentifier(), + primaryConnectionIdentifier); + + } // end for each sharing profile + + } + /** * Adds all descendants of the given parent groups to their corresponding * parents already stored under root. @@ -167,7 +249,7 @@ public class ConnectionGroupTree { * @throws GuacamoleException * If an error occurs while retrieving the descendants. */ - private void addDescendants(Collection parents, + private void addConnectionGroupDescendants(Collection parents, List permissions) throws GuacamoleException { @@ -185,22 +267,64 @@ public class ConnectionGroupTree { } // Filter identifiers based on permissions, if requested - if (permissions != null && !permissions.isEmpty()) { - ObjectPermissionSet permissionSet = userContext.self().getConnectionPermissions(); - childConnectionIdentifiers = permissionSet.getAccessibleObjects(permissions, childConnectionIdentifiers); - } + if (permissions != null && !permissions.isEmpty()) + childConnectionIdentifiers = connectionPermissions.getAccessibleObjects( + permissions, childConnectionIdentifiers); // Retrieve child connections if (!childConnectionIdentifiers.isEmpty()) { - Collection childConnections = userContext.getConnectionDirectory().getAll(childConnectionIdentifiers); + Collection childConnections = connectionDirectory.getAll(childConnectionIdentifiers); addConnections(childConnections); + addConnectionDescendants(childConnections, permissions); } // Retrieve child connection groups if (!childConnectionGroupIdentifiers.isEmpty()) { - Collection childConnectionGroups = userContext.getConnectionGroupDirectory().getAll(childConnectionGroupIdentifiers); + Collection childConnectionGroups = connectionGroupDirectory.getAll(childConnectionGroupIdentifiers); addConnectionGroups(childConnectionGroups); - addDescendants(childConnectionGroups, permissions); + addConnectionGroupDescendants(childConnectionGroups, permissions); + } + + } + + /** + * Adds all descendant sharing profiles of the given connections to their + * corresponding primary connections already stored under root. + * + * @param connections + * The connections whose descendant sharing profiles should be added to + * the tree. + * + * @param permissions + * If specified and non-empty, limit added sharing profiles to only + * those for which the current user has any of the given + * permissions. Otherwise, all visible sharing profiles are added. + * + * @throws GuacamoleException + * If an error occurs while retrieving the descendants. + */ + private void addConnectionDescendants(Collection connections, + List permissions) + throws GuacamoleException { + + // If no connections, nothing to do + if (connections.isEmpty()) + return; + + // Build lists of sharing profile identifiers for retrieval + Collection identifiers = new ArrayList(); + for (Connection connection : connections) + identifiers.addAll(connection.getSharingProfileIdentifiers()); + + // Filter identifiers based on permissions, if requested + if (permissions != null && !permissions.isEmpty()) + identifiers = sharingProfilePermissions.getAccessibleObjects( + permissions, identifiers); + + // Retrieve and add all associated sharing profiles + if (!identifiers.isEmpty()) { + Collection sharingProfiles = sharingProfileDirectory.getAll(identifiers); + addSharingProfiles(sharingProfiles); } } @@ -229,14 +353,22 @@ public class ConnectionGroupTree { public ConnectionGroupTree(UserContext userContext, ConnectionGroup root, List permissions) throws GuacamoleException { - this.userContext = userContext; - // Store root of tree this.rootAPIGroup = new APIConnectionGroup(root); retrievedGroups.put(root.getIdentifier(), this.rootAPIGroup); + // Store user's current permissions + User self = userContext.self(); + this.connectionPermissions = self.getConnectionPermissions(); + this.sharingProfilePermissions = self.getSharingProfilePermissions(); + + // Store required directories + this.connectionDirectory = userContext.getConnectionDirectory(); + this.connectionGroupDirectory = userContext.getConnectionGroupDirectory(); + this.sharingProfileDirectory = userContext.getSharingProfileDirectory(); + // Add all descendants - addDescendants(Collections.singleton(root), permissions); + addConnectionGroupDescendants(Collections.singleton(root), permissions); } diff --git a/guacamole/src/main/webapp/app/rest/types/Connection.js b/guacamole/src/main/webapp/app/rest/types/Connection.js index e000761d8..b4639b23a 100644 --- a/guacamole/src/main/webapp/app/rest/types/Connection.js +++ b/guacamole/src/main/webapp/app/rest/types/Connection.js @@ -95,6 +95,15 @@ angular.module('rest').factory('Connection', [function defineConnection() { */ this.activeConnections = template.activeConnections; + /** + * An array of all associated sharing profiles, if known. This property + * may be null or undefined if sharing profiles have not been queried, + * and thus the sharing profiles are unknown. + * + * @type SharingProfile[] + */ + this.sharingProfiles = template.sharingProfiles; + }; return Connection; From da9ddf7683d3cbeb24fae5bdeb87258298467764 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 17:28:26 -0700 Subject: [PATCH 2/8] GUACAMOLE-5: Add support for sharing profiles to the guacGroupList directive. --- .../app/groupList/directives/guacGroupList.js | 30 ++++- .../groupList/templates/guacGroupList.html | 40 +++++-- .../app/groupList/types/GroupListItem.js | 105 +++++++++++++++--- 3 files changed, 147 insertions(+), 28 deletions(-) diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js index 0b5ed6f90..d5404c778 100644 --- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js +++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js @@ -67,6 +67,17 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() */ connectionGroupTemplate : '=', + /** + * The URL or ID of the Angular template to use when rendering a + * sharing profile. The @link{GroupListItem} associated with that + * sharing profile will be exposed within the scope of the template + * as item, and the arbitrary context object, if any, + * will be exposed as context. + * + * @type String + */ + sharingProfileTemplate : '=', + /** * Whether the root of the connection group hierarchy given should * be shown. If false (the default), only the descendants of the @@ -165,6 +176,22 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() return item.isConnectionGroup && !!$scope.connectionGroupTemplate; }; + /** + * Returns whether the given item represents a sharing profile that + * can be displayed. If there is no sharing profile template, then + * no sharing profile is visible. + * + * @param {GroupListItem} item + * The item to check. + * + * @returns {Boolean} + * true if the given item is a sharing profile that can be + * displayed, false otherwise. + */ + $scope.isVisibleSharingProfile = function isVisibleSharingProfile(item) { + return item.isSharingProfile && !!$scope.sharingProfileTemplate; + }; + // Set contents whenever the connection group is assigned or changed $scope.$watch('connectionGroups', function setContents(connectionGroups) { @@ -185,7 +212,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() // Create root item for current connection group var rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, - !!$scope.connectionTemplate, countActiveConnections); + !!$scope.connectionTemplate, !!$scope.sharingProfileTemplate, + countActiveConnections); // If root group is to be shown, add it as a root item if ($scope.showRootGroup) diff --git a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html index 19d9fd508..df9b5b6c1 100644 --- a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html +++ b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html @@ -1,38 +1,60 @@
-
-
+
diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js index a46eb204b..2fc9cf5b0 100644 --- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js @@ -37,16 +37,16 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio template = template || {}; /** - * The identifier of the data source associated with the connection or - * connection group this item represents. + * The identifier of the data source associated with the connection, + * connection group, or sharing profile this item represents. * * @type String */ this.dataSource = template.dataSource; /** - * The unique identifier associated with the connection or connection - * group this item represents. + * The unique identifier associated with the connection, connection + * group, or sharing profile this item represents. * * @type String */ @@ -78,7 +78,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio /** * Whether this item represents a connection. If this item represents - * a connection group, this MUST be false. + * a connection group or sharing profile, this MUST be false. * * @type Boolean */ @@ -86,12 +86,20 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio /** * Whether this item represents a connection group. If this item - * represents a connection, this MUST be false. + * represents a connection or sharing profile, this MUST be false. * * @type Boolean */ this.isConnectionGroup = template.isConnectionGroup; + /** + * Whether this item represents a sharing profile. If this item + * represents a connection or connection group, this MUST be false. + * + * @type Boolean + */ + this.isSharingProfile = template.isSharingProfile; + /** * Whether this item represents a balancing connection group. * @@ -107,8 +115,8 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio this.isExpanded = template.isExpanded; /** - * Returns the number of currently active users for this connection or - * connection group, if known. + * Returns the number of currently active users for this connection, + * connection group, or sharing profile, if known. * * @type Number */ @@ -117,10 +125,10 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio }); /** - * The connection or connection group whose data is exposed within - * this GroupListItem. + * The connection, connection group, or sharing profile whose data is + * exposed within this GroupListItem. * - * @type Connection|ConnectionGroup + * @type Connection|ConnectionGroup|SharingProfile */ this.wrappedItem = template.wrappedItem; @@ -137,6 +145,10 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * The connection whose contents should be represented by the new * GroupListItem. * + * @param {Boolean} [includeSharingProfiles=true] + * Whether sharing profiles should be included in the contents of the + * resulting GroupListItem. By default, sharing profiles 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 @@ -148,7 +160,17 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * A new GroupListItem which represents the given connection. */ GroupListItem.fromConnection = function fromConnection(dataSource, - connection, countActiveConnections) { + connection, includeSharingProfiles, countActiveConnections) { + + var children = []; + + // Add any sharing profiles + if (connection.sharingProfiles && includeSharingProfiles !== false) { + connection.sharingProfiles.forEach(function addSharingProfile(child) { + children.push(GroupListItem.fromSharingProfile(dataSource, + child, countActiveConnections)); + }); + } // Return item representing the given connection return new GroupListItem({ @@ -162,7 +184,11 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Type information isConnection : true, isConnectionGroup : false, - + isSharingProfile : false, + + // Already-converted children + children : children, + // Count of currently active connections using this connection getActiveConnections : function getActiveConnections() { @@ -197,6 +223,10 @@ 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 {Boolean} [includeSharingProfiles=true] + * Whether sharing profiles should be included in the contents of the + * resulting GroupListItem. By default, sharing profiles 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 @@ -216,8 +246,8 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio * including all descendants. */ GroupListItem.fromConnectionGroup = function fromConnectionGroup(dataSource, - connectionGroup, includeConnections, countActiveConnections, - countActiveConnectionGroups) { + connectionGroup, includeConnections, includeSharingProfiles, + countActiveConnections, countActiveConnectionGroups) { var children = []; @@ -225,7 +255,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio if (connectionGroup.childConnections && includeConnections !== false) { connectionGroup.childConnections.forEach(function addChildConnection(child) { children.push(GroupListItem.fromConnection(dataSource, child, - countActiveConnections)); + includeSharingProfiles, countActiveConnections)); }); } @@ -233,8 +263,8 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio if (connectionGroup.childConnectionGroups) { connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { children.push(GroupListItem.fromConnectionGroup(dataSource, - child, includeConnections, countActiveConnections, - countActiveConnectionGroups)); + child, includeConnections, includeSharingProfiles, + countActiveConnections, countActiveConnectionGroups)); }); } @@ -249,6 +279,7 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio // Type information isConnection : false, isConnectionGroup : true, + isSharingProfile : false, isBalancing : connectionGroup.type === ConnectionGroup.Type.BALANCING, // Already-converted children @@ -273,6 +304,44 @@ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', functio }; + /** + * Creates a new GroupListItem using the contents of the given sharing + * profile. + * + * @param {String} dataSource + * The identifier of the data source containing the given sharing + * profile. + * + * @param {SharingProfile} sharingProfile + * The sharing profile whose contents should be represented by the new + * GroupListItem. + * + * @returns {GroupListItem} + * A new GroupListItem which represents the given sharing profile. + */ + GroupListItem.fromSharingProfile = function fromSharingProfile(dataSource, + sharingProfile) { + + // Return item representing the given sharing profile + return new GroupListItem({ + + // Identifying information + name : sharingProfile.name, + identifier : sharingProfile.identifier, + dataSource : dataSource, + + // Type information + isConnection : false, + isConnectionGroup : false, + isSharingProfile : true, + + // Wrapped item + wrappedItem : sharingProfile + + }); + + }; + return GroupListItem; }]); From 85f15b7cd1378ff4572e05567c1b09d87e1a0331 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 17:50:33 -0700 Subject: [PATCH 3/8] GUACAMOLE-5: Correct and simply guacGroupList styling/structure. --- .../webapp/app/home/templates/connection.html | 24 +++++------ .../main/webapp/app/index/styles/lists.css | 15 ++----- .../src/main/webapp/app/index/styles/ui.css | 42 ++++++++++++------- .../templates/connectionGroupPermission.html | 6 +++ .../templates/connectionPermission.html | 6 +-- .../app/settings/templates/connection.html | 24 +++++------ 6 files changed, 58 insertions(+), 59 deletions(-) diff --git a/guacamole/src/main/webapp/app/home/templates/connection.html b/guacamole/src/main/webapp/app/home/templates/connection.html index 01f58bb52..8e8ccc2dd 100644 --- a/guacamole/src/main/webapp/app/home/templates/connection.html +++ b/guacamole/src/main/webapp/app/home/templates/connection.html @@ -1,19 +1,15 @@ - + -
+ +
- -
-
-
+ + {{item.name}} - - {{item.name}} - - - + + -
diff --git a/guacamole/src/main/webapp/app/index/styles/lists.css b/guacamole/src/main/webapp/app/index/styles/lists.css index 536169fc5..5ce162a13 100644 --- a/guacamole/src/main/webapp/app/index/styles/lists.css +++ b/guacamole/src/main/webapp/app/index/styles/lists.css @@ -44,15 +44,16 @@ color: black; } -.connection:hover { +.recent-connections .connection:hover { background: #CDA; } -.connection .thumbnail { +.recent-connections .connection .thumbnail { + display: block; margin: 0.5em; } -.connection .thumbnail > * { +.recent-connections .connection .thumbnail > * { border: 1px solid black; background: black; box-shadow: 1px 1px 5px black; @@ -60,14 +61,6 @@ display: inline-block; } -div.recent-connections .connection .thumbnail { - display: block; -} - -div.recent-connections .protocol { - display: none; -} - .caption * { vertical-align: middle; } diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css index d91becf91..f324e3d29 100644 --- a/guacamole/src/main/webapp/app/index/styles/ui.css +++ b/guacamole/src/main/webapp/app/index/styles/ui.css @@ -164,14 +164,11 @@ div.section { background-image: url('images/action-icons/guac-monitor-add.png'); } -.protocol { +.connection .icon, +.connection-group .icon { display: inline-block; -} - -.protocol .icon { width: 24px; height: 24px; - background-image: url('images/protocol-icons/guac-plug.png'); background-size: 16px 16px; -moz-background-size: 16px 16px; -webkit-background-size: 16px 16px; @@ -180,44 +177,57 @@ div.section { background-position: center center; } -.protocol .icon.ssh, -.protocol .icon.telnet { +.group .icon { + background-image: url('images/folder-closed.png'); +} + +.group.expanded .icon { + background-image: url('images/folder-open.png'); +} + +.connection .icon { + background-image: url('images/protocol-icons/guac-plug.png'); +} + +.connection .icon.ssh, +.connection .icon.telnet { background-image: url('images/protocol-icons/guac-text.png'); } -.protocol .icon.vnc, -.protocol .icon.rdp { +.connection .icon.vnc, +.connection .icon.rdp { background-image: url('images/protocol-icons/guac-monitor.png'); } + /* * Groups */ -.group > .children { +.expandable > .children { margin-left: 13px; - padding-left: 6px; + padding-left: 13px; } -.group .icon.group.type.empty.balancer { +.group.empty.balancer .icon { opacity: 1; background-image: url('images/protocol-icons/guac-monitor.png'); } -.group.expanded > .children { +.expandable.expanded > .children { display: block; border-left: 1px dotted rgba(0, 0, 0, 0.25); } -.group > .caption .icon.group { +.expandable > .caption .icon.expand { opacity: 0.75; background-image: url('images/group-icons/guac-closed.png'); } -.group .icon.type.group.expanded { +.expandable.expanded .icon.expand { background-image: url('images/group-icons/guac-open.png'); } -.group .icon.type.group.empty { +.expandable.empty .icon.expand { opacity: 0.25; background-image: url('images/group-icons/guac-open.png'); } diff --git a/guacamole/src/main/webapp/app/manage/templates/connectionGroupPermission.html b/guacamole/src/main/webapp/app/manage/templates/connectionGroupPermission.html index b43376b8b..6b27055a6 100644 --- a/guacamole/src/main/webapp/app/manage/templates/connectionGroupPermission.html +++ b/guacamole/src/main/webapp/app/manage/templates/connectionGroupPermission.html @@ -1,7 +1,13 @@
+ +
+ + + {{item.name}} +
diff --git a/guacamole/src/main/webapp/app/manage/templates/connectionPermission.html b/guacamole/src/main/webapp/app/manage/templates/connectionPermission.html index 8ccaf440a..8f1bb8b11 100644 --- a/guacamole/src/main/webapp/app/manage/templates/connectionPermission.html +++ b/guacamole/src/main/webapp/app/manage/templates/connectionPermission.html @@ -1,11 +1,9 @@
-
-
-
+
- + diff --git a/guacamole/src/main/webapp/app/settings/templates/connection.html b/guacamole/src/main/webapp/app/settings/templates/connection.html index 0a6001c50..7db6835d6 100644 --- a/guacamole/src/main/webapp/app/settings/templates/connection.html +++ b/guacamole/src/main/webapp/app/settings/templates/connection.html @@ -1,19 +1,15 @@ - + -
+ +
- -
-
-
+ + {{item.name}} - - {{item.name}} + + - - - -
From 8a8ae8496cee67f055266d63d64eae2c5d24f9e0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 18:15:28 -0700 Subject: [PATCH 4/8] GUACAMOLE-5: Add support for sharing profile permissions to JavaScript implementations of REST API objects. --- .../app/rest/services/permissionService.js | 4 + .../app/rest/types/PermissionFlagSet.js | 22 +++++ .../webapp/app/rest/types/PermissionSet.js | 88 ++++++++++++++++++- 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/rest/services/permissionService.js b/guacamole/src/main/webapp/app/rest/services/permissionService.js index 8a4184526..67fceba7e 100644 --- a/guacamole/src/main/webapp/app/rest/services/permissionService.js +++ b/guacamole/src/main/webapp/app/rest/services/permissionService.js @@ -175,6 +175,10 @@ angular.module('rest').factory('permissionService', ['$injector', addObjectPatchOperations(patch, operation, "/connectionGroupPermissions", permissions.connectionGroupPermissions); + // Add sharing profile permission operations to patch + addObjectPatchOperations(patch, operation, "/sharingProfilePermissions", + permissions.sharingProfilePermissions); + // Add active connection permission operations to patch addObjectPatchOperations(patch, operation, "/activeConnectionPermissions", permissions.activeConnectionPermissions); diff --git a/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js b/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js index be2771cf4..64b942bea 100644 --- a/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js +++ b/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js @@ -87,6 +87,25 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet', 'ADMINISTER' : {} }; + /** + * The granted state of each permission for each sharing profile, as a + * map of object permission type string to permission map. The + * permission map is, in turn, a map of sharing profile identifier to + * boolean value. A particular permission is granted if its + * corresponding boolean value is set to true. Valid permission type + * strings are defined within PermissionSet.ObjectPermissionType. + * Permissions which are not granted may be set to false, but this is + * not required. + * + * @type Object.> + */ + this.sharingProfilePermissions = template.sharingProfilePermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER' : {} + }; + /** * The granted state of each permission for each active connection, as * a map of object permission type string to permission map. The @@ -188,6 +207,9 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet', // Add all granted connection group permissions addObjectPermissions(permissionSet.connectionGroupPermissions, permissionFlagSet.connectionGroupPermissions); + // Add all granted sharing profile permissions + addObjectPermissions(permissionSet.sharingProfilePermissions, permissionFlagSet.sharingProfilePermissions); + // Add all granted active connection permissions addObjectPermissions(permissionSet.activeConnectionPermissions, permissionFlagSet.activeConnectionPermissions); diff --git a/guacamole/src/main/webapp/app/rest/types/PermissionSet.js b/guacamole/src/main/webapp/app/rest/types/PermissionSet.js index 2ac418e03..8fd1ef684 100644 --- a/guacamole/src/main/webapp/app/rest/types/PermissionSet.js +++ b/guacamole/src/main/webapp/app/rest/types/PermissionSet.js @@ -53,6 +53,15 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() * @type Object. */ this.connectionGroupPermissions = template.connectionGroupPermissions || {}; + + /** + * Map of sharing profile identifiers to the corresponding array of + * granted permissions. Each permission is represented by a string + * listed within PermissionSet.ObjectPermissionType. + * + * @type Object. + */ + this.sharingProfilePermissions = template.sharingProfilePermissions || {}; /** * Map of active connection identifiers to the corresponding array of @@ -132,7 +141,12 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() /** * Permission to create new connection groups. */ - CREATE_CONNECTION_GROUP : "CREATE_CONNECTION_GROUP" + CREATE_CONNECTION_GROUP : "CREATE_CONNECTION_GROUP", + + /** + * Permission to create new sharing profiles. + */ + CREATE_SHARING_PROFILE : "CREATE_SHARING_PROFILE" }; @@ -247,6 +261,28 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() return hasPermission(permSet.connectionGroupPermissions, type, identifier); }; + /** + * Returns whether the given permission is granted for the sharing profile + * having the given ID. + * + * @param {PermissionSet|Object} permSet + * The permission set to check. + * + * @param {String} type + * The permission to search for, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param {String} identifier + * The identifier of the sharing profile to which the permission + * applies. + * + * @returns {Boolean} + * true if the permission is present (granted), false otherwise. + */ + PermissionSet.hasSharingProfilePermission = function hasSharingProfilePermission(permSet, type, identifier) { + return hasPermission(permSet.sharingProfilePermissions, type, identifier); + }; + /** * Returns whether the given permission is granted for the active * connection having the given ID. @@ -548,6 +584,56 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() return removeObjectPermission(permSet.connectionGroupPermissions, type, identifier); }; + /** + * Adds the given sharing profile permission applying to the sharing profile + * with the given ID to the given permission set, if not already present. If + * the permission is already present, this function has no effect. + * + * @param {PermissionSet} permSet + * The permission set to modify. + * + * @param {String} type + * The permission to add, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param {String} identifier + * The identifier of the sharing profile to which the permission + * applies. + * + * @returns {Boolean} + * true if the permission was added, false if the permission was + * already present in the given permission set. + */ + PermissionSet.addSharingProfilePermission = function addSharingProfilePermission(permSet, type, identifier) { + permSet.sharingProfilePermissions = permSet.sharingProfilePermissions || {}; + return addObjectPermission(permSet.sharingProfilePermissions, type, identifier); + }; + + /** + * Removes the given sharing profile permission applying to the sharing + * profile with the given ID from the given permission set, if present. If + * the permission is not present, this function has no effect. + * + * @param {PermissionSet} permSet + * The permission set to modify. + * + * @param {String} type + * The permission to remove, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param {String} identifier + * The identifier of the sharing profile to which the permission + * applies. + * + * @returns {Boolean} + * true if the permission was removed, false if the permission was not + * present in the given permission set. + */ + PermissionSet.removeSharingProfilePermission = function removeSharingProfilePermission(permSet, type, identifier) { + permSet.sharingProfilePermissions = permSet.sharingProfilePermissions || {}; + return removeObjectPermission(permSet.sharingProfilePermissions, type, identifier); + }; + /** * Adds the given active connection permission applying to the connection * group with the given ID to the given permission set, if not already From e3db19d633c1123ec0e071c8a998a7dca6ad54d3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 18:21:32 -0700 Subject: [PATCH 5/8] GUACAMOLE-5: Implement management interface for sharing profile permissions. --- .../src/main/webapp/app/index/styles/ui.css | 7 +- .../controllers/manageUserController.js | 65 +++++++++++++++++++ .../app/manage/templates/manageUser.html | 1 + .../templates/sharingProfilePermission.html | 13 ++++ .../src/main/webapp/translations/en.json | 1 + 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/webapp/app/manage/templates/sharingProfilePermission.html diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css index f324e3d29..67e34e48f 100644 --- a/guacamole/src/main/webapp/app/index/styles/ui.css +++ b/guacamole/src/main/webapp/app/index/styles/ui.css @@ -165,7 +165,8 @@ div.section { } .connection .icon, -.connection-group .icon { +.connection-group .icon, +.sharing-profile .icon { display: inline-block; width: 24px; height: 24px; @@ -199,6 +200,10 @@ div.section { background-image: url('images/protocol-icons/guac-monitor.png'); } +.sharing-profile .icon { + background-image: url('images/share.png'); +} + /* * Groups */ diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 2a308f15d..36d00c624 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -633,6 +633,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto { label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", value: PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP + }, + { + label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + value: PermissionSet.SystemPermissionType.CREATE_SHARING_PROFILE } ]; @@ -861,6 +865,45 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; + /** + * Updates the permissionsAdded and permissionsRemoved permission sets to + * reflect the addition of the given sharing profile permission. + * + * @param {String} identifier + * The identifier of the sharing profile to add READ permission for. + */ + var addSharingProfilePermission = function addSharingProfilePermission(identifier) { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasSharingProfilePermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeSharingProfilePermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addSharingProfilePermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets to + * reflect the removal of the given sharing profile permission. + * + * @param {String} identifier + * The identifier of the sharing profile to remove READ permission for. + */ + var removeSharingProfilePermission = function removeSharingProfilePermission(identifier) { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasSharingProfilePermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeSharingProfilePermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addSharingProfilePermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + }; + + // Expose permission query and modification functions to group list template $scope.groupListContext = { @@ -918,6 +961,28 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto else removeConnectionGroupPermission(identifier); + }, + + /** + * Notifies the controller that a change has been made to the given + * sharing profile permission for the user being edited. This only + * applies to READ permissions. + * + * @param {String} identifier + * The identifier of the sharing profile affected by the changed + * permission. + */ + sharingProfilePermissionChanged : function sharingProfilePermissionChanged(identifier) { + + // Determine current permission setting + var value = $scope.permissionFlags.sharingProfilePermissions.READ[identifier]; + + // Add/remove permission depending on flag state + if (value) + addSharingProfilePermission(identifier); + else + removeSharingProfilePermission(identifier); + } }; diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 14edccda1..82bc1e978 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -79,6 +79,7 @@ context="groupListContext" connection-groups="filteredRootGroups" connection-template="'app/manage/templates/connectionPermission.html'" + sharing-profile-template="'app/manage/templates/sharingProfilePermission.html'" connection-group-template="'app/manage/templates/connectionGroupPermission.html'" page-size="20"/>
diff --git a/guacamole/src/main/webapp/app/manage/templates/sharingProfilePermission.html b/guacamole/src/main/webapp/app/manage/templates/sharingProfilePermission.html new file mode 100644 index 000000000..bc2b1302c --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/templates/sharingProfilePermission.html @@ -0,0 +1,13 @@ +
+ + +
+ + + + + + {{item.name}} + +
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index 3c9ce9ccd..e9914368c 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -256,6 +256,7 @@ "FIELD_HEADER_CREATE_NEW_USERS" : "Create new users:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Create new connections:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Create new connection groups:", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Create new sharing profiles:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Username:", From f4ad97e73286b972ef33b9fa4f3a761daeb74061 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 18:54:17 -0700 Subject: [PATCH 6/8] GUACAMOLE-5: Switch to nice rendering of connection tree lines. --- .../src/main/webapp/app/index/styles/ui.css | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css index 67e34e48f..d5174456b 100644 --- a/guacamole/src/main/webapp/app/index/styles/ui.css +++ b/guacamole/src/main/webapp/app/index/styles/ui.css @@ -214,17 +214,42 @@ div.section { } .group.empty.balancer .icon { - opacity: 1; background-image: url('images/protocol-icons/guac-monitor.png'); } -.expandable.expanded > .children { +.expandable.expanded > .children > .list-item { + position: relative; +} + +.expandable.expanded > .children > .list-item:before, +.expandable.expanded > .children > .list-item:after { display: block; - border-left: 1px dotted rgba(0, 0, 0, 0.25); + content: ' '; + position: absolute; + z-index: -1; +} + +.expandable.expanded > .children > .list-item:before { + border-left: 1px solid #BBB; + left: -13px; + top: -0.75em; + bottom: 0; +} + +.expandable.expanded > .children > .list-item:last-child:before { + height: 1.5em; +} + +.expandable.expanded > .children > .list-item:after { + display: block; + content: ' '; + border-bottom: 1px solid #BBB; + left: -13px; + width: 13px; + top: 0.75em; } .expandable > .caption .icon.expand { - opacity: 0.75; background-image: url('images/group-icons/guac-closed.png'); } From 031763af05508c95556ae07b2d39b6b09fe247ab Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 19:18:30 -0700 Subject: [PATCH 7/8] GUACAMOLE-5: Fix CSS selector match of open/closed groups. --- guacamole/src/main/webapp/app/index/styles/ui.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css index d5174456b..62db41fb5 100644 --- a/guacamole/src/main/webapp/app/index/styles/ui.css +++ b/guacamole/src/main/webapp/app/index/styles/ui.css @@ -178,11 +178,11 @@ div.section { background-position: center center; } -.group .icon { +.group > .caption .icon { background-image: url('images/folder-closed.png'); } -.group.expanded .icon { +.group.expanded > .caption .icon { background-image: url('images/folder-open.png'); } @@ -253,11 +253,11 @@ div.section { background-image: url('images/group-icons/guac-closed.png'); } -.expandable.expanded .icon.expand { +.expandable.expanded > .caption .icon.expand { background-image: url('images/group-icons/guac-open.png'); } -.expandable.empty .icon.expand { +.expandable.empty > .caption .icon.expand { opacity: 0.25; background-image: url('images/group-icons/guac-open.png'); } From f3a201a4152504388063387ac11d77dbf582eb51 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Aug 2016 20:42:19 -0700 Subject: [PATCH 8/8] GUACAMOLE-5: Rename permission granted/revoked flag sensibly. --- .../controllers/manageUserController.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 36d00c624..7a4db92b5 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -709,10 +709,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.systemPermissionChanged = function systemPermissionChanged(type) { // Determine current permission setting - var value = $scope.permissionFlags.systemPermissions[type]; + var granted = $scope.permissionFlags.systemPermissions[type]; // Add/remove permission depending on flag state - if (value) + if (granted) addSystemPermission(type); else removeSystemPermission(type); @@ -779,10 +779,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.userPermissionChanged = function userPermissionChanged(type, identifier) { // Determine current permission setting - var value = $scope.permissionFlags.userPermissions[type][identifier]; + var granted = $scope.permissionFlags.userPermissions[type][identifier]; // Add/remove permission depending on flag state - if (value) + if (granted) addUserPermission(type, identifier); else removeUserPermission(type, identifier); @@ -931,10 +931,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto connectionPermissionChanged : function connectionPermissionChanged(identifier) { // Determine current permission setting - var value = $scope.permissionFlags.connectionPermissions.READ[identifier]; + var granted = $scope.permissionFlags.connectionPermissions.READ[identifier]; // Add/remove permission depending on flag state - if (value) + if (granted) addConnectionPermission(identifier); else removeConnectionPermission(identifier); @@ -953,10 +953,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto connectionGroupPermissionChanged : function connectionGroupPermissionChanged(identifier) { // Determine current permission setting - var value = $scope.permissionFlags.connectionGroupPermissions.READ[identifier]; + var granted = $scope.permissionFlags.connectionGroupPermissions.READ[identifier]; // Add/remove permission depending on flag state - if (value) + if (granted) addConnectionGroupPermission(identifier); else removeConnectionGroupPermission(identifier); @@ -975,10 +975,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto sharingProfilePermissionChanged : function sharingProfilePermissionChanged(identifier) { // Determine current permission setting - var value = $scope.permissionFlags.sharingProfilePermissions.READ[identifier]; + var granted = $scope.permissionFlags.sharingProfilePermissions.READ[identifier]; // Add/remove permission depending on flag state - if (value) + if (granted) addSharingProfilePermission(identifier); else removeSharingProfilePermission(identifier);