From 9f01fcb1558b11b52c78bcddee2ea601ab4b102c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Apr 2018 14:38:24 -0700 Subject: [PATCH 01/11] GUACAMOLE-220: Add JavaScript service for retrieving/manipulating user groups. --- .../webapp/app/rest/services/cacheService.js | 3 +- .../app/rest/services/membershipService.js | 385 ++++++++++++++++++ .../app/rest/services/userGroupService.js | 223 ++++++++++ .../app/rest/types/RelatedObjectPatch.js | 85 ++++ .../main/webapp/app/rest/types/UserGroup.js | 59 +++ 5 files changed, 754 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/webapp/app/rest/services/membershipService.js create mode 100644 guacamole/src/main/webapp/app/rest/services/userGroupService.js create mode 100644 guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js create mode 100644 guacamole/src/main/webapp/app/rest/types/UserGroup.js diff --git a/guacamole/src/main/webapp/app/rest/services/cacheService.js b/guacamole/src/main/webapp/app/rest/services/cacheService.js index 55b7fc1d2..9a32004d3 100644 --- a/guacamole/src/main/webapp/app/rest/services/cacheService.js +++ b/guacamole/src/main/webapp/app/rest/services/cacheService.js @@ -60,7 +60,8 @@ angular.module('rest').factory('cacheService', ['$injector', service.schema = $cacheFactory('API-SCHEMA'); /** - * Shared cache used by both userService and permissionService. + * Shared cache used by userService, userGroupService, permissionService, + * and membershipService. * * @type $cacheFactory.Cache */ diff --git a/guacamole/src/main/webapp/app/rest/services/membershipService.js b/guacamole/src/main/webapp/app/rest/services/membershipService.js new file mode 100644 index 000000000..58181c897 --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/services/membershipService.js @@ -0,0 +1,385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service for operating on user group memberships via the REST API. + */ +angular.module('rest').factory('membershipService', ['$injector', + function membershipService($injector) { + + // Required services + var requestService = $injector.get('requestService'); + var authenticationService = $injector.get('authenticationService'); + var cacheService = $injector.get('cacheService'); + + // Required types + var RelatedObjectPatch = $injector.get('RelatedObjectPatch'); + + var service = {}; + + /** + * Creates a new array of patches which represents the given changes to an + * arbitrary set of objects sharing some common relation. + * + * @param {String[]} [identifiersToAdd] + * The identifiers of all objects which should be added to the + * relation, if any. + * + * @param {String[]} [identifiersToRemove] + * The identifiers of all objects which should be removed from the + * relation, if any. + * + * @returns {RelatedObjectPatch[]} + * A new array of patches which represents the given changes. + */ + var getRelatedObjectPatch = function getRelatedObjectPatch(identifiersToAdd, identifiersToRemove) { + + var patch = []; + + angular.forEach(identifiersToAdd, function addIdentifier(identifier) { + patch.push(new RelatedObjectPatch({ + op : RelatedObjectPatch.Operation.ADD, + value : identifier + })); + }); + + angular.forEach(identifiersToRemove, function removeIdentifier(identifier) { + patch.push(new RelatedObjectPatch({ + op : RelatedObjectPatch.Operation.REMOVE, + value : identifier + })); + }); + + return patch; + + }; + + /** + * Returns the URL for the REST resource most appropriate for accessing + * the parent user groups of the user or group having the given identifier. + * + * It is important to note that a particular data source can authenticate + * and provide user groups for a user, even if that user does not exist + * within that data source (and thus cannot be found beneath + * "api/session/data/{dataSource}/users") + * + * @param {String} dataSource + * The unique identifier of the data source containing the user or + * group whose parent user groups should be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * + * @param {String} identifier + * The identifier of the user or group for which the URL of the proper + * REST resource should be derived. + * + * @param {Boolean} [group] + * Whether the provided identifier refers to a user group. If false or + * omitted, the identifier given is assumed to refer to a user. + * + * @returns {String} + * The URL for the REST resource representing the parent user groups of + * the user or group having the given identifier. + */ + var getUserGroupsResourceURL = function getUserGroupsResourceURL(dataSource, identifier, group) { + + // Create base URL for data source + var base = 'api/session/data/' + encodeURIComponent(dataSource); + + // Access parent groups directly (there is no "self" for user groups + // as there is for users) + if (group) + return base + '/userGroups/' + encodeURIComponent(identifier) + '/userGroups'; + + // If the username is that of the current user, do not rely on the + // user actually existing (they may not). Access their parent groups via + // "self" rather than the collection of defined users. + if (identifier === authenticationService.getCurrentUsername()) + return base + '/self/userGroups'; + + // Otherwise, the user must exist for their parent groups to be + // accessible. Use the collection of defined users. + return base + '/users/' + encodeURIComponent(identifier) + '/userGroups'; + + }; + + /** + * Makes a request to the REST API to retrieve the identifiers of all + * parent user groups of which a given user or group is a member, returning + * a promise that can be used for processing the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user or + * group whose parent user groups should be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * + * @param {String} identifier + * The identifier of the user or group to retrieve the parent user + * groups of. + * + * @param {Boolean} [group] + * Whether the provided identifier refers to a user group. If false or + * omitted, the identifier given is assumed to refer to a user. + * + * @returns {Promise.} + * A promise for the HTTP call which will resolve with an array + * containing the requested identifiers upon success. + */ + service.getUserGroups = function getUserGroups(dataSource, identifier, group) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve parent groups + return requestService({ + cache : cacheService.users, + method : 'GET', + url : getUserGroupsResourceURL(dataSource, identifier, group), + params : httpParameters + }); + + }; + + /** + * Makes a request to the REST API to modify the parent user groups of + * which a given user or group is a member, returning a promise that can be + * used for processing the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user or + * group whose parent user groups should be modified. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * + * @param {String} identifier + * The identifier of the user or group to modify the parent user + * groups of. + * + * @param {String[]} [addToUserGroups] + * The identifier of all parent user groups to which the given user or + * group should be added as a member, if any. + * + * @param {String[]} [removeFromUserGroups] + * The identifier of all parent user groups from which the given member + * user or group should be removed, if any. + * + * @param {Boolean} [group] + * Whether the provided identifier refers to a user group. If false or + * omitted, the identifier given is assumed to refer to a user. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchUserGroups = function patchUserGroups(dataSource, identifier, + addToUserGroups, removeFromUserGroups, group) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Update parent user groups + return requestService({ + method : 'PATCH', + url : getUserGroupsResourceURL(dataSource, identifier, group), + params : httpParameters, + data : getRelatedObjectPatch(addToUserGroups, removeFromUserGroups) + }) + + // Clear the cache + .then(function parentUserGroupsChanged(){ + cacheService.users.removeAll(); + }); + + }; + + /** + * Makes a request to the REST API to retrieve the identifiers of all + * users which are members of the given user group, returning a promise + * that can be used for processing the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user group + * whose member users should be retrieved. This identifier corresponds + * to an AuthenticationProvider within the Guacamole web application. + * + * @param {String} identifier + * The identifier of the user group to retrieve the member users of. + * + * @returns {Promise.} + * A promise for the HTTP call which will resolve with an array + * containing the requested identifiers upon success. + */ + service.getMemberUsers = function getMemberUsers(dataSource, identifier) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve member users + return requestService({ + cache : cacheService.users, + method : 'GET', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers', + params : httpParameters + }); + + }; + + /** + * Makes a request to the REST API to modify the member users of a given + * user group, returning a promise that can be used for processing the + * results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user group + * whose member users should be modified. This identifier corresponds + * to an AuthenticationProvider within the Guacamole web application. + * + * @param {String} identifier + * The identifier of the user group to modify the member users of. + * + * @param {String[]} [usersToAdd] + * The identifier of all users to add as members of the given user + * group, if any. + * + * @param {String[]} [usersToRemove] + * The identifier of all users to remove from the given user group, + * if any. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchMemberUsers = function patchMemberUsers(dataSource, identifier, + usersToAdd, usersToRemove) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Update member users + return requestService({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers', + params : httpParameters, + data : getRelatedObjectPatch(usersToAdd, usersToRemove) + }) + + // Clear the cache + .then(function memberUsersChanged(){ + cacheService.users.removeAll(); + }); + + }; + + /** + * Makes a request to the REST API to retrieve the identifiers of all + * user groups which are members of the given user group, returning a + * promise that can be used for processing the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user group + * whose member user groups should be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * + * @param {String} identifier + * The identifier of the user group to retrieve the member user + * groups of. + * + * @returns {Promise.} + * A promise for the HTTP call which will resolve with an array + * containing the requested identifiers upon success. + */ + service.getMemberUserGroups = function getMemberUserGroups(dataSource, identifier) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve member user groups + return requestService({ + cache : cacheService.users, + method : 'GET', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups', + params : httpParameters + }); + + }; + + /** + * Makes a request to the REST API to modify the member user groups of a + * given user group, returning a promise that can be used for processing + * the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user group + * whose member user groups should be modified. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * + * @param {String} identifier + * The identifier of the user group to modify the member user groups of. + * + * @param {String[]} [userGroupsToAdd] + * The identifier of all user groups to add as members of the given + * user group, if any. + * + * @param {String[]} [userGroupsToRemove] + * The identifier of all member user groups to remove from the given + * user group, if any. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchMemberUserGroups = function patchMemberUserGroups(dataSource, + identifier, userGroupsToAdd, userGroupsToRemove) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Update member user groups + return requestService({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups', + params : httpParameters, + data : getRelatedObjectPatch(userGroupsToAdd, userGroupsToRemove) + }) + + // Clear the cache + .then(function memberUserGroupsChanged(){ + cacheService.users.removeAll(); + }); + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/rest/services/userGroupService.js b/guacamole/src/main/webapp/app/rest/services/userGroupService.js new file mode 100644 index 000000000..ad29837f5 --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/services/userGroupService.js @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service for operating on user groups via the REST API. + */ +angular.module('rest').factory('userGroupService', ['$injector', + function userGroupService($injector) { + + // Required services + var requestService = $injector.get('requestService'); + var authenticationService = $injector.get('authenticationService'); + var cacheService = $injector.get('cacheService'); + + var service = {}; + + /** + * Makes a request to the REST API to get the list of user groups, + * returning a promise that provides an array of @link{UserGroup} objects if + * successful. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user groups + * to be retrieved. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * + * @param {String[]} [permissionTypes] + * The set of permissions to filter with. A user group must have one or + * more of these permissions for a user group to appear in the result. + * If null, no filtering will be performed. Valid values are listed + * within PermissionSet.ObjectType. + * + * @returns {Promise.>} + * A promise which will resolve with a map of @link{UserGroup} objects + * where each key is the identifier of the corresponding user group. + */ + service.getUserGroups = function getUserGroups(dataSource, permissionTypes) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Add permission filter if specified + if (permissionTypes) + httpParameters.permission = permissionTypes; + + // Retrieve user groups + return requestService({ + cache : cacheService.users, + method : 'GET', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + params : httpParameters + }); + + }; + + /** + * Makes a request to the REST API to get the user group having the given + * identifier, returning a promise that provides the corresponding + * @link{UserGroup} if successful. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user group to + * be retrieved. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * + * @param {String} identifier + * The identifier of the user group to retrieve. + * + * @returns {Promise.} + * A promise which will resolve with a @link{UserGroup} upon success. + */ + service.getUserGroup = function getUserGroup(dataSource, identifier) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve user group + return requestService({ + cache : cacheService.users, + method : 'GET', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier), + params : httpParameters + }); + + }; + + /** + * Makes a request to the REST API to delete a user group, returning a + * promise that can be used for processing the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user group to + * be deleted. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param {UserGroup} userGroup + * The user group to delete. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * delete operation is successful. + */ + service.deleteUserGroup = function deleteUserGroup(dataSource, userGroup) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Delete user group + return requestService({ + method : 'DELETE', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier), + params : httpParameters + }) + + // Clear the cache + .then(function userGroupDeleted(){ + cacheService.users.removeAll(); + }); + + + }; + + /** + * Makes a request to the REST API to create a user group, returning a promise + * that can be used for processing the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source in which the user group + * should be created. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * + * @param {UserGroup} userGroup + * The user group to create. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * create operation is successful. + */ + service.createUserGroup = function createUserGroup(dataSource, userGroup) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Create user group + return requestService({ + method : 'POST', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + params : httpParameters, + data : userGroup + }) + + // Clear the cache + .then(function userGroupCreated(){ + cacheService.users.removeAll(); + }); + + }; + + /** + * Makes a request to the REST API to save a user group, returning a + * promise that can be used for processing the results of the call. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user group to + * be updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param {UserGroup} userGroup + * The user group to update. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * save operation is successful. + */ + service.saveUserGroup = function saveUserGroup(dataSource, userGroup) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Update user group + return requestService({ + method : 'PUT', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier), + params : httpParameters, + data : userGroup + }) + + // Clear the cache + .then(function userGroupUpdated(){ + cacheService.users.removeAll(); + }); + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js b/guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js new file mode 100644 index 000000000..bb82def73 --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service which defines the RelatedObjectPatch class. + */ +angular.module('rest').factory('RelatedObjectPatch', [function defineRelatedObjectPatch() { + + /** + * The object returned by REST API calls when representing changes to an + * arbitrary set of objects which share some common relation. + * + * @constructor + * @param {RelatedObjectPatch|Object} [template={}] + * The object whose properties should be copied within the new + * RelatedObjectPatch. + */ + var RelatedObjectPatch = function RelatedObjectPatch(template) { + + // Use empty object by default + template = template || {}; + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within RelatedObjectPatch.Operation. + * + * @type String + */ + this.op = template.op; + + /** + * The path of the objects to modify. This will always be "/". + * + * @type String + * @default '/' + */ + this.path = template.path || '/'; + + /** + * The identifier of the object being added or removed from the + * relation. + * + * @type String + */ + this.value = template.value; + + }; + + /** + * All valid patch operations for objects sharing some common relation. + * Currently, only add and remove are supported. + */ + RelatedObjectPatch.Operation = { + + /** + * Adds the specified object to the relation. + */ + ADD : "add", + + /** + * Removes the specified object from the relation. + */ + REMOVE : "remove" + + }; + + return RelatedObjectPatch; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/rest/types/UserGroup.js b/guacamole/src/main/webapp/app/rest/types/UserGroup.js new file mode 100644 index 000000000..03b73e29f --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/types/UserGroup.js @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service which defines the UserGroup class. + */ +angular.module('rest').factory('UserGroup', [function defineUserGroup() { + + /** + * The object returned by REST API calls when representing the data + * associated with a user group. + * + * @constructor + * @param {UserGroup|Object} [template={}] + * The object whose properties should be copied within the new + * UserGroup. + */ + var UserGroup = function UserGroup(template) { + + // Use empty object by default + template = template || {}; + + /** + * The name which uniquely identifies this user group. + * + * @type String + */ + this.identifier = template.identifier; + + /** + * Arbitrary name/value pairs which further describe this user group. + * The semantics and validity of these attributes are dictated by the + * extension which defines them. + * + * @type Object. + */ + this.attributes = {}; + + }; + + return UserGroup; + +}]); \ No newline at end of file From 55bcf25a1c53f85ba2383fc83b46e2bc2ccd05af Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Apr 2018 14:39:04 -0700 Subject: [PATCH 02/11] GUACAMOLE-220: Allow manipulation and retrieval of user group permissions via JavaScript. --- .../app/rest/services/permissionService.js | 91 +++++++++++++------ .../app/rest/types/PermissionFlagSet.js | 23 ++++- .../webapp/app/rest/types/PermissionSet.js | 82 ++++++++++++++++- 3 files changed, 163 insertions(+), 33 deletions(-) diff --git a/guacamole/src/main/webapp/app/rest/services/permissionService.js b/guacamole/src/main/webapp/app/rest/services/permissionService.js index 6d3dfdf5e..21c5a02ec 100644 --- a/guacamole/src/main/webapp/app/rest/services/permissionService.js +++ b/guacamole/src/main/webapp/app/rest/services/permissionService.js @@ -45,6 +45,11 @@ angular.module('rest').factory('permissionService', ['$injector', * within that data source (and thus cannot be found beneath * "api/session/data/{dataSource}/users") * + * NOTE: Unlike getPermissionsResourceURL(), + * getEffectivePermissionsResourceURL() CANNOT be applied to user groups. + * Only users have retrievable effective permissions as far as the REST API + * is concerned. + * * @param {String} dataSource * The unique identifier of the data source containing the user whose * permissions should be retrieved. This identifier corresponds to an @@ -82,6 +87,10 @@ angular.module('rest').factory('permissionService', ['$injector', * from the permissions returned via getPermissions() in that permissions * which are not directly granted to the user are included. * + * NOTE: Unlike getPermissions(), getEffectivePermissions() CANNOT be + * applied to user groups. Only users have retrievable effective + * permissions as far as the REST API is concerned. + * * @param {String} dataSource * The unique identifier of the data source containing the user whose * permissions should be retrieved. This identifier corresponds to an @@ -113,10 +122,10 @@ angular.module('rest').factory('permissionService', ['$injector', /** * Returns the URL for the REST resource most appropriate for accessing - * the permissions of the user having the given identifier. The permissions - * retrieved differ from effective permissions (those returned by - * getEffectivePermissions()) in that only permissions which are directly - * granted to the user are included. + * the permissions of the user or group having the given identifier. The + * permissions retrieved differ from effective permissions (those returned + * by getEffectivePermissions()) in that only permissions which are directly + * granted to the user or group are included. * * It is important to note that a particular data source can authenticate * and provide permissions for a user, even if that user does not exist @@ -129,18 +138,27 @@ angular.module('rest').factory('permissionService', ['$injector', * AuthenticationProvider within the Guacamole web application. * * @param {String} identifier - * The identifier of the user for which the URL of the proper REST - * resource should be derived. + * The identifier of the user or group for which the URL of the proper + * REST resource should be derived. + * + * @param {Boolean} [group] + * Whether the provided identifier refers to a user group. If false or + * omitted, the identifier given is assumed to refer to a user. * * @returns {String} - * The URL for the REST resource representing the user having the given - * identifier. + * The URL for the REST resource representing the user or group having + * the given identifier. */ - var getPermissionsResourceURL = function getPermissionsResourceURL(dataSource, identifier) { + var getPermissionsResourceURL = function getPermissionsResourceURL(dataSource, identifier, group) { // Create base URL for data source var base = 'api/session/data/' + encodeURIComponent(dataSource); + // Access group permissions directly (there is no "self" for user groups + // as there is for users) + if (group) + return base + '/userGroups/' + encodeURIComponent(identifier) + '/permissions'; + // If the username is that of the current user, do not rely on the // user actually existing (they may not). Access their permissions via // "self" rather than the collection of defined users. @@ -155,36 +173,41 @@ angular.module('rest').factory('permissionService', ['$injector', /** * Makes a request to the REST API to get the list of permissions for a - * given user, returning a promise that provides an array of + * given user or user group, returning a promise that provides an array of * @link{Permission} objects if successful. The permissions retrieved * differ from effective permissions (those returned by - * getEffectivePermissions()) in that only permissions which are directly - * granted to the user included. + * getEffectivePermissions()) in that both users and groups may be queried, + * and only permissions which are directly granted to the user or group are + * included. * * @param {String} dataSource - * The unique identifier of the data source containing the user whose - * permissions should be retrieved. This identifier corresponds to an - * AuthenticationProvider within the Guacamole web application. + * The unique identifier of the data source containing the user or group + * whose permissions should be retrieved. This identifier corresponds to + * an AuthenticationProvider within the Guacamole web application. * * @param {String} identifier - * The identifier of the user to retrieve the permissions for. + * The identifier of the user or group to retrieve the permissions for. + * + * @param {Boolean} [group] + * Whether the provided identifier refers to a user group. If false or + * omitted, the identifier given is assumed to refer to a user. * * @returns {Promise.} * A promise which will resolve with a @link{PermissionSet} upon * success. */ - service.getPermissions = function getPermissions(dataSource, identifier) { + service.getPermissions = function getPermissions(dataSource, identifier, group) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; - // Retrieve user permissions + // Retrieve user/group permissions return requestService({ cache : cacheService.users, method : 'GET', - url : getPermissionsResourceURL(dataSource, identifier), + url : getPermissionsResourceURL(dataSource, identifier, group), params : httpParameters }); @@ -261,6 +284,10 @@ angular.module('rest').factory('permissionService', ['$injector', addObjectPatchOperations(patch, operation, "/userPermissions", permissions.userPermissions); + // Add user group permission operations to patch + addObjectPatchOperations(patch, operation, "/userGroupPermissions", + permissions.userGroupPermissions); + // Add system operations to patch permissions.systemPermissions.forEach(function addSystemPatch(type) { patch.push({ @@ -274,18 +301,18 @@ angular.module('rest').factory('permissionService', ['$injector', /** * Makes a request to the REST API to modify the permissions for a given - * user, returning a promise that can be used for processing the results of - * the call. This request affects only the permissions directly granted to - * the user, and may not affect permissions inherited through other means - * (effective permissions). + * user or group, returning a promise that can be used for processing the + * results of the call. This request affects only the permissions directly + * granted to the user or group, and may not affect permissions inherited + * through other means (effective permissions). * * @param {String} dataSource - * The unique identifier of the data source containing the user whose - * permissions should be modified. This identifier corresponds to an - * AuthenticationProvider within the Guacamole web application. + * The unique identifier of the data source containing the user or group + * whose permissions should be modified. This identifier corresponds to + * an AuthenticationProvider within the Guacamole web application. * * @param {String} identifier - * The identifier of the user to modify the permissions of. + * The identifier of the user or group to modify the permissions of. * * @param {PermissionSet} [permissionsToAdd] * The set of permissions to add, if any. @@ -293,12 +320,16 @@ angular.module('rest').factory('permissionService', ['$injector', * @param {PermissionSet} [permissionsToRemove] * The set of permissions to remove, if any. * + * @param {Boolean} [group] + * Whether the provided identifier refers to a user group. If false or + * omitted, the identifier given is assumed to refer to a user. + * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * patch operation is successful. */ service.patchPermissions = function patchPermissions(dataSource, identifier, - permissionsToAdd, permissionsToRemove) { + permissionsToAdd, permissionsToRemove, group) { var permissionPatch = []; @@ -313,10 +344,10 @@ angular.module('rest').factory('permissionService', ['$injector', // Add all the remove operations to the patch addPatchOperations(permissionPatch, PermissionPatch.Operation.REMOVE, permissionsToRemove); - // Patch user permissions + // Patch user/group permissions return requestService({ method : 'PATCH', - url : getPermissionsResourceURL(dataSource, identifier), + url : getPermissionsResourceURL(dataSource, identifier, group), params : httpParameters, data : permissionPatch }) diff --git a/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js b/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js index 64b942bea..f79e3b964 100644 --- a/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js +++ b/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js @@ -133,7 +133,7 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet', * 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.userPermissions = template.userPermissions || { @@ -143,6 +143,24 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet', 'ADMINISTER' : {} }; + /** + * The granted state of each permission for each user group, as a map of + * object permission type string to permission map. The permission map + * is, in turn, a map of group 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.userGroupPermissions = template.userGroupPermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER' : {} + }; + }; /** @@ -216,6 +234,9 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet', // Add all granted user permissions addObjectPermissions(permissionSet.userPermissions, permissionFlagSet.userPermissions); + // Add all granted user group permissions + addObjectPermissions(permissionSet.userGroupPermissions, permissionFlagSet.userGroupPermissions); + return permissionFlagSet; }; diff --git a/guacamole/src/main/webapp/app/rest/types/PermissionSet.js b/guacamole/src/main/webapp/app/rest/types/PermissionSet.js index 8fd1ef684..2bc2e9eb4 100644 --- a/guacamole/src/main/webapp/app/rest/types/PermissionSet.js +++ b/guacamole/src/main/webapp/app/rest/types/PermissionSet.js @@ -81,6 +81,15 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() */ this.userPermissions = template.userPermissions || {}; + /** + * Map of user group identifiers to the corresponding array of granted + * permissions. Each permission is represented by a string listed + * within PermissionSet.ObjectPermissionType. + * + * @type Object. + */ + this.userGroupPermissions = template.userGroupPermissions || {}; + /** * Array of granted system permissions. Each permission is represented * by a string listed within PermissionSet.SystemPermissionType. @@ -306,7 +315,7 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() }; /** - * Returns whether the given permission is granted for the user having the + * Returns whether the given permission is granted for the user having the * given ID. * * @param {PermissionSet|Object} permSet @@ -315,7 +324,7 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() * @param {String} type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. - * + * * @param {String} identifier * The identifier of the user to which the permission applies. * @@ -326,6 +335,27 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() return hasPermission(permSet.userPermissions, type, identifier); }; + /** + * Returns whether the given permission is granted for the user group having + * the given identifier. + * + * @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 user group to which the permission applies. + * + * @returns {Boolean} + * true if the permission is present (granted), false otherwise. + */ + PermissionSet.hasUserGroupPermission = function hasUserGroupPermission(permSet, type, identifier) { + return hasPermission(permSet.userGroupPermissions, type, identifier); + }; + /** * Returns whether the given permission is granted at the system level. * @@ -733,6 +763,54 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() return removeObjectPermission(permSet.userPermissions, type, identifier); }; + /** + * Adds the given user group permission applying to the user group with the + * given identifier 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 user group 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.addUserGroupPermission = function addUserGroupPermission(permSet, type, identifier) { + permSet.userGroupPermissions = permSet.userGroupPermissions || {}; + return addObjectPermission(permSet.userGroupPermissions, type, identifier); + }; + + /** + * Removes the given user group permission applying to the user group with + * the given identifier 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 user group to whom the permission applies. + * + * @returns {Boolean} + * true if the permission was removed, false if the permission was not + * present in the given permission set. + */ + PermissionSet.removeUserGroupPermission = function removeUserGroupPermission(permSet, type, identifier) { + permSet.userGroupPermissions = permSet.userGroupPermissions || {}; + return removeObjectPermission(permSet.userGroupPermissions, type, identifier); + }; + return PermissionSet; }]); \ No newline at end of file From 615f5c6bab28f78884cf42fb29c76ad44822a33d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 19 Apr 2018 23:21:17 -0700 Subject: [PATCH 03/11] GUACAMOLE-220: Add missing getUserGroupAttributes() to JavaScript schemaService. --- .../webapp/app/rest/services/schemaService.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/guacamole/src/main/webapp/app/rest/services/schemaService.js b/guacamole/src/main/webapp/app/rest/services/schemaService.js index cc871d842..61c8639de 100644 --- a/guacamole/src/main/webapp/app/rest/services/schemaService.js +++ b/guacamole/src/main/webapp/app/rest/services/schemaService.js @@ -64,6 +64,40 @@ angular.module('rest').factory('schemaService', ['$injector', }; + /** + * Makes a request to the REST API to get the list of available attributes + * for user group objects, returning a promise that provides an array of + * @link{Form} objects if successful. Each element of the array describes + * a logical grouping of possible attributes. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user groups + * whose available attributes are to be retrieved. This identifier + * corresponds to an AuthenticationProvider within the Guacamole web + * application. + * + * @returns {Promise.} + * A promise which will resolve with an array of @link{Form} + * objects, where each @link{Form} describes a logical grouping of + * possible attributes. + */ + service.getUserGroupAttributes = function getUserGroupAttributes(dataSource) { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve available user group attributes + return requestService({ + cache : cacheService.schema, + method : 'GET', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userGroupAttributes', + params : httpParameters + }); + + }; + /** * Makes a request to the REST API to get the list of available attributes * for connection objects, returning a promise that provides an array of From 1cf16d1dc6f02e608eb860d627c423dc6be958da Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 21 Jul 2018 19:41:47 -0700 Subject: [PATCH 04/11] GUACAMOLE-220: Upgrade to latest versions of jQuery and Lodash. The version of Lodash previously included with Guacamole lacks the sortedIndexOf() function, which is needed by the identity set editor directive for manipulating sorted arrays of identifiers. If upgrading Lodash, may as well upgrade jQuery while we're at it. The version previously included within Guacamole is no longer maintained. --- guacamole/pom.xml | 4 +- guacamole/src/licenses/LICENSE | 60 +++++++++++++++---- .../LICENSE.txt} | 19 +++++- .../LICENSE.txt => lodash-4.17.10/LICENSE} | 31 +++++++++- guacamole/src/main/webapp/index.html | 4 +- 5 files changed, 99 insertions(+), 19 deletions(-) rename guacamole/src/licenses/bundled/{jquery-2.1.3/MIT-LICENSE.txt => jquery-3.3.1/LICENSE.txt} (63%) rename guacamole/src/licenses/bundled/{lodash-2.4.1/LICENSE.txt => lodash-4.17.10/LICENSE} (53%) diff --git a/guacamole/pom.xml b/guacamole/pom.xml index dc4f0824b..0899e78d0 100644 --- a/guacamole/pom.xml +++ b/guacamole/pom.xml @@ -308,7 +308,7 @@ org.webjars.bower lodash - 2.4.1 + 4.17.10 runtime @@ -316,7 +316,7 @@ org.webjars.bower jquery - 2.1.3 + 3.3.1 runtime diff --git a/guacamole/src/licenses/LICENSE b/guacamole/src/licenses/LICENSE index 447470769..51f5b2171 100644 --- a/guacamole/src/licenses/LICENSE +++ b/guacamole/src/licenses/LICENSE @@ -486,13 +486,21 @@ Jettison (https://github.com/jettison-json/jettison) jQuery (http://jquery.com/) --------------------------- - Version: 2.1.3 - From: 'jQuery Foundation' (http://jquery.com/) + Version: 3.3.1 + From: 'JS Foundation' (https://js.foundation/) License(s): - MIT (bundled/jquery-2.1.3/MIT-LICENSE.txt) + MIT (bundled/jquery-3.3.1/LICENSE.txt) -Copyright 2014 jQuery Foundation and other contributors -http://jquery.com/ +Copyright JS Foundation and other contributors, https://js.foundation/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery + +The following license applies to all parts of this software except as +documented below: + +==== Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -513,6 +521,13 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + JSR-250 Reference Implementation (https://jcp.org/aboutJava/communityprocess/final/jsr250/index.html) @@ -536,15 +551,25 @@ JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/) Lodash (https://lodash.com/) ---------------------------- - Version: 2.4.1 - From: 'The Dojo Foundation' (http://dojofoundation.org/) + Version: 4.17.10 + From: 'JS Foundation' (https://js.foundation/) License(s): - MIT (bundled/lodash-2.4.1/LICENSE.txt) + MIT (bundled/lodash-4.17.10/LICENSE) -Copyright 2012-2013 The Dojo Foundation -Based on Underscore.js 1.5.2, copyright 2009-2013 Jeremy Ashkenas, +Copyright JS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -564,6 +589,21 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + Logback (http://logback.qos.ch/) -------------------------------- diff --git a/guacamole/src/licenses/bundled/jquery-2.1.3/MIT-LICENSE.txt b/guacamole/src/licenses/bundled/jquery-3.3.1/LICENSE.txt similarity index 63% rename from guacamole/src/licenses/bundled/jquery-2.1.3/MIT-LICENSE.txt rename to guacamole/src/licenses/bundled/jquery-3.3.1/LICENSE.txt index cdd31b5c7..e4e5e00ef 100644 --- a/guacamole/src/licenses/bundled/jquery-2.1.3/MIT-LICENSE.txt +++ b/guacamole/src/licenses/bundled/jquery-3.3.1/LICENSE.txt @@ -1,5 +1,13 @@ -Copyright 2014 jQuery Foundation and other contributors -http://jquery.com/ +Copyright JS Foundation and other contributors, https://js.foundation/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery + +The following license applies to all parts of this software except as +documented below: + +==== Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -19,3 +27,10 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. diff --git a/guacamole/src/licenses/bundled/lodash-2.4.1/LICENSE.txt b/guacamole/src/licenses/bundled/lodash-4.17.10/LICENSE similarity index 53% rename from guacamole/src/licenses/bundled/lodash-2.4.1/LICENSE.txt rename to guacamole/src/licenses/bundled/lodash-4.17.10/LICENSE index 49869bbab..c6f2f6145 100644 --- a/guacamole/src/licenses/bundled/lodash-2.4.1/LICENSE.txt +++ b/guacamole/src/licenses/bundled/lodash-4.17.10/LICENSE @@ -1,7 +1,17 @@ -Copyright 2012-2013 The Dojo Foundation -Based on Underscore.js 1.5.2, copyright 2009-2013 Jeremy Ashkenas, +Copyright JS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -19,4 +29,19 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html index 5a53d8a23..a4a93daf4 100644 --- a/guacamole/src/main/webapp/index.html +++ b/guacamole/src/main/webapp/index.html @@ -57,8 +57,8 @@ - - + + From 229b0dee4882352e7583c4f5872bee92158da712 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 25 Jul 2018 02:34:27 -0700 Subject: [PATCH 05/11] GUACAMOLE-220: Implement generic editor directive for manipulating sets of identifiers. --- .../manage/directives/identifierSetEditor.js | 267 ++++++++++++++++++ .../app/manage/styles/related-objects.css | 82 ++++++ .../manage/templates/identifierSetEditor.html | 46 +++ .../src/main/webapp/images/arrows/right.png | Bin 0 -> 264 bytes guacamole/src/main/webapp/images/x-red.png | Bin 0 -> 583 bytes 5 files changed, 395 insertions(+) create mode 100644 guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js create mode 100644 guacamole/src/main/webapp/app/manage/styles/related-objects.css create mode 100644 guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html create mode 100644 guacamole/src/main/webapp/images/arrows/right.png create mode 100644 guacamole/src/main/webapp/images/x-red.png diff --git a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js new file mode 100644 index 000000000..82f110915 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A directive for manipulating a set of objects sharing some common relation + * and represented by an array of their identifiers. The specific objects + * added or removed are tracked within a separate pair of arrays of + * identifiers. + */ +angular.module('manage').directive('identifierSetEditor', ['$injector', + function identifierSetEditor($injector) { + + var directive = { + + // Element only + restrict: 'E', + replace: true, + + scope: { + + /** + * The translation key of the text which should be displayed within + * the main header of the identifier set editor. + * + * @type String + */ + header : '@', + + /** + * The translation key of the text which should be displayed if no + * identifiers are currently present within the set. + * + * @type String + */ + emptyPlaceholder : '@', + + /** + * The translation key of the text which should be displayed if no + * identifiers are available to be added within the set. + * + * @type String + */ + unavailablePlaceholder : '@', + + /** + * All identifiers which are available to be added to or removed + * from the identifier set being edited. + * + * @type String[] + */ + identifiersAvailable : '=', + + /** + * The current state of the identifier set being manipulated. This + * array will be modified as changes are made through this + * identifier set editor. + * + * @type String[] + */ + identifiers : '=', + + /** + * The set of identifiers that have been added, relative to the + * initial state of the identifier set being manipulated. + * + * @type String[] + */ + identifiersAdded : '=', + + /** + * The set of identifiers that have been removed, relative to the + * initial state of the identifier set being manipulated. + * + * @type String[] + */ + identifiersRemoved : '=' + + }, + + templateUrl: 'app/manage/templates/identifierSetEditor.html' + + }; + + directive.controller = ['$scope', function identifierSetEditorController($scope) { + + /** + * Whether the full list of available identifiers should be displayed. + * Initially, only an abbreviated list of identifiers currently present + * is shown. + * + * @type Boolean + */ + $scope.expanded = false; + + /** + * Map of identifiers to boolean flags indicating whether that + * identifier is currently present (true) or absent (false). If an + * identifier is absent, it may also be absent from this map. + * + * @type Object. + */ + $scope.identifierFlags = {}; + + /** + * Adds the given identifier to the given sorted array of identifiers, + * preserving the sorted order of the array. If the identifier is + * already present, no change is made to the array. The given array + * must already be sorted in ascending order. + * + * @param {String[]} arr + * The sorted array of identifiers to add the given identifier to. + * + * @param {String} identifier + * The identifier to add to the given array. + */ + var addIdentifier = function addIdentifier(arr, identifier) { + + // Determine location that the identifier should be added to + // maintain sorted order + var index = _.sortedIndex(arr, identifier); + + // Do not add if already present + if (arr[index] === identifier) + return; + + // Insert identifier at determined location + arr.splice(index, 0, identifier); + + }; + + /** + * Removes the given identifier from the given sorted array of + * identifiers, preserving the sorted order of the array. If the + * identifier is already absent, no change is made to the array. The + * given array must already be sorted in ascending order. + * + * @param {String[]} arr + * The sorted array of identifiers to remove the given identifier + * from. + * + * @param {String} identifier + * The identifier to remove from the given array. + * + * @returns {Boolean} + * true if the identifier was present in the given array and has + * been removed, false otherwise. + */ + var removeIdentifier = function removeIdentifier(arr, identifier) { + + // Search for identifier in sorted array + var index = _.sortedIndexOf(arr, identifier); + + // Nothing to do if already absent + if (index === -1) + return false; + + // Remove identifier + arr.splice(index, 1); + return true; + + }; + + // Keep identifierFlags up to date when identifiers array is replaced + // or initially assigned + $scope.$watch('identifiers', function identifiersChanged(identifiers) { + + // Maintain identifiers in sorted order so additions and removals + // can be made more efficiently + if (identifiers) + identifiers.sort(); + + // Convert array of identifiers into set of boolean + // presence/absence flags + $scope.identifierFlags = {}; + angular.forEach(identifiers, function storeIdentifierFlag(identifier) { + $scope.identifierFlags[identifier] = true; + }); + + }); + + /** + * Notifies the controller that a change has been made to the flag + * denoting presence/absence of a particular identifier within the + * identifierFlags map. The identifiers, + * identifiersAdded, and identifiersRemoved + * arrays are updated accordingly. + * + * @param {String} identifier + * The identifier which has been added or removed through modifying + * its boolean flag within identifierFlags. + */ + $scope.identifierChanged = function identifierChanged(identifier) { + + // Determine status of modified identifier + var present = !!$scope.identifierFlags[identifier]; + + // Add/remove identifier from added/removed sets depending on + // change in flag state + if (present) { + + addIdentifier($scope.identifiers, identifier); + + if (!removeIdentifier($scope.identifiersRemoved, identifier)) + addIdentifier($scope.identifiersAdded, identifier); + + } + else { + + removeIdentifier($scope.identifiers, identifier); + + if (!removeIdentifier($scope.identifiersAdded, identifier)) + addIdentifier($scope.identifiersRemoved, identifier); + + } + + }; + + /** + * Removes the given identifier, updating identifierFlags, + * identifiers, identifiersAdded, and + * identifiersRemoved accordingly. + * + * @param {String} identifier + * The identifier to remove. + */ + $scope.removeIdentifier = function removeIdentifier(identifier) { + $scope.identifierFlags[identifier] = false; + $scope.identifierChanged(identifier); + }; + + /** + * Shows the full list of available identifiers. If the full list is + * already shown, this function has no effect. + */ + $scope.expand = function expand() { + $scope.expanded = true; + }; + + /** + * Hides the full list of available identifiers. If the full list is + * already hidden, this function has no effect. + */ + $scope.collapse = function collapse() { + $scope.expanded = false; + }; + + }]; + + return directive; + +}]); diff --git a/guacamole/src/main/webapp/app/manage/styles/related-objects.css b/guacamole/src/main/webapp/app/manage/styles/related-objects.css new file mode 100644 index 000000000..ddc85b1de --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/styles/related-objects.css @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.related-objects .abbreviated-related-objects { + display: table; + margin: 1em 0; +} + +.related-objects .abbreviated-related-objects ul { + display: table-cell; + vertical-align: top; +} + +.related-objects .abbreviated-related-objects ul, +.related-objects .all-related-objects ul { + padding: 0; + list-style: none; +} + +.related-objects .abbreviated-related-objects ul li { + + display: inline-block; + margin: 0.25em; + padding: 0.25em; + + border: 1px solid silver; + background: #F5F5F5; + -moz-border-radius: 0.25em; + -webkit-border-radius: 0.25em; + -khtml-border-radius: 0.25em; + border-radius: 0.25em; + +} + +.related-objects .abbreviated-related-objects ul li img.remove { + max-height: 0.75em; + max-width: 0.75em; + margin: 0 0.25em; +} + +.related-objects .abbreviated-related-objects ul li .identifier { + margin: 0 0.25em; +} + +.related-objects .abbreviated-related-objects img.expand, +.related-objects .abbreviated-related-objects img.collapse { + display: table-cell; + max-height: 1.5em; + max-width: 1.5em; + margin: 0.375em 0; +} + +.related-objects .all-related-objects { + border-top: 1px solid silver; +} + +.related-objects .abbreviated-related-objects p.no-related-objects, +.related-objects .all-related-objects p.no-objects-available { + font-style: italic; + opacity: 0.5; +} + +.related-objects .abbreviated-related-objects p.no-related-objects { + display: table-cell; + vertical-align: middle; +} diff --git a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html new file mode 100644 index 000000000..838decf03 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html @@ -0,0 +1,46 @@ + \ No newline at end of file diff --git a/guacamole/src/main/webapp/images/arrows/right.png b/guacamole/src/main/webapp/images/arrows/right.png new file mode 100644 index 0000000000000000000000000000000000000000..1b3483edfa3b383bd6184096b2e4a4bdf1da743a GIT binary patch literal 264 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Y)RhkE)4%caKYZ?lYt_f1s;*b zKpodXn9)gNb_Gz7y~NYkmHiRBB!?pFul|%DK%uFgE{-7;x8B}xgTe~DWM4fT$5+D literal 0 HcmV?d00001 diff --git a/guacamole/src/main/webapp/images/x-red.png b/guacamole/src/main/webapp/images/x-red.png new file mode 100644 index 0000000000000000000000000000000000000000..e5497f34aeed6295428b4220c9b55747b222a610 GIT binary patch literal 583 zcmV-N0=WH&P)f0Mv|$=mo&Xj``bCz(lkMaKB@IRs--u^hUq~fL{Q2RuE?K zp8&2Ic)wwO><(Zd{y0n^EX5zctAS7iAR*oaLP~rsglqsC;#DDRiLZf>1t3Gb3WOZ- zr4UX6C=lNjLW%ei2%7*Z#CL&EBR&_xDu4#@q7Yid=RimTn1h@rfgi64f`s_RcufUM zYXBrCu7aSohSe=#2|;u2%|p-vg7%yzuR+ZPSpd%pgftdrwdJxy;hqb$Ja3AUp&kpJ zKbze+#p$pl1O^TOK){vNbz5bK2>%mcCBD@XI~NE|(Pon=dIk`h^NnU#*bst9vL#}R z8$b|Cx5erLiV#G-2GPQZ0t9ilNxU-D5keQMtxIWaBHA Date: Tue, 7 Aug 2018 12:25:55 -0700 Subject: [PATCH 06/11] GUACAMOLE-220: Hide identifier set editor if there are no identifiers to edit. --- .../app/manage/directives/identifierSetEditor.js | 14 ++++++++++++++ .../app/manage/templates/identifierSetEditor.html | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js index 82f110915..4240901b9 100644 --- a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js +++ b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js @@ -260,6 +260,20 @@ angular.module('manage').directive('identifierSetEditor', ['$injector', $scope.expanded = false; }; + /** + * Returns whether there are absolutely no identifiers that can be + * managed using this editor. If true, the editor is effectively + * useless, as there is nothing whatsoever to display. + * + * @returns {Boolean} + * true if there are no identifiers that can be managed using this + * editor, false otherwise. + */ + $scope.isEmpty = function isEmpty() { + return _.isEmpty($scope.identifiers) + && _.isEmpty($scope.identifiersAvailable); + }; + }]; return directive; diff --git a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html index 838decf03..7f660883d 100644 --- a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html +++ b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html @@ -1,4 +1,4 @@ -