From 94ad1f9f349826ede932ea843ed87f58cc76d598 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 28 Apr 2018 23:05:27 -0700 Subject: [PATCH 01/20] GUACAMOLE-220: Separate system/connection permission editing into directives. --- .../controllers/manageUserController.js | 580 ++---------------- .../directives/connectionPermissionEditor.js | 406 ++++++++++++ .../directives/systemPermissionEditor.js | 308 ++++++++++ .../templates/connectionPermissionEditor.html | 21 + .../app/manage/templates/manageUser.html | 52 +- .../templates/systemPermissionEditor.html | 18 + 6 files changed, 809 insertions(+), 576 deletions(-) create mode 100644 guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js create mode 100644 guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js create mode 100644 guacamole/src/main/webapp/app/manage/templates/connectionPermissionEditor.html create mode 100644 guacamole/src/main/webapp/app/manage/templates/systemPermissionEditor.html diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index eae141b26..c3850650c 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -24,8 +24,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto function manageUserController($scope, $injector) { // Required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var GroupListItem = $injector.get('GroupListItem'); var PageDefinition = $injector.get('PageDefinition'); var PermissionFlagSet = $injector.get('PermissionFlagSet'); var PermissionSet = $injector.get('PermissionSet'); @@ -35,7 +33,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); @@ -71,14 +68,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ var currentUsername = authenticationService.getCurrentUsername(); - /** - * The unique identifier of the data source containing the user being - * edited. - * - * @type String - */ - var selectedDataSource = $routeParams.dataSource; - /** * The username of the original user from which this user is * being cloned. Only valid if this is a new user. @@ -95,6 +84,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ var username = $routeParams.id; + /** + * The unique identifier of the data source containing the user being + * edited. + * + * @type String + */ + $scope.dataSource = $routeParams.dataSource; + /** * The string value representing the user currently being edited within the * permission flag set. Note that his may not match the user's actual @@ -130,34 +127,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ $scope.permissionFlags = null; - /** - * A map of data source identifiers to the root connection groups within - * thost data sources. As only one data source is applicable to any one - * user being edited/created, this will only contain a single key. - * - * @type Object. - */ - $scope.rootGroups = null; - - /** - * Array of all connection properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionProperties = [ - 'name', - 'protocol' - ]; - - /** - * Array of all connection group properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionGroupProperties = [ - 'name' - ]; - /** * A map of data source identifiers to the set of all permissions * associated with the current user under that data source, or null if the @@ -219,7 +188,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return false; // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; + dataSource = dataSource || $scope.dataSource; // Account exists only if it was successfully retrieved return (dataSource in $scope.users); @@ -245,7 +214,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return false; // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; + dataSource = dataSource || $scope.dataSource; // Attributes can always be set if we are creating the user if (!$scope.userExists(dataSource)) @@ -275,7 +244,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.canChangeAllAttributes = function canChangeAllAttributes() { // All attributes can be set if we are creating the user - return !$scope.userExists(selectedDataSource); + return !$scope.userExists($scope.dataSource); }; @@ -299,7 +268,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return false; // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; + dataSource = dataSource || $scope.dataSource; // Permissions can always be set if we are creating the user if (!$scope.userExists(dataSource)) @@ -317,33 +286,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; - /** - * Returns whether the current user can change the system permissions - * granted to the user being edited within the given data source. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the current user can grant or revoke system permissions to - * the user being edited, false otherwise. - */ - $scope.canChangeSystemPermissions = function canChangeSystemPermissions(dataSource) { - - // Do not check if permissions are not yet loaded - if (!$scope.permissions) - return false; - - // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; - - // Only the administrator can modify system permissions - return PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.ADMINISTER); - - }; - /** * Returns whether the current user can edit the username of the user being * edited within the given data source. @@ -380,7 +322,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return false; // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; + dataSource = dataSource || $scope.dataSource; // The administrator can always save users if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], @@ -417,10 +359,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return false; // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; + dataSource = dataSource || $scope.dataSource; // If we are not editing an existing user, we cannot clone - if (!$scope.userExists(selectedDataSource)) + if (!$scope.userExists($scope.dataSource)) return false; // The administrator can always clone users @@ -453,7 +395,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return false; // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; + dataSource = dataSource || $scope.dataSource; // Can't delete what doesn't exist if (!$scope.userExists(dataSource)) @@ -485,7 +427,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.isReadOnly = function isReadOnly(dataSource) { // Use currently-selected data source if unspecified - dataSource = dataSource || selectedDataSource; + dataSource = dataSource || $scope.dataSource; // User is read-only if they cannot be saved return !$scope.canSaveUser(dataSource); @@ -509,7 +451,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return; // Only the selected data source is relevant when cloning - if (cloneSourceUsername && dataSource !== selectedDataSource) + if (cloneSourceUsername && dataSource !== $scope.dataSource) return; // Determine class name based on read-only / linked status @@ -530,7 +472,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }); // Pull user attribute schema - schemaService.getUserAttributes(selectedDataSource).then(function attributesReceived(attributes) { + schemaService.getUserAttributes($scope.dataSource).then(function attributesReceived(attributes) { $scope.attributes = attributes; }, requestService.WARN); @@ -543,7 +485,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // Get user for currently-selected data source $scope.users = users; - $scope.user = users[selectedDataSource]; + $scope.user = users[$scope.dataSource]; // Create skeleton user if user does not exist if (!$scope.user) @@ -558,7 +500,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.selfUsername = username; // Pull user permissions - permissionService.getPermissions(selectedDataSource, username).then(function gotPermissions(permissions) { + permissionService.getPermissions($scope.dataSource, username).then(function gotPermissions(permissions) { $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); }) @@ -576,7 +518,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // Get user for currently-selected data source $scope.users = {}; - $scope.user = users[selectedDataSource]; + $scope.user = users[$scope.dataSource]; }, requestService.WARN); @@ -585,10 +527,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.selfUsername = cloneSourceUsername; // Pull user permissions - permissionService.getPermissions(selectedDataSource, cloneSourceUsername) + permissionService.getPermissions($scope.dataSource, cloneSourceUsername) .then(function gotPermissions(permissions) { $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); - permissionsAdded = permissions; + $scope.permissionsAdded = permissions; }) // If permissions cannot be retrieved, use empty permissions @@ -607,78 +549,13 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.user = new User(); $scope.permissionFlags = new PermissionFlagSet(); + // As no permissions are yet associated with the user, it is safe to + // use any non-empty username as a placeholder for self-referential + // permissions + $scope.selfUsername = 'SELF'; + } - /** - * Expands all items within the tree descending from the given - * GroupListItem which have at least one descendant for which explicit READ - * permission is granted. The expanded state of all other items is left - * untouched. - * - * @param {GroupListItem} item - * The GroupListItem which should be conditionally expanded depending - * on whether READ permission is granted for any of its descendants. - * - * @param {PemissionFlagSet} flags - * The set of permissions which should be used to determine whether the - * given item and its descendants are expanded. - */ - var expandReadable = function expandReadable(item, flags) { - - // If the current item is expandable and has defined children, - // determine whether it should be expanded - if (item.expandable && item.children) { - angular.forEach(item.children, function expandReadableChild(child) { - - // Determine whether the user has READ permission for the - // current child object - var readable = false; - switch (child.type) { - - case GroupListItem.Type.CONNECTION: - readable = flags.connectionPermissions.READ[child.identifier]; - break; - - case GroupListItem.Type.CONNECTION_GROUP: - readable = flags.connectionGroupPermissions.READ[child.identifier]; - break; - - case GroupListItem.Type.SHARING_PROFILE: - readable = flags.sharingProfilePermissions.READ[child.identifier]; - break; - - } - - // The parent should be expanded by default if the child is - // expanded by default OR the user has READ permission on the - // child - item.expanded |= expandReadable(child, flags) || readable; - - }); - } - - return item.expanded; - - }; - - - // Retrieve all connections for which we have ADMINISTER permission - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - [selectedDataSource], - ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.ADMINISTER] - ) - .then(function connectionGroupReceived(rootGroups) { - - // Convert all received ConnectionGroup objects into GroupListItems - $scope.rootGroups = {}; - angular.forEach(rootGroups, function addGroupListItem(rootGroup, dataSource) { - $scope.rootGroups[dataSource] = GroupListItem.fromConnectionGroup(dataSource, rootGroup); - }); - - }, requestService.WARN); - // Query the user's permissions for the current user dataSourceService.apply( permissionService.getEffectivePermissions, @@ -689,48 +566,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissions = permissions; }, requestService.WARN); - // Update default expanded state whenever connection groups and associated - // permissions change - $scope.$watchGroup(['rootGroups', 'permissionFlags'], function updateDefaultExpandedStates() { - angular.forEach($scope.rootGroups, function updateExpandedStates(rootGroup) { - - // Automatically expand all objects with any descendants for which - // the user has READ permission - if ($scope.permissionFlags) - expandReadable(rootGroup, $scope.permissionFlags); - - }); - }); - - /** - * Available system permission types, as translation string / internal - * value pairs. - * - * @type Object[] - */ - $scope.systemPermissionTypes = [ - { - label: "MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - value: PermissionSet.SystemPermissionType.ADMINISTER - }, - { - label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - value: PermissionSet.SystemPermissionType.CREATE_USER - }, - { - label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - value: PermissionSet.SystemPermissionType.CREATE_CONNECTION - }, - { - 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 - } - ]; - /** * The set of permissions that will be added to the user when the user is * saved. Permissions will only be present in this set if they are @@ -738,7 +573,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto * * @type PermissionSet */ - var permissionsAdded = new PermissionSet(); + $scope.permissionsAdded = new PermissionSet(); /** * The set of permissions that will be removed from the user when the user @@ -747,336 +582,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto * * @type PermissionSet */ - var permissionsRemoved = new PermissionSet(); - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the addition of the given system permission. - * - * @param {String} type - * The system permission to add, as defined by - * PermissionSet.SystemPermissionType. - */ - var addSystemPermission = function addSystemPermission(type) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasSystemPermission(permissionsRemoved, type)) - PermissionSet.removeSystemPermission(permissionsRemoved, type); - - // Otherwise, explicitly add the permission - else - PermissionSet.addSystemPermission(permissionsAdded, type); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the removal of the given system permission. - * - * @param {String} type - * The system permission to remove, as defined by - * PermissionSet.SystemPermissionType. - */ - var removeSystemPermission = function removeSystemPermission(type) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasSystemPermission(permissionsAdded, type)) - PermissionSet.removeSystemPermission(permissionsAdded, type); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addSystemPermission(permissionsRemoved, type); - - }; - - /** - * Notifies the controller that a change has been made to the given - * system permission for the user being edited. - * - * @param {String} type - * The system permission that was changed, as defined by - * PermissionSet.SystemPermissionType. - */ - $scope.systemPermissionChanged = function systemPermissionChanged(type) { - - // Determine current permission setting - var granted = $scope.permissionFlags.systemPermissions[type]; - - // Add/remove permission depending on flag state - if (granted) - addSystemPermission(type); - else - removeSystemPermission(type); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the addition of the given user permission. - * - * @param {String} type - * The user permission to add, as defined by - * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier - * The identifier of the user affected by the permission being added. - */ - var addUserPermission = function addUserPermission(type, identifier) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasUserPermission(permissionsRemoved, type, identifier)) - PermissionSet.removeUserPermission(permissionsRemoved, type, identifier); - - // Otherwise, explicitly add the permission - else - PermissionSet.addUserPermission(permissionsAdded, type, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the removal of the given user permission. - * - * @param {String} type - * The user permission to remove, as defined by - * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier - * The identifier of the user affected by the permission being removed. - */ - var removeUserPermission = function removeUserPermission(type, identifier) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasUserPermission(permissionsAdded, type, identifier)) - PermissionSet.removeUserPermission(permissionsAdded, type, identifier); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addUserPermission(permissionsRemoved, type, identifier); - - }; - - /** - * Notifies the controller that a change has been made to the given user - * permission for the user being edited. - * - * @param {String} type - * The user permission that was changed, as defined by - * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier - * The identifier of the user affected by the changed permission. - */ - $scope.userPermissionChanged = function userPermissionChanged(type, identifier) { - - // Determine current permission setting - var granted = $scope.permissionFlags.userPermissions[type][identifier]; - - // Add/remove permission depending on flag state - if (granted) - addUserPermission(type, identifier); - else - removeUserPermission(type, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the addition of the given connection permission. - * - * @param {String} identifier - * The identifier of the connection to add READ permission for. - */ - var addConnectionPermission = function addConnectionPermission(identifier) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasConnectionPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly add the permission - else - PermissionSet.addConnectionPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the removal of the given connection permission. - * - * @param {String} identifier - * The identifier of the connection to remove READ permission for. - */ - var removeConnectionPermission = function removeConnectionPermission(identifier) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasConnectionPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addConnectionPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the addition of the given connection group permission. - * - * @param {String} identifier - * The identifier of the connection group to add READ permission for. - */ - var addConnectionGroupPermission = function addConnectionGroupPermission(identifier) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasConnectionGroupPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionGroupPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly add the permission - else - PermissionSet.addConnectionGroupPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets to - * reflect the removal of the given connection permission. - * - * @param {String} identifier - * The identifier of the connection to remove READ permission for. - */ - var removeConnectionGroupPermission = function removeConnectionGroupPermission(identifier) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasConnectionGroupPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionGroupPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addConnectionGroupPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * 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 = { - - /** - * Returns the PermissionFlagSet that contains the current state of - * granted permissions. - * - * @returns {PermissionFlagSet} - * The PermissionFlagSet describing the current state of granted - * permissions for the user being edited. - */ - getPermissionFlags : function getPermissionFlags() { - return $scope.permissionFlags; - }, - - /** - * Notifies the controller that a change has been made to the given - * connection permission for the user being edited. This only applies - * to READ permissions. - * - * @param {String} identifier - * The identifier of the connection affected by the changed - * permission. - */ - connectionPermissionChanged : function connectionPermissionChanged(identifier) { - - // Determine current permission setting - var granted = $scope.permissionFlags.connectionPermissions.READ[identifier]; - - // Add/remove permission depending on flag state - if (granted) - addConnectionPermission(identifier); - else - removeConnectionPermission(identifier); - - }, - - /** - * Notifies the controller that a change has been made to the given - * connection group permission for the user being edited. This only - * applies to READ permissions. - * - * @param {String} identifier - * The identifier of the connection group affected by the changed - * permission. - */ - connectionGroupPermissionChanged : function connectionGroupPermissionChanged(identifier) { - - // Determine current permission setting - var granted = $scope.permissionFlags.connectionGroupPermissions.READ[identifier]; - - // Add/remove permission depending on flag state - if (granted) - addConnectionGroupPermission(identifier); - 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 granted = $scope.permissionFlags.sharingProfilePermissions.READ[identifier]; - - // Add/remove permission depending on flag state - if (granted) - addSharingProfilePermission(identifier); - else - removeSharingProfilePermission(identifier); - - } - - }; + $scope.permissionsRemoved = new PermissionSet(); /** * Cancels all pending edits, returning to the management page. @@ -1090,7 +596,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto * which is prepopulated with the data from the user currently being edited. */ $scope.cloneUser = function cloneUser() { - $location.path('/manage/' + encodeURIComponent(selectedDataSource) + '/users').search('clone', username); + $location.path('/manage/' + encodeURIComponent($scope.dataSource) + '/users').search('clone', username); }; /** @@ -1113,10 +619,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // Save or create the user, depending on whether the user exists var saveUserPromise; - if ($scope.userExists(selectedDataSource)) - saveUserPromise = userService.saveUser(selectedDataSource, $scope.user); + if ($scope.userExists($scope.dataSource)) + saveUserPromise = userService.saveUser($scope.dataSource, $scope.user); else - saveUserPromise = userService.createUser(selectedDataSource, $scope.user); + saveUserPromise = userService.createUser($scope.dataSource, $scope.user); saveUserPromise.then(function savedUser() { @@ -1124,21 +630,21 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto if ($scope.selfUsername !== $scope.user.username) { // Rename added permission - if (permissionsAdded.userPermissions[$scope.selfUsername]) { - permissionsAdded.userPermissions[$scope.user.username] = permissionsAdded.userPermissions[$scope.selfUsername]; - delete permissionsAdded.userPermissions[$scope.selfUsername]; + if ($scope.permissionsAdded.userPermissions[$scope.selfUsername]) { + $scope.permissionsAdded.userPermissions[$scope.user.username] = $scope.permissionsAdded.userPermissions[$scope.selfUsername]; + delete $scope.permissionsAdded.userPermissions[$scope.selfUsername]; } // Rename removed permission - if (permissionsRemoved.userPermissions[$scope.selfUsername]) { - permissionsRemoved.userPermissions[$scope.user.username] = permissionsRemoved.userPermissions[$scope.selfUsername]; - delete permissionsRemoved.userPermissions[$scope.selfUsername]; + if ($scope.permissionsRemoved.userPermissions[$scope.selfUsername]) { + $scope.permissionsRemoved.userPermissions[$scope.user.username] = $scope.permissionsRemoved.userPermissions[$scope.selfUsername]; + delete $scope.permissionsRemoved.userPermissions[$scope.selfUsername]; } } // Upon success, save any changed permissions - permissionService.patchPermissions(selectedDataSource, $scope.user.username, permissionsAdded, permissionsRemoved) + permissionService.patchPermissions($scope.dataSource, $scope.user.username, $scope.permissionsAdded, $scope.permissionsRemoved) .then(function patchedUserPermissions() { $location.url('/settings/users'); }, guacNotification.SHOW_REQUEST_ERROR); @@ -1180,7 +686,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var deleteUserImmediately = function deleteUserImmediately() { // Delete the user - userService.deleteUser(selectedDataSource, $scope.user) + userService.deleteUser($scope.dataSource, $scope.user) .then(function deletedUser() { $location.path('/settings/users'); }, guacNotification.SHOW_REQUEST_ERROR); diff --git a/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js new file mode 100644 index 000000000..c4be07105 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js @@ -0,0 +1,406 @@ +/* + * 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 the connection permissions granted within a + * given {@link PermissionFlagSet}, tracking the specific permissions added or + * removed within a separate pair of {@link PermissionSet} objects. + */ +angular.module('manage').directive('connectionPermissionEditor', ['$injector', + function connectionPermissionEditor($injector) { + + // Required types + var ConnectionGroup = $injector.get('ConnectionGroup'); + var GroupListItem = $injector.get('GroupListItem'); + var PermissionSet = $injector.get('PermissionSet'); + + // Required services + var connectionGroupService = $injector.get('connectionGroupService'); + var dataSourceService = $injector.get('dataSourceService'); + var requestService = $injector.get('requestService'); + + var directive = { + + // Element only + restrict: 'E', + replace: true, + + scope: { + + /** + * The unique identifier of the data source associated with the + * permissions being manipulated. + * + * @type String + */ + dataSource : '=', + + /** + * The current state of the permissions being manipulated. This + * {@link PemissionFlagSet} will be modified as changes are made + * through this permission editor. + * + * @type PermissionFlagSet + */ + permissionFlags : '=', + + /** + * The set of permissions that have been added, relative to the + * initial state of the permissions being manipulated. + * + * @type PermissionSet + */ + permissionsAdded : '=', + + /** + * The set of permissions that have been added, relative to the + * initial state of the permissions being manipulated. + * + * @type PermissionSet + */ + permissionsRemoved : '=' + + }, + + templateUrl: 'app/manage/templates/connectionPermissionEditor.html' + + }; + + directive.controller = ['$scope', function connectionPermissionEditorController($scope) { + + /** + * Array of all connection properties that are filterable. + * + * @type String[] + */ + $scope.filteredConnectionProperties = [ + 'name', + 'protocol' + ]; + + /** + * Array of all connection group properties that are filterable. + * + * @type String[] + */ + $scope.filteredConnectionGroupProperties = [ + 'name' + ]; + + /** + * A map of data source identifiers to the root connection groups within + * thost data sources. As only one data source is applicable to any + * particular permission set being edited/created, this will only + * contain a single key. + * + * @type Object. + */ + $scope.rootGroups = null; + + // Retrieve all connections for which we have ADMINISTER permission + dataSourceService.apply( + connectionGroupService.getConnectionGroupTree, + [$scope.dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) + .then(function connectionGroupReceived(rootGroups) { + + // Convert all received ConnectionGroup objects into GroupListItems + $scope.rootGroups = {}; + angular.forEach(rootGroups, function addGroupListItem(rootGroup, dataSource) { + $scope.rootGroups[dataSource] = GroupListItem.fromConnectionGroup(dataSource, rootGroup); + }); + + }, requestService.WARN); + + /** + * Expands all items within the tree descending from the given + * GroupListItem which have at least one descendant for which explicit + * READ permission is granted. The expanded state of all other items is + * left untouched. + * + * @param {GroupListItem} item + * The GroupListItem which should be conditionally expanded + * depending on whether READ permission is granted for any of its + * descendants. + * + * @param {PemissionFlagSet} flags + * The set of permissions which should be used to determine whether + * the given item and its descendants are expanded. + */ + var expandReadable = function expandReadable(item, flags) { + + // If the current item is expandable and has defined children, + // determine whether it should be expanded + if (item.expandable && item.children) { + angular.forEach(item.children, function expandReadableChild(child) { + + // Determine whether the permission set contains READ + // permission for the current child object + var readable = false; + switch (child.type) { + + case GroupListItem.Type.CONNECTION: + readable = flags.connectionPermissions.READ[child.identifier]; + break; + + case GroupListItem.Type.CONNECTION_GROUP: + readable = flags.connectionGroupPermissions.READ[child.identifier]; + break; + + case GroupListItem.Type.SHARING_PROFILE: + readable = flags.sharingProfilePermissions.READ[child.identifier]; + break; + + } + + // The parent should be expanded by default if the child is + // expanded by default OR the permission set contains READ + // permission on the child + item.expanded |= expandReadable(child, flags) || readable; + + }); + } + + return item.expanded; + + }; + + // Update default expanded state whenever connection groups and + // associated permissions change + $scope.$watchGroup(['rootGroups', 'permissionFlags'], function updateDefaultExpandedStates() { + + if (!$scope.rootGroups || !$scope.permissionFlags) + return; + + angular.forEach($scope.rootGroups, function updateExpandedStates(rootGroup) { + + // Automatically expand all objects with any descendants for + // which the permission set contains READ permission + expandReadable(rootGroup, $scope.permissionFlags); + + }); + + }); + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given connection permission. + * + * @param {String} identifier + * The identifier of the connection to add READ permission for. + */ + var addConnectionPermission = function addConnectionPermission(identifier) { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasConnectionPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addConnectionPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given connection permission. + * + * @param {String} identifier + * The identifier of the connection to remove READ permission for. + */ + var removeConnectionPermission = function removeConnectionPermission(identifier) { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasConnectionPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addConnectionPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given connection group permission. + * + * @param {String} identifier + * The identifier of the connection group to add READ permission + * for. + */ + var addConnectionGroupPermission = function addConnectionGroupPermission(identifier) { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasConnectionGroupPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionGroupPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addConnectionGroupPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given connection permission. + * + * @param {String} identifier + * The identifier of the connection to remove READ permission for. + */ + var removeConnectionGroupPermission = function removeConnectionGroupPermission(identifier) { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasConnectionGroupPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionGroupPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addConnectionGroupPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + }; + + /** + * 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($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeSharingProfilePermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addSharingProfilePermission($scope.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($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeSharingProfilePermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addSharingProfilePermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + }; + + // Expose permission query and modification functions to group list template + $scope.groupListContext = { + + /** + * Returns the PermissionFlagSet that contains the current state of + * granted permissions. + * + * @returns {PermissionFlagSet} + * The PermissionFlagSet describing the current state of granted + * permissions for the permission set being edited. + */ + getPermissionFlags : function getPermissionFlags() { + return $scope.permissionFlags; + }, + + /** + * Notifies the controller that a change has been made to the given + * connection permission for the permission set being edited. This + * only applies to READ permissions. + * + * @param {String} identifier + * The identifier of the connection affected by the changed + * permission. + */ + connectionPermissionChanged : function connectionPermissionChanged(identifier) { + + // Determine current permission setting + var granted = $scope.permissionFlags.connectionPermissions.READ[identifier]; + + // Add/remove permission depending on flag state + if (granted) + addConnectionPermission(identifier); + else + removeConnectionPermission(identifier); + + }, + + /** + * Notifies the controller that a change has been made to the given + * connection group permission for the permission set being edited. + * This only applies to READ permissions. + * + * @param {String} identifier + * The identifier of the connection group affected by the + * changed permission. + */ + connectionGroupPermissionChanged : function connectionGroupPermissionChanged(identifier) { + + // Determine current permission setting + var granted = $scope.permissionFlags.connectionGroupPermissions.READ[identifier]; + + // Add/remove permission depending on flag state + if (granted) + addConnectionGroupPermission(identifier); + else + removeConnectionGroupPermission(identifier); + + }, + + /** + * Notifies the controller that a change has been made to the given + * sharing profile permission for the permission set 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 granted = $scope.permissionFlags.sharingProfilePermissions.READ[identifier]; + + // Add/remove permission depending on flag state + if (granted) + addSharingProfilePermission(identifier); + else + removeSharingProfilePermission(identifier); + + } + + }; + + }]; + + return directive; + +}]); diff --git a/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js b/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js new file mode 100644 index 000000000..ec4187256 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js @@ -0,0 +1,308 @@ +/* + * 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 the system permissions granted within a given + * {@link PermissionFlagSet}, tracking the specific permissions added or + * removed within a separate pair of {@link PermissionSet} objects. Optionally, + * the permission for a particular user to update themselves (change their own + * password/attributes) may also be manipulated. + */ +angular.module('manage').directive('systemPermissionEditor', ['$injector', + function systemPermissionEditor($injector) { + + // Required services + var authenticationService = $injector.get('authenticationService'); + var dataSourceService = $injector.get('dataSourceService'); + var permissionService = $injector.get('permissionService'); + var requestService = $injector.get('requestService'); + + // Required types + var PermissionSet = $injector.get('PermissionSet'); + + var directive = { + + // Element only + restrict: 'E', + replace: true, + + scope: { + + /** + * The unique identifier of the data source associated with the + * permissions being manipulated. + * + * @type String + */ + dataSource : '=', + + /** + * The username of the user whose self-update permission (whether + * the user has permission to update their own user account) should + * be additionally controlled by this editor. If no such user + * permissions should be controlled, this should be left undefined. + * + * @type String + */ + username : '=', + + /** + * The current state of the permissions being manipulated. This + * {@link PemissionFlagSet} will be modified as changes are made + * through this permission editor. + * + * @type PermissionFlagSet + */ + permissionFlags : '=', + + /** + * The set of permissions that have been added, relative to the + * initial state of the permissions being manipulated. + * + * @type PermissionSet + */ + permissionsAdded : '=', + + /** + * The set of permissions that have been removed, relative to the + * initial state of the permissions being manipulated. + * + * @type PermissionSet + */ + permissionsRemoved : '=' + + }, + + templateUrl: 'app/manage/templates/systemPermissionEditor.html' + + }; + + directive.controller = ['$scope', function systemPermissionEditorController($scope) { + + /** + * The identifiers of all data sources currently available to the + * authenticated user. + * + * @type String[] + */ + var dataSources = authenticationService.getAvailableDataSources(); + + /** + * The username of the current, authenticated user. + * + * @type String + */ + var currentUsername = authenticationService.getCurrentUsername(); + + /** + * Available system permission types, as translation string / internal + * value pairs. + * + * @type Object[] + */ + $scope.systemPermissionTypes = [ + { + label: "MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", + value: PermissionSet.SystemPermissionType.ADMINISTER + }, + { + label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", + value: PermissionSet.SystemPermissionType.CREATE_USER + }, + { + label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", + value: PermissionSet.SystemPermissionType.CREATE_CONNECTION + }, + { + 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 + } + ]; + + // Query the permissions granted to the currently-authenticated user + dataSourceService.apply( + permissionService.getEffectivePermissions, + dataSources, + currentUsername + ) + .then(function permissionsReceived(permissions) { + $scope.permissions = permissions; + }, requestService.WARN); + + /** + * Returns whether the current user has permission to change the system + * permissions granted to users. + * + * @returns {Boolean} + * true if the current user can grant or revoke system permissions + * to the permission set being edited, false otherwise. + */ + $scope.canChangeSystemPermissions = function canChangeSystemPermissions() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Only the administrator can modify system permissions + return PermissionSet.hasSystemPermission($scope.permissions[$scope.dataSource], + PermissionSet.SystemPermissionType.ADMINISTER); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given system permission. + * + * @param {String} type + * The system permission to add, as defined by + * PermissionSet.SystemPermissionType. + */ + var addSystemPermission = function addSystemPermission(type) { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasSystemPermission($scope.permissionsRemoved, type)) + PermissionSet.removeSystemPermission($scope.permissionsRemoved, type); + + // Otherwise, explicitly add the permission + else + PermissionSet.addSystemPermission($scope.permissionsAdded, type); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given system permission. + * + * @param {String} type + * The system permission to remove, as defined by + * PermissionSet.SystemPermissionType. + */ + var removeSystemPermission = function removeSystemPermission(type) { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasSystemPermission($scope.permissionsAdded, type)) + PermissionSet.removeSystemPermission($scope.permissionsAdded, type); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addSystemPermission($scope.permissionsRemoved, type); + + }; + + /** + * Notifies the controller that a change has been made to the given + * system permission for the permission set being edited. + * + * @param {String} type + * The system permission that was changed, as defined by + * PermissionSet.SystemPermissionType. + */ + $scope.systemPermissionChanged = function systemPermissionChanged(type) { + + // Determine current permission setting + var granted = $scope.permissionFlags.systemPermissions[type]; + + // Add/remove permission depending on flag state + if (granted) + addSystemPermission(type); + else + removeSystemPermission(type); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given user permission. + * + * @param {String} type + * The user permission to add, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param {String} identifier + * The identifier of the user affected by the permission being added. + */ + var addUserPermission = function addUserPermission(type, identifier) { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasUserPermission($scope.permissionsRemoved, type, identifier)) + PermissionSet.removeUserPermission($scope.permissionsRemoved, type, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addUserPermission($scope.permissionsAdded, type, identifier); + + }; + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given user permission. + * + * @param {String} type + * The user permission to remove, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param {String} identifier + * The identifier of the user affected by the permission being + * removed. + */ + var removeUserPermission = function removeUserPermission(type, identifier) { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasUserPermission($scope.permissionsAdded, type, identifier)) + PermissionSet.removeUserPermission($scope.permissionsAdded, type, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addUserPermission($scope.permissionsRemoved, type, identifier); + + }; + + /** + * Notifies the controller that a change has been made to the given user + * permission for the permission set being edited. + * + * @param {String} type + * The user permission that was changed, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param {String} identifier + * The identifier of the user affected by the changed permission. + */ + $scope.userPermissionChanged = function userPermissionChanged(type, identifier) { + + // Determine current permission setting + var granted = $scope.permissionFlags.userPermissions[type][identifier]; + + // Add/remove permission depending on flag state + if (granted) + addUserPermission(type, identifier); + else + removeUserPermission(type, identifier); + + }; + + }]; + + return directive; + +}]); diff --git a/guacamole/src/main/webapp/app/manage/templates/connectionPermissionEditor.html b/guacamole/src/main/webapp/app/manage/templates/connectionPermissionEditor.html new file mode 100644 index 000000000..61d380408 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/templates/connectionPermissionEditor.html @@ -0,0 +1,21 @@ +
+
+

{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

+ +
+
+ +
+
\ No newline at end of file diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 55b6d3098..5fed148ef 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -46,47 +46,21 @@ -
-

{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

-
- - - - - - - - - -
{{systemPermissionType.label | translate}}
{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
-
-
+ + -
-
-

{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

- -
-
- -
-
+ +
diff --git a/guacamole/src/main/webapp/app/manage/templates/systemPermissionEditor.html b/guacamole/src/main/webapp/app/manage/templates/systemPermissionEditor.html new file mode 100644 index 000000000..47fec6630 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/templates/systemPermissionEditor.html @@ -0,0 +1,18 @@ +
+

{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

+
+ + + + + + + + + +
{{systemPermissionType.label | translate}}
{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
+
+
\ No newline at end of file From 507202d1f3c6b5e96e9f2a6d6f341331af9d34c0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 30 Apr 2018 23:24:15 -0700 Subject: [PATCH 02/20] GUACAMOLE-220: Define abstract object for querying the management-related actions a user may take on a particular object or type of object. --- .../app/manage/types/ManagementPermissions.js | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js diff --git a/guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js b/guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js new file mode 100644 index 000000000..f9a78b7dc --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js @@ -0,0 +1,172 @@ +/* + * 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 service for defining the ManagementPermissions class. + */ +angular.module('manage').factory('ManagementPermissions', ['$injector', + function defineManagementPermissions($injector) { + + // Required types + var PermissionSet = $injector.get('PermissionSet'); + + /** + * Higher-level representation of the management-related permissions + * available to the current user on a particular, arbitrary object. + * + * @constructor + * @param {ManagementPermissions|Object} template + * An object whose properties should be copied into the new + * ManagementPermissions object. + */ + var ManagementPermissions = function ManagementPermissions(template) { + + /** + * Whether the user can save the associated object. This could be + * updating an existing object, or creating a new object. + * + * @type Boolean + */ + this.canSaveObject = template.canSaveObject; + + /** + * Whether the user can clone the associated object. + * + * @type Boolean + */ + this.canCloneObject = template.canCloneObject; + + /** + * Whether the user can delete the associated object. + * + * @type Boolean + */ + this.canDeleteObject = template.canDeleteObject; + + /** + * Whether the user can change attributes which are currently + * associated with the object. + * + * @type Boolean + */ + this.canChangeAttributes = template.canChangeAttributes; + + /** + * Whether the user can change absolutely all attributes associated + * with the object, including those which are not already present. + * + * @type Boolean + */ + this.canChangeAllAttributes = template.canChangeAllAttributes; + + /** + * Whether the user can change permissions which are assigned to the + * associated object, if the object is capable of being assigned + * permissions. + * + * @type Boolean + */ + this.canChangePermissions = template.canChangePermissions; + + }; + + /** + * Creates a new {@link ManagementPermissions} which defines the high-level + * actions the current user may take for the given object. + * + * @param {PermissionSet} permissions + * The effective permissions granted to the current user within the + * data source associated with the object being managed. + * + * @param {String} createPermission + * The system permission required to create objects of the same type as + * the object being managed, as defined by + * {@link PermissionSet.SystemPermissionTypes}. + * + * @param {Function} hasObjectPermission + * The function to invoke to test whether a {@link PermissionSet} + * contains a particular object permission. The parameters accepted + * by this function must be identical to those accepted by + * {@link PermissionSet.hasUserPermission()}, + * {@link PermissionSet.hasConnectionPermission()}, etc. + * + * @param {String} [identifier] + * The identifier of the object being managed. If the object does not + * yet exist, this parameter should be omitted or set to null. + * + * @returns {ManagementPermissions} + * A new {@link ManagementPermissions} which defines the high-level + * actions the current user may take for the given object. + */ + ManagementPermissions.fromPermissionSet = function fromPermissionSet( + permissions, createPermission, hasObjectPermission, identifier) { + + var isAdmin = PermissionSet.hasSystemPermission(permissions, + PermissionSet.SystemPermissionType.ADMINISTER); + + var canCreate = PermissionSet.hasSystemPermission(permissions, createPermission); + var canAdminister = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER, identifier); + var canUpdate = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); + var canDelete = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier); + + var exists = !!identifier; + + return new ManagementPermissions({ + + // A user can save (create or update) an object if they are a + // system-level administrator, OR the object does not yet exist and + // the user has explicit permission to create such objects, OR the + // object does already exist and the user has explicit UPDATE + // permission on the object + canSaveObject : isAdmin || (!exists && canCreate) || canUpdate, + + // A user can clone an object only if the object exists, and + // only if they are a system-level administrator OR they have + // explicit permission to create such objects + canCloneObject : exists && (isAdmin || canCreate), + + // A user can delete an object only if the object exists, and + // only if they are a system-level administrator OR they have + // explicit DELETE permission on the object + canDeleteObject : exists && (isAdmin || canDelete), + + // Attributes in general (with or without existing values) can only + // be changed if the object is being created, OR the user is a + // system-level administrator, OR the user has explicit UPDATE + // permission on the object + canChangeAttributes : !exists || isAdmin || canUpdate, + + // A user can change the attributes of an object which are not + // explicitly defined on that object when the object is being + // created + canChangeAllAttributes : !exists, + + // A user can change the system permissions related to an object + // if they are a system-level admin, OR they are creating the + // object, OR they have explicit ADMINISTER permission on the + // existing object + canChangePermissions : isAdmin || !exists || canAdminister + + }); + + }; + + return ManagementPermissions; + +}]); From 4f43ddc4203a59c7aea6834cfd8b5961c74ba57e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 00:03:52 -0700 Subject: [PATCH 03/20] GUACAMOLE-220: Migrate user management controller to ManagementPermissions. --- .../controllers/manageUserController.js | 526 ++++++------------ .../app/manage/templates/manageUser.html | 18 +- 2 files changed, 184 insertions(+), 360 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index c3850650c..388e71753 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -24,14 +24,16 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto function manageUserController($scope, $injector) { // Required types - var PageDefinition = $injector.get('PageDefinition'); - var PermissionFlagSet = $injector.get('PermissionFlagSet'); - var PermissionSet = $injector.get('PermissionSet'); - var User = $injector.get('User'); + var ManagementPermissions = $injector.get('ManagementPermissions'); + var PageDefinition = $injector.get('PageDefinition'); + var PermissionFlagSet = $injector.get('PermissionFlagSet'); + var PermissionSet = $injector.get('PermissionSet'); + var User = $injector.get('User'); // Required services var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams'); + var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); @@ -128,13 +130,31 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissionFlags = null; /** - * A map of data source identifiers to the set of all permissions - * associated with the current user under that data source, or null if the - * user's permissions have not yet been loaded. + * The set of permissions that will be added to the user when the user is + * saved. Permissions will only be present in this set if they are + * manually added, and not later manually removed before saving. * - * @type Object. + * @type PermissionSet */ - $scope.permissions = null; + $scope.permissionsAdded = new PermissionSet(); + + /** + * The set of permissions that will be removed from the user when the user + * is saved. Permissions will only be present in this set if they are + * manually removed, and not later manually added before saving. + * + * @type PermissionSet + */ + $scope.permissionsRemoved = new PermissionSet(); + + /** + * The managment-related actions that the current user may perform on the + * user currently being created/modified, or null if the current user's + * permissions have not yet been loaded. + * + * @type ManagementPermissions + */ + $scope.managementPermissions = null; /** * All available user attributes. This is only the set of attribute @@ -162,11 +182,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ $scope.isLoaded = function isLoaded() { - return $scope.users !== null - && $scope.permissionFlags !== null - && $scope.rootGroups !== null - && $scope.permissions !== null - && $scope.attributes !== null; + return $scope.users !== null + && $scope.permissionFlags !== null + && $scope.managementPermissions !== null + && $scope.attributes !== null; }; @@ -195,97 +214,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; - /** - * Returns whether the current user can change attributes explicitly - * associated with the user being edited within the given data source. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the current user can change attributes associated with the - * user being edited, false otherwise. - */ - $scope.canChangeAttributes = function canChangeAttributes(dataSource) { - - // Do not check if permissions are not yet loaded - if (!$scope.permissions) - return false; - - // Use currently-selected data source if unspecified - dataSource = dataSource || $scope.dataSource; - - // Attributes can always be set if we are creating the user - if (!$scope.userExists(dataSource)) - return true; - - // The administrator can always change attributes - if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.ADMINISTER)) - return true; - - // Otherwise, can change attributes if we have permission to update this user - return PermissionSet.hasUserPermission($scope.permissions[dataSource], - PermissionSet.ObjectPermissionType.UPDATE, username); - - }; - - /** - * Returns whether the current user can change/set all user attributes for - * the user being edited, regardless of whether those attributes are - * already explicitly associated with that user. - * - * @returns {Boolean} - * true if the current user can change all attributes for the user - * being edited, regardless of whether those attributes are already - * explicitly associated with that user, false otherwise. - */ - $scope.canChangeAllAttributes = function canChangeAllAttributes() { - - // All attributes can be set if we are creating the user - return !$scope.userExists($scope.dataSource); - - }; - - /** - * Returns whether the current user can change permissions of any kind - * which are associated with the user being edited within the given data - * source. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the current user can grant or revoke permissions of any kind - * which are associated with the user being edited, false otherwise. - */ - $scope.canChangePermissions = function canChangePermissions(dataSource) { - - // Do not check if permissions are not yet loaded - if (!$scope.permissions) - return false; - - // Use currently-selected data source if unspecified - dataSource = dataSource || $scope.dataSource; - - // Permissions can always be set if we are creating the user - if (!$scope.userExists(dataSource)) - return true; - - // The administrator can always modify permissions - if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.ADMINISTER)) - return true; - - // Otherwise, can only modify permissions if we have explicit - // ADMINISTER permission - return PermissionSet.hasUserPermission($scope.permissions[dataSource], - PermissionSet.ObjectPermissionType.ADMINISTER, username); - - }; - /** * Returns whether the current user can edit the username of the user being * edited within the given data source. @@ -303,189 +231,29 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; /** - * Returns whether the current user can save the user being edited within - * the given data source. Saving will create or update that user depending - * on whether the user already exists. + * Loads the data associated with the user having the given username, + * preparing the interface for making modifications to that existing user. * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. + * @param {String} dataSource + * The unique identifier of the data source containing the user to + * load. * - * @returns {Boolean} - * true if the current user can save changes to the user being edited, - * false otherwise. + * @param {String} username + * The username of the user to load. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * editing the given user. */ - $scope.canSaveUser = function canSaveUser(dataSource) { + var loadExistingUser = function loadExistingUser(dataSource, username) { + return $q.all({ + users : dataSourceService.apply(userService.getUser, dataSources, username), + permissions : permissionService.getPermissions(dataSource, username) + }) + .then(function userDataRetrieved(values) { - // Do not check if permissions are not yet loaded - if (!$scope.permissions) - return false; - - // Use currently-selected data source if unspecified - dataSource = dataSource || $scope.dataSource; - - // The administrator can always save users - if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.ADMINISTER)) - return true; - - // If user does not exist, can only save if we have permission to create users - if (!$scope.userExists(dataSource)) - return PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.CREATE_USER); - - // Otherwise, can only save if we have permission to update this user - return PermissionSet.hasUserPermission($scope.permissions[dataSource], - PermissionSet.ObjectPermissionType.UPDATE, username); - - }; - - /** - * Returns whether the current user can clone the user being edited within - * the given data source. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the current user can clone the user being edited, false - * otherwise. - */ - $scope.canCloneUser = function canCloneUser(dataSource) { - - // Do not check if permissions are not yet loaded - if (!$scope.permissions) - return false; - - // Use currently-selected data source if unspecified - dataSource = dataSource || $scope.dataSource; - - // If we are not editing an existing user, we cannot clone - if (!$scope.userExists($scope.dataSource)) - return false; - - // The administrator can always clone users - if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.ADMINISTER)) - return true; - - // Otherwise we need explicit CREATE_USER permission - return PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.CREATE_USER); - - }; - - /** - * Returns whether the current user can delete the user being edited from - * the given data source. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the current user can delete the user being edited, false - * otherwise. - */ - $scope.canDeleteUser = function canDeleteUser(dataSource) { - - // Do not check if permissions are not yet loaded - if (!$scope.permissions) - return false; - - // Use currently-selected data source if unspecified - dataSource = dataSource || $scope.dataSource; - - // Can't delete what doesn't exist - if (!$scope.userExists(dataSource)) - return false; - - // The administrator can always delete users - if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], - PermissionSet.SystemPermissionType.ADMINISTER)) - return true; - - // Otherwise, require explicit DELETE permission on the user - return PermissionSet.hasUserPermission($scope.permissions[dataSource], - PermissionSet.ObjectPermissionType.DELETE, username); - - }; - - /** - * Returns whether the user being edited within the given data source is - * read-only, and thus cannot be modified by the current user. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the user being edited is actually read-only and cannot be - * edited at all, false otherwise. - */ - $scope.isReadOnly = function isReadOnly(dataSource) { - - // Use currently-selected data source if unspecified - dataSource = dataSource || $scope.dataSource; - - // User is read-only if they cannot be saved - return !$scope.canSaveUser(dataSource); - - }; - - // Update visible account pages whenever available users/permissions changes - $scope.$watchGroup(['users', 'permissions'], function updateAccountPages() { - - // Generate pages for each applicable data source - $scope.accountPages = []; - angular.forEach(dataSources, function addAccountPage(dataSource) { - - // Determine whether data source contains this user - var linked = $scope.userExists(dataSource); - var readOnly = $scope.isReadOnly(dataSource); - - // Account is not relevant if it does not exist and cannot be - // created - if (!linked && readOnly) - return; - - // Only the selected data source is relevant when cloning - if (cloneSourceUsername && dataSource !== $scope.dataSource) - return; - - // Determine class name based on read-only / linked status - var className; - if (readOnly) className = 'read-only'; - else if (linked) className = 'linked'; - else className = 'unlinked'; - - // Add page entry - $scope.accountPages.push(new PageDefinition({ - name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', - url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username || ''), - className : className - })); - - }); - - }); - - // Pull user attribute schema - schemaService.getUserAttributes($scope.dataSource).then(function attributesReceived(attributes) { - $scope.attributes = attributes; - }, requestService.WARN); - - // Pull user data and permissions if we are editing an existing user - if (username) { - - // Pull user data - dataSourceService.apply(userService.getUser, dataSources, username) - .then(function usersReceived(users) { - - // Get user for currently-selected data source - $scope.users = users; - $scope.user = users[$scope.dataSource]; + $scope.users = values.users; + $scope.user = values.users[dataSource]; // Create skeleton user if user does not exist if (!$scope.user) @@ -493,54 +261,57 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto 'username' : username }); - }, requestService.WARN); + // The current user will be associated with username of the existing + // user in the retrieved permission set + $scope.selfUsername = username; + $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions); - // The current user will be associated with username of the existing - // user in the retrieved permission set - $scope.selfUsername = username; + }); + }; - // Pull user permissions - permissionService.getPermissions($scope.dataSource, username).then(function gotPermissions(permissions) { - $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + /** + * Loads the data associated with the user having the given username, + * preparing the interface for cloning that existing user. + * + * @param {String} dataSource + * The unique identifier of the data source containing the user to + * be cloned. + * + * @param {String} username + * The username of the user being cloned. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * cloning the given user. + */ + var loadClonedUser = function loadClonedUser(dataSource, username) { + return $q.all({ + users : dataSourceService.apply(userService.getUser, [dataSource], username), + permissions : permissionService.getPermissions(dataSource, username) }) + .then(function userDataRetrieved(values) { - // If permissions cannot be retrieved, use empty permissions - ['catch'](requestService.createErrorCallback(function permissionRetrievalFailed() { - $scope.permissionFlags = new PermissionFlagSet(); - })); - } - - // If we are cloning an existing user, pull his/her data instead - else if (cloneSourceUsername) { - - dataSourceService.apply(userService.getUser, dataSources, cloneSourceUsername) - .then(function usersReceived(users) { - - // Get user for currently-selected data source $scope.users = {}; - $scope.user = users[$scope.dataSource]; + $scope.user = values.users[dataSource]; - }, requestService.WARN); + // The current user will be associated with cloneSourceUsername in the + // retrieved permission set + $scope.selfUsername = username; + $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions); + $scope.permissionsAdded = values.permissions; - // The current user will be associated with cloneSourceUsername in the - // retrieved permission set - $scope.selfUsername = cloneSourceUsername; + }); + }; - // Pull user permissions - permissionService.getPermissions($scope.dataSource, cloneSourceUsername) - .then(function gotPermissions(permissions) { - $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); - $scope.permissionsAdded = permissions; - }) - - // If permissions cannot be retrieved, use empty permissions - ['catch'](requestService.createErrorCallback(function permissionRetrievalFailed() { - $scope.permissionFlags = new PermissionFlagSet(); - })); - } - - // Use skeleton data if we are creating a new user - else { + /** + * Loads skeleton user data, preparing the interface for creating a new + * user. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * creating a new user. + */ + var loadSkeletonUser = function loadSkeletonUser() { // No users exist regardless of data source if there is no username $scope.users = {}; @@ -554,36 +325,89 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // permissions $scope.selfUsername = 'SELF'; - } + return $q.resolve(); + + }; + + /** + * Loads the data requred for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing user, cloning an existing user, or + * creating an entirely new user. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared + * for performing the requested management task. + */ + var loadRequestedUser = function loadRequestedUser() { + + // Pull user data and permissions if we are editing an existing user + if (username) + return loadExistingUser($scope.dataSource, username); + + // If we are cloning an existing user, pull his/her data instead + if (cloneSourceUsername) + return loadClonedUser($scope.dataSource, cloneSourceUsername); + + return loadSkeletonUser(); + + }; + + // Populate interface with requested data + $q.all({ + userData : loadRequestedUser(), + permissions : dataSourceService.apply(permissionService.getEffectivePermissions, dataSources, currentUsername), + attributes : schemaService.getUserAttributes($scope.dataSource) + }) + .then(function dataReceived(values) { + + var managementPermissions = {}; + + $scope.attributes = values.attributes; + + // Generate pages for each applicable data source + $scope.accountPages = []; + angular.forEach(dataSources, function addAccountPage(dataSource) { + + // Determine whether data source contains this user + var exists = (dataSource in $scope.users); + + // Calculate management actions available for this specific account + managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet( + values.permissions[dataSource], + PermissionSet.SystemPermissionType.CREATE_USER, + PermissionSet.hasUserPermission, + exists ? username : null); + + // Account is not relevant if it does not exist and cannot be + // created + var readOnly = !managementPermissions[dataSource].canSaveObject; + if (!exists && readOnly) + return; + + // Only the selected data source is relevant when cloning + if (cloneSourceUsername && dataSource !== $scope.dataSource) + return; + + // Determine class name based on read-only / linked status + var className; + if (readOnly) className = 'read-only'; + else if (exists) className = 'linked'; + else className = 'unlinked'; + + // Add page entry + $scope.accountPages.push(new PageDefinition({ + name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username || ''), + className : className + })); + + }); + + $scope.managementPermissions = managementPermissions[$scope.dataSource]; - // Query the user's permissions for the current user - dataSourceService.apply( - permissionService.getEffectivePermissions, - dataSources, - currentUsername - ) - .then(function permissionsReceived(permissions) { - $scope.permissions = permissions; }, requestService.WARN); - /** - * The set of permissions that will be added to the user when the user is - * saved. Permissions will only be present in this set if they are - * manually added, and not later manually removed before saving. - * - * @type PermissionSet - */ - $scope.permissionsAdded = new PermissionSet(); - - /** - * The set of permissions that will be removed from the user when the user - * is saved. Permissions will only be present in this set if they are - * manually removed, and not later manually added before saving. - * - * @type PermissionSet - */ - $scope.permissionsRemoved = new PermissionSet(); - /** * Cancels all pending edits, returning to the management page. */ diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 5fed148ef..24db74e2c 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -11,12 +11,12 @@
-
+

{{'MANAGE_USER.INFO_READ_ONLY' | translate}}

-
+
@@ -40,13 +40,13 @@
-
+
+ model="user.attributes" model-only="!managementPermissions.canChangeAllAttributes">
- -
- - + + - +
From e045da132c729d0567f256708d204d607c053f9f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 09:46:46 -0700 Subject: [PATCH 04/20] GUACAMOLE-220: Add common directive for displaying the save/clone/cancel/delete buttons shared by all object management pages. --- .../manage/directives/managementButtons.js | 201 ++++++++++++++++++ .../manage/templates/managementButtons.html | 6 + 2 files changed, 207 insertions(+) create mode 100644 guacamole/src/main/webapp/app/manage/directives/managementButtons.js create mode 100644 guacamole/src/main/webapp/app/manage/templates/managementButtons.html diff --git a/guacamole/src/main/webapp/app/manage/directives/managementButtons.js b/guacamole/src/main/webapp/app/manage/directives/managementButtons.js new file mode 100644 index 000000000..407454881 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/directives/managementButtons.js @@ -0,0 +1,201 @@ +/* + * 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. + */ + +/** + * Directive which displays a set of object management buttons (save, delete, + * clone, etc.) representing the actions available to the current user in + * context of the object being edited/created. + */ +angular.module('manage').directive('managementButtons', ['$injector', + function managementButtons($injector) { + + // Required services + var guacNotification = $injector.get('guacNotification'); + + var directive = { + + restrict : 'E', + replace : true, + templateUrl : 'app/manage/templates/managementButtons.html', + + scope : { + + /** + * The translation namespace associated with all applicable + * translation strings. This directive requires at least the + * following translation strings within the given namespace: + * + * - ACTION_CANCEL + * - ACTION_CLONE + * - ACTION_DELETE + * - ACTION_SAVE + * - DIALOG_HEADER_CONFIRM_DELETE + * - TEXT_CONFIRM_DELETE + * + * @type String + */ + namespace : '=', + + /** + * The permissions which dictate the management actions available + * to the current user. + * + * @type ManagementPermissions + */ + permissions : '=', + + /** + * The function to invoke to save the arbitrary object being edited + * if the current user has permission to do so. The provided + * function MUST return a promise which is resolved if the save + * operation succeeds and is rejected with an {@link Error} if the + * save operation fails. + * + * @type Function + */ + save : '&', + + /** + * The function to invoke when the current user chooses to clone + * the object being edited. The provided function MUST perform the + * actions necessary to produce an interface which will clone the + * object. + * + * @type Function + */ + clone : '&', + + /** + * The function to invoke to delete the arbitrary object being edited + * if the current user has permission to do so. The provided + * function MUST return a promise which is resolved if the delete + * operation succeeds and is rejected with an {@link Error} if the + * delete operation fails. + * + * @type Function + */ + delete : '&', + + /** + * The function to invoke when the current user chooses to cancel + * the edit in progress, or when a save/delete operation has + * succeeded. The provided function MUST perform the actions + * necessary to return the user to a reasonable starting point. + * + * @type Function + */ + return : '&' + + } + + }; + + directive.controller = ['$scope', function managementButtonsController($scope) { + + /** + * An action to be provided along with the object sent to showStatus which + * immediately deletes the current connection. + */ + var DELETE_ACTION = { + name : $scope.namespace + '.ACTION_DELETE', + className : 'danger', + callback : function deleteCallback() { + deleteObjectImmediately(); + guacNotification.showStatus(false); + } + }; + + /** + * An action to be provided along with the object sent to showStatus which + * closes the currently-shown status dialog. + */ + var CANCEL_ACTION = { + name : $scope.namespace + '.ACTION_CANCEL', + callback : function cancelCallback() { + guacNotification.showStatus(false); + } + }; + + /** + * Invokes the provided return function to navigate the user back to + * the page they started from. + */ + var navigateBack = function navigateBack() { + $scope['return']($scope.$parent); + }; + + /** + * Invokes the provided delete function, immediately deleting the + * current object without prompting the user for confirmation. If + * deletion is successful, the user is navigated back to the page they + * started from. If the deletion fails, an error notification is + * displayed. + */ + var deleteObjectImmediately = function deleteObjectImmediately() { + $scope['delete']($scope.$parent).then(navigateBack, guacNotification.SHOW_REQUEST_ERROR); + }; + + /** + * Cancels all pending edits, returning to the page the user started + * from. + */ + $scope.cancel = navigateBack; + + /** + * Cancels all pending edits, invoking the provided clone function to + * open an edit page for a new object which is prepopulated with the + * data from the current object. + */ + $scope.cloneObject = function cloneObject () { + $scope.clone($scope.$parent); + }; + + /** + * Invokes the provided save function to saves the current object. If + * saving is successful, the user is navigated back to the page they + * started from. If saving fails, an error notification is displayed. + */ + $scope.saveObject = function saveObject() { + $scope.save($scope.$parent).then(navigateBack, guacNotification.SHOW_REQUEST_ERROR); + }; + + /** + * Deletes the current object, prompting the user first to confirm that + * deletion is desired. If the user confirms that deletion is desired, + * the object is deleted through invoking the provided delete function. + * The user is automatically navigated back to the page they started + * from or given an error notification depending on whether deletion + * succeeds. + */ + $scope.deleteObject = function deleteObject() { + + // Confirm deletion request + guacNotification.showStatus({ + title : $scope.namespace + '.DIALOG_HEADER_CONFIRM_DELETE', + text : { key : $scope.namespace + '.TEXT_CONFIRM_DELETE' }, + actions : [ DELETE_ACTION, CANCEL_ACTION] + }); + + }; + + }]; + + return directive; + +}]); diff --git a/guacamole/src/main/webapp/app/manage/templates/managementButtons.html b/guacamole/src/main/webapp/app/manage/templates/managementButtons.html new file mode 100644 index 000000000..d43209a45 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/templates/managementButtons.html @@ -0,0 +1,6 @@ +
+ + + + +
From 0414cdd3edc4f5ba11900750ed26782e335de7b2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 09:53:26 -0700 Subject: [PATCH 05/20] GUACAMOLE-220: Migrate user management screen to new, common management button directive. --- .../controllers/manageUserController.js | 105 +++++------------- .../app/manage/templates/manageUser.html | 13 ++- 2 files changed, 32 insertions(+), 86 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 388e71753..b1f4f2ec3 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -24,6 +24,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto function manageUserController($scope, $injector) { // Required types + var Error = $injector.get('Error'); var ManagementPermissions = $injector.get('ManagementPermissions'); var PageDefinition = $injector.get('PageDefinition'); var PermissionFlagSet = $injector.get('PermissionFlagSet'); @@ -36,25 +37,12 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var dataSourceService = $injector.get('dataSourceService'); - var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var requestService = $injector.get('requestService'); var schemaService = $injector.get('schemaService'); var translationStringService = $injector.get('translationStringService'); var userService = $injector.get('userService'); - /** - * An action to be provided along with the object sent to showStatus which - * closes the currently-shown status dialog. - */ - var ACKNOWLEDGE_ACTION = { - name : "MANAGE_USER.ACTION_ACKNOWLEDGE", - // Handle action - callback : function acknowledgeCallback() { - guacNotification.showStatus(false); - } - }; - /** * The identifiers of all data sources currently available to the * authenticated user. @@ -409,9 +397,9 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }, requestService.WARN); /** - * Cancels all pending edits, returning to the management page. + * Cancels all pending edits, returning to the main list of users. */ - $scope.cancel = function cancel() { + $scope.returnToUserList = function returnToUserList() { $location.url('/settings/users'); }; @@ -424,21 +412,23 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; /** - * Saves the user, updating the existing user only. + * Saves the current user, creating a new user or updating the existing + * user depending on context, returning a promise which is resolved if the + * save operation succeeds and rejected if the save operation fails. + * + * @returns {Promise} + * A promise which is resolved if the save operation succeeds and is + * rejected with an {@link Error} if the save operation fails. */ $scope.saveUser = function saveUser() { // Verify passwords match if ($scope.passwordMatch !== $scope.user.password) { - guacNotification.showStatus({ - 'className' : 'error', - 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', - 'text' : { + return $q.reject(new Error({ + translatableMessage : { key : 'MANAGE_USER.ERROR_PASSWORD_MISMATCH' - }, - 'actions' : [ ACKNOWLEDGE_ACTION ] - }); - return; + } + })); } // Save or create the user, depending on whether the user exists @@ -448,7 +438,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto else saveUserPromise = userService.createUser($scope.dataSource, $scope.user); - saveUserPromise.then(function savedUser() { + return saveUserPromise.then(function savedUser() { // Move permission flags if username differs from marker if ($scope.selfUsername !== $scope.user.username) { @@ -468,70 +458,25 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto } // Upon success, save any changed permissions - permissionService.patchPermissions($scope.dataSource, $scope.user.username, $scope.permissionsAdded, $scope.permissionsRemoved) + return permissionService.patchPermissions($scope.dataSource, $scope.user.username, $scope.permissionsAdded, $scope.permissionsRemoved) .then(function patchedUserPermissions() { $location.url('/settings/users'); - }, guacNotification.SHOW_REQUEST_ERROR); + }); - }, guacNotification.SHOW_REQUEST_ERROR); + }); }; /** - * An action to be provided along with the object sent to showStatus which - * immediately deletes the current user. - */ - var DELETE_ACTION = { - name : "MANAGE_USER.ACTION_DELETE", - className : "danger", - // Handle action - callback : function deleteCallback() { - deleteUserImmediately(); - guacNotification.showStatus(false); - } - }; - - /** - * An action to be provided along with the object sent to showStatus which - * closes the currently-shown status dialog. - */ - var CANCEL_ACTION = { - name : "MANAGE_USER.ACTION_CANCEL", - // Handle action - callback : function cancelCallback() { - guacNotification.showStatus(false); - } - }; - - /** - * Immediately deletes the current user, without prompting the user for - * confirmation. - */ - var deleteUserImmediately = function deleteUserImmediately() { - - // Delete the user - userService.deleteUser($scope.dataSource, $scope.user) - .then(function deletedUser() { - $location.path('/settings/users'); - }, guacNotification.SHOW_REQUEST_ERROR); - - }; - - /** - * Deletes the user, prompting the user first to confirm that deletion is - * desired. + * Deletes the current user, returning a promise which is resolved if the + * delete operation succeeds and rejected if the delete operation fails. + * + * @returns {Promise} + * A promise which is resolved if the delete operation succeeds and is + * rejected with an {@link Error} if the delete operation fails. */ $scope.deleteUser = function deleteUser() { - - // Confirm deletion request - guacNotification.showStatus({ - 'title' : 'MANAGE_USER.DIALOG_HEADER_CONFIRM_DELETE', - 'text' : { - key : 'MANAGE_USER.TEXT_CONFIRM_DELETE' - }, - 'actions' : [ DELETE_ACTION, CANCEL_ACTION] - }); - + return userService.deleteUser($scope.dataSource, $scope.user); }; }]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 24db74e2c..02b3ddce5 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -63,12 +63,13 @@ -
- - - - -
+ +
From 00fee4ac3af746faea9596b7e69af20d8b02d662 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 11:22:31 -0700 Subject: [PATCH 06/20] GUACAMOLE-220: Migrate connection management screen to common buttons and permission logic. --- .../controllers/manageConnectionController.js | 375 ++++++++---------- .../manage/templates/manageConnection.html | 15 +- 2 files changed, 169 insertions(+), 221 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js index 2bbd999f5..7cd08ac01 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js @@ -24,17 +24,18 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i function manageConnectionController($scope, $injector) { // Required types - var Connection = $injector.get('Connection'); - var ConnectionGroup = $injector.get('ConnectionGroup'); - var HistoryEntryWrapper = $injector.get('HistoryEntryWrapper'); - var PermissionSet = $injector.get('PermissionSet'); + var Connection = $injector.get('Connection'); + var ConnectionGroup = $injector.get('ConnectionGroup'); + var HistoryEntryWrapper = $injector.get('HistoryEntryWrapper'); + var ManagementPermissions = $injector.get('ManagementPermissions'); + var PermissionSet = $injector.get('PermissionSet'); // Required services var $location = $injector.get('$location'); + var $q = $injector.get('$q'); var $routeParams = $injector.get('$routeParams'); var $translate = $injector.get('$translate'); var authenticationService = $injector.get('authenticationService'); - var guacNotification = $injector.get('guacNotification'); var connectionService = $injector.get('connectionService'); var connectionGroupService = $injector.get('connectionGroupService'); var permissionService = $injector.get('permissionService'); @@ -108,36 +109,15 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i * @type HistoryEntryWrapper[] */ $scope.historyEntryWrappers = null; - - /** - * Whether the user can save the connection being edited. This could be - * updating an existing connection, or creating a new connection. - * - * @type Boolean - */ - $scope.canSaveConnection = null; - - /** - * Whether the user can delete the connection being edited. - * - * @type Boolean - */ - $scope.canDeleteConnection = null; - - /** - * Whether the user can clone the connection being edited. - * - * @type Boolean - */ - $scope.canCloneConnection = null; /** - * All permissions associated with the current user, or null if the user's - * permissions have not yet been loaded. + * The managment-related actions that the current user may perform on the + * connection currently being created/modified, or null if the current + * user's permissions have not yet been loaded. * - * @type PermissionSet + * @type ManagementPermissions */ - $scope.permissions = null; + $scope.managementPermissions = null; /** * All available connection attributes. This is only the set of attribute @@ -157,154 +137,163 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i */ $scope.isLoaded = function isLoaded() { - return $scope.protocols !== null - && $scope.rootGroup !== null - && $scope.connection !== null - && $scope.parameters !== null - && $scope.historyDateFormat !== null - && $scope.historyEntryWrappers !== null - && $scope.permissions !== null - && $scope.attributes !== null - && $scope.canSaveConnection !== null - && $scope.canDeleteConnection !== null - && $scope.canCloneConnection !== null; + return $scope.protocols !== null + && $scope.rootGroup !== null + && $scope.connection !== null + && $scope.parameters !== null + && $scope.historyDateFormat !== null + && $scope.historyEntryWrappers !== null + && $scope.managementPermissions !== null + && $scope.attributes !== null; }; - // Pull connection attribute schema - schemaService.getConnectionAttributes($scope.selectedDataSource) - .then(function attributesReceived(attributes) { - $scope.attributes = attributes; - }, requestService.WARN); + /** + * Loads the data associated with the connection having the given + * identifier, preparing the interface for making modifications to that + * existing connection. + * + * @param {String} dataSource + * The unique identifier of the data source containing the connection to + * load. + * + * @param {String} identifier + * The identifier of the connection to load. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * editing the given connection. + */ + var loadExistingConnection = function loadExistingConnection(dataSource, identifier) { + return $q.all({ + connection : connectionService.getConnection(dataSource, identifier), + historyEntries : connectionService.getConnectionHistory(dataSource, identifier), + parameters : connectionService.getConnectionParameters(dataSource, identifier) + }) + .then(function connectionDataRetrieved(values) { - // Pull connection group hierarchy - connectionGroupService.getConnectionGroupTree( - $scope.selectedDataSource, - ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.ADMINISTER] - ) - .then(function connectionGroupReceived(rootGroup) { - $scope.rootGroup = rootGroup; - }, requestService.WARN); - - // Query the user's permissions for the current connection - permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) - .then(function permissionsReceived(permissions) { - - $scope.permissions = permissions; - - // Check if the connection is new or if the user has UPDATE permission - $scope.canSaveConnection = - !identifier - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); - - // Check if connection is not new and the user has DELETE permission - $scope.canDeleteConnection = - !!identifier && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier) - ); - - // Check if the connection is not new and the user has UPDATE and CREATE_CONNECTION permissions - $scope.canCloneConnection = - !!identifier && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || ( - PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier) - && PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) - ) - ); - - }, requestService.WARN); - - // Get protocol metadata - schemaService.getProtocols($scope.selectedDataSource) - .then(function protocolsReceived(protocols) { - $scope.protocols = protocols; - }, requestService.WARN); - - // Get history date format - $translate('MANAGE_CONNECTION.FORMAT_HISTORY_START').then(function historyDateFormatReceived(historyDateFormat) { - $scope.historyDateFormat = historyDateFormat; - }, angular.noop); - - // If we are editing an existing connection, pull its data - if (identifier) { - - // Pull data from existing connection - connectionService.getConnection($scope.selectedDataSource, identifier) - .then(function connectionRetrieved(connection) { - $scope.connection = connection; - }, requestService.WARN); - - // Pull connection history - connectionService.getConnectionHistory($scope.selectedDataSource, identifier) - .then(function historyReceived(historyEntries) { + $scope.connection = values.connection; + $scope.parameters = values.parameters; // Wrap all history entries for sake of display $scope.historyEntryWrappers = []; - historyEntries.forEach(function wrapHistoryEntry(historyEntry) { - $scope.historyEntryWrappers.push(new HistoryEntryWrapper(historyEntry)); + angular.forEach(values.historyEntries, function wrapHistoryEntry(historyEntry) { + $scope.historyEntryWrappers.push(new HistoryEntryWrapper(historyEntry)); }); - }, requestService.WARN); + }); + }; - // Pull connection parameters - connectionService.getConnectionParameters($scope.selectedDataSource, identifier) - .then(function parametersReceived(parameters) { - $scope.parameters = parameters; - }, requestService.WARN); - } - - // If we are cloning an existing connection, pull its data instead - else if (cloneSourceIdentifier) { + /** + * Loads the data associated with the connection having the given + * identifier, preparing the interface for cloning that existing + * connection. + * + * @param {String} dataSource + * The unique identifier of the data source containing the connection + * to be cloned. + * + * @param {String} identifier + * The identifier of the connection being cloned. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * cloning the given connection. + */ + var loadClonedConnection = function loadClonedConnection(dataSource, identifier) { + return $q.all({ + connection : connectionService.getConnection(dataSource, identifier), + parameters : connectionService.getConnectionParameters(dataSource, identifier) + }) + .then(function connectionDataRetrieved(values) { + + $scope.connection = values.connection; + $scope.parameters = values.parameters; - // Pull data from cloned connection - connectionService.getConnection($scope.selectedDataSource, cloneSourceIdentifier) - .then(function connectionRetrieved(connection) { - $scope.connection = connection; - // Clear the identifier field because this connection is new delete $scope.connection.identifier; - }, requestService.WARN); - // Do not pull connection history - $scope.historyEntryWrappers = []; - - // Pull connection parameters from cloned connection - connectionService.getConnectionParameters($scope.selectedDataSource, cloneSourceIdentifier) - .then(function parametersReceived(parameters) { - $scope.parameters = parameters; - }, requestService.WARN); - } + // Cloned connections have no history + $scope.historyEntryWrappers = []; - // If we are creating a new connection, populate skeleton connection data - else { + }); + }; + + /** + * Loads skeleton connection data, preparing the interface for creating a + * new connection. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * creating a new connection. + */ + var loadSkeletonConnection = function loadSkeletonConnection() { + + // Use skeleton connection object with no associated permissions, + // history, or parameters $scope.connection = new Connection({ protocol : 'vnc', parentIdentifier : $location.search().parent }); $scope.historyEntryWrappers = []; $scope.parameters = {}; - } - /** - * Returns whether the current user can change/set all connection - * attributes for the connection being edited, regardless of whether those - * attributes are already explicitly associated with that connection. - * - * @returns {Boolean} - * true if the current user can change all attributes for the - * connection being edited, regardless of whether those attributes are - * already explicitly associated with that connection, false otherwise. - */ - $scope.canChangeAllAttributes = function canChangeAllAttributes() { - - // All attributes can be set if we are creating the connection - return !identifier; + return $q.resolve(); }; + /** + * Loads the data requred for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing connection, cloning an existing + * connection, or creating an entirely new connection. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared + * for performing the requested management task. + */ + var loadRequestedConnection = function loadRequestedConnection() { + + // If we are editing an existing connection, pull its data + if (identifier) + return loadExistingConnection($scope.selectedDataSource, identifier); + + // If we are cloning an existing connection, pull its data instead + if (cloneSourceIdentifier) + return loadClonedConnection($scope.selectedDataSource, cloneSourceIdentifier); + + // If we are creating a new connection, populate skeleton connection data + return loadSkeletonConnection(); + + }; + + // Populate interface with requested data + $q.all({ + connectionData : loadRequestedConnection(), + attributes : schemaService.getConnectionAttributes($scope.selectedDataSource), + permissions : permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()), + protocols : schemaService.getProtocols($scope.selectedDataSource), + rootGroup : connectionGroupService.getConnectionGroupTree($scope.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) + }) + .then(function dataRetrieved(values) { + + $scope.attributes = values.attributes; + $scope.protocols = values.protocols; + $scope.rootGroup = values.rootGroup; + + $scope.managementPermissions = ManagementPermissions.fromPermissionSet( + values.permissions, + PermissionSet.SystemPermissionType.CREATE_CONNECTION, + PermissionSet.hasConnectionPermission, + identifier); + + }, requestService.WARN); + + // Get history date format + $translate('MANAGE_CONNECTION.FORMAT_HISTORY_START').then(function historyDateFormatReceived(historyDateFormat) { + $scope.historyDateFormat = historyDateFormat; + }, angular.noop); + /** * Returns the translation string namespace for the protocol having the * given name. The namespace will be of the form: @@ -353,9 +342,10 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }; /** - * Cancels all pending edits, returning to the management page. + * Cancels all pending edits, returning to the main list of connections + * within the selected data source. */ - $scope.cancel = function cancel() { + $scope.returnToConnectionList = function returnToConnectionList() { $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }; @@ -368,76 +358,33 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }; /** - * Saves the connection, creating a new connection or updating the existing - * connection. + * Saves the current connection, creating a new connection or updating the + * existing connection, returning a promise which is resolved if the save + * operation succeeds and rejected if the save operation fails. + * + * @returns {Promise} + * A promise which is resolved if the save operation succeeds and is + * rejected with an {@link Error} if the save operation fails. */ $scope.saveConnection = function saveConnection() { $scope.connection.parameters = $scope.parameters; // Save the connection - connectionService.saveConnection($scope.selectedDataSource, $scope.connection) - .then(function savedConnection() { - $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }, guacNotification.SHOW_REQUEST_ERROR); - - }; - - /** - * An action to be provided along with the object sent to showStatus which - * immediately deletes the current connection. - */ - var DELETE_ACTION = { - name : "MANAGE_CONNECTION.ACTION_DELETE", - className : "danger", - // Handle action - callback : function deleteCallback() { - deleteConnectionImmediately(); - guacNotification.showStatus(false); - } - }; - - /** - * An action to be provided along with the object sent to showStatus which - * closes the currently-shown status dialog. - */ - var CANCEL_ACTION = { - name : "MANAGE_CONNECTION.ACTION_CANCEL", - // Handle action - callback : function cancelCallback() { - guacNotification.showStatus(false); - } - }; - - /** - * Immediately deletes the current connection, without prompting the user - * for confirmation. - */ - var deleteConnectionImmediately = function deleteConnectionImmediately() { - - // Delete the connection - connectionService.deleteConnection($scope.selectedDataSource, $scope.connection) - .then(function deletedConnection() { - $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }, guacNotification.SHOW_REQUEST_ERROR); + return connectionService.saveConnection($scope.selectedDataSource, $scope.connection); }; /** - * Deletes the connection, prompting the user first to confirm that - * deletion is desired. + * Deletes the current connection, returning a promise which is resolved if + * the delete operation succeeds and rejected if the delete operation fails. + * + * @returns {Promise} + * A promise which is resolved if the delete operation succeeds and is + * rejected with an {@link Error} if the delete operation fails. */ $scope.deleteConnection = function deleteConnection() { - - // Confirm deletion request - guacNotification.showStatus({ - 'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_CONFIRM_DELETE', - 'text' : { - key : 'MANAGE_CONNECTION.TEXT_CONFIRM_DELETE' - }, - 'actions' : [ DELETE_ACTION, CANCEL_ACTION] - }); - + return connectionService.deleteConnection($scope.selectedDataSource, $scope.connection); }; }]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manageConnection.html b/guacamole/src/main/webapp/app/manage/templates/manageConnection.html index ed103351d..e78051373 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageConnection.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageConnection.html @@ -41,7 +41,7 @@
+ model="connection.attributes" model-only="!managementPermissions.canChangeAllAttributes">
@@ -53,12 +53,13 @@
-
- - - - -
+ +

{{'MANAGE_CONNECTION.SECTION_HEADER_HISTORY' | translate}}

From 7e1dbf7d11f51f7acd8b3ad39e30695bbda15abd Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 11:54:38 -0700 Subject: [PATCH 07/20] GUACAMOLE-220: Migrate connection group management screen to common buttons and permission logic. Add required clone option. --- .../manageConnectionGroupController.js | 343 +++++++++--------- .../templates/manageConnectionGroup.html | 14 +- .../src/main/webapp/translations/en.json | 1 + 3 files changed, 185 insertions(+), 173 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js index de29aff12..842c3954d 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js @@ -24,15 +24,16 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' function manageConnectionGroupController($scope, $injector) { // Required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var PermissionSet = $injector.get('PermissionSet'); + var ConnectionGroup = $injector.get('ConnectionGroup'); + var ManagementPermissions = $injector.get('ManagementPermissions'); + var PermissionSet = $injector.get('PermissionSet'); // Required services var $location = $injector.get('$location'); + var $q = $injector.get('$q'); var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); - var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var requestService = $injector.get('requestService'); var schemaService = $injector.get('schemaService'); @@ -45,6 +46,15 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' */ $scope.selectedDataSource = $routeParams.dataSource; + /** + * The identifier of the original connection group from which this + * connection group is being cloned. Only valid if this is a new + * connection group. + * + * @type String + */ + var cloneSourceIdentifier = $location.search().clone; + /** * The identifier of the connection group being edited. If a new connection * group is being created, this will not be defined. @@ -53,6 +63,23 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' */ var identifier = $routeParams.id; + /** + * Available connection group types, as translation string / internal value + * pairs. + * + * @type Object[] + */ + $scope.types = [ + { + label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_ORGANIZATIONAL", + value: ConnectionGroup.Type.ORGANIZATIONAL + }, + { + label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_BALANCING", + value : ConnectionGroup.Type.BALANCING + } + ]; + /** * The root connection group of the connection group hierarchy. * @@ -68,26 +95,13 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' $scope.connectionGroup = null; /** - * Whether the user has UPDATE permission for the current connection group. - * - * @type Boolean - */ - $scope.hasUpdatePermission = null; - - /** - * Whether the user has DELETE permission for the current connection group. - * - * @type Boolean - */ - $scope.hasDeletePermission = null; - - /** - * All permissions associated with the current user, or null if the user's - * permissions have not yet been loaded. + * The managment-related actions that the current user may perform on the + * connection group currently being created/modified, or null if the current + * user's permissions have not yet been loaded. * - * @type PermissionSet + * @type ManagementPermissions */ - $scope.permissions = null; + $scope.managementPermissions = null; /** * All available connection group attributes. This is only the set of @@ -109,177 +123,172 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' return $scope.rootGroup !== null && $scope.connectionGroup !== null - && $scope.permissions !== null - && $scope.attributes !== null - && $scope.canSaveConnectionGroup !== null - && $scope.canDeleteConnectionGroup !== null; + && $scope.managementPermissions !== null + && $scope.attributes !== null; }; - - // Pull connection group attribute schema - schemaService.getConnectionGroupAttributes($scope.selectedDataSource) - .then(function attributesReceived(attributes) { - $scope.attributes = attributes; - }, requestService.WARN); - // Query the user's permissions for the current connection group - permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) - .then(function permissionsReceived(permissions) { - - $scope.permissions = permissions; - - // Check if the connection group is new or if the user has UPDATE permission - $scope.canSaveConnectionGroup = - !identifier - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); - - // Check if connection group is not new and the user has DELETE permission - $scope.canDeleteConnectionGroup = - !!identifier && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier) - ); - - }, requestService.WARN); - - - // Pull connection group hierarchy - connectionGroupService.getConnectionGroupTree( - $scope.selectedDataSource, - ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.ADMINISTER] - ) - .then(function connectionGroupReceived(rootGroup) { - $scope.rootGroup = rootGroup; - }, requestService.WARN); - - // If we are editing an existing connection group, pull its data - if (identifier) { - connectionGroupService.getConnectionGroup($scope.selectedDataSource, identifier) + /** + * Loads the data associated with the connection group having the given + * identifier, preparing the interface for making modifications to that + * existing connection group. + * + * @param {String} dataSource + * The unique identifier of the data source containing the connection + * group to load. + * + * @param {String} identifier + * The identifier of the connection group to load. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * editing the given connection group. + */ + var loadExistingConnectionGroup = function loadExistingConnectionGroup(dataSource, identifier) { + return connectionGroupService.getConnectionGroup( + dataSource, + identifier + ) .then(function connectionGroupReceived(connectionGroup) { $scope.connectionGroup = connectionGroup; - }, requestService.WARN); - } + }); + }; - // If we are creating a new connection group, populate skeleton connection group data - else + /** + * Loads the data associated with the connection group having the given + * identifier, preparing the interface for cloning that existing + * connection group. + * + * @param {String} dataSource + * The unique identifier of the data source containing the connection + * group to be cloned. + * + * @param {String} identifier + * The identifier of the connection group being cloned. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * cloning the given connection group. + */ + var loadClonedConnectionGroup = function loadClonedConnectionGroup(dataSource, identifier) { + return connectionGroupService.getConnectionGroup( + dataSource, + identifier + ) + .then(function connectionGroupReceived(connectionGroup) { + $scope.connectionGroup = connectionGroup; + delete $scope.connectionGroup.identifier; + }); + }; + + /** + * Loads skeleton connection group data, preparing the interface for + * creating a new connection group. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * creating a new connection group. + */ + var loadSkeletonConnectionGroup = function loadSkeletonConnectionGroup() { + + // Use skeleton connection group object with specified parent $scope.connectionGroup = new ConnectionGroup({ parentIdentifier : $location.search().parent }); - /** - * Available connection group types, as translation string / internal value - * pairs. - * - * @type Object[] - */ - $scope.types = [ - { - label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_ORGANIZATIONAL", - value: ConnectionGroup.Type.ORGANIZATIONAL - }, - { - label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_BALANCING", - value : ConnectionGroup.Type.BALANCING - } - ]; - - /** - * Returns whether the current user can change/set all connection group - * attributes for the connection group being edited, regardless of whether - * those attributes are already explicitly associated with that connection - * group. - * - * @returns {Boolean} - * true if the current user can change all attributes for the - * connection group being edited, regardless of whether those - * attributes are already explicitly associated with that connection - * group, false otherwise. - */ - $scope.canChangeAllAttributes = function canChangeAllAttributes() { - - // All attributes can be set if we are creating the connection group - return !identifier; + return $q.resolve(); }; /** - * Cancels all pending edits, returning to the management page. + * Loads the data requred for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing connection group, cloning an + * existing connection group, or creating an entirely new connection group. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared + * for performing the requested management task. */ - $scope.cancel = function cancel() { + var loadRequestedConnectionGroup = function loadRequestedConnectionGroup() { + + // If we are editing an existing connection group, pull its data + if (identifier) + return loadExistingConnectionGroup($scope.selectedDataSource, identifier); + + // If we are cloning an existing connection group, pull its data + // instead + if (cloneSourceIdentifier) + return loadClonedConnectionGroup($scope.selectedDataSource, cloneSourceIdentifier); + + // If we are creating a new connection group, populate skeleton + // connection group data + return loadSkeletonConnectionGroup(); + + }; + + // Query the user's permissions for the current connection group + $q.all({ + connectionGroupData : loadRequestedConnectionGroup(), + attributes : schemaService.getConnectionGroupAttributes($scope.selectedDataSource), + permissions : permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()), + rootGroup : connectionGroupService.getConnectionGroupTree($scope.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) + }) + .then(function connectionGroupDataRetrieved(values) { + + $scope.attributes = values.attributes; + $scope.rootGroup = values.rootGroup; + + $scope.managementPermissions = ManagementPermissions.fromPermissionSet( + values.permissions, + PermissionSet.SystemPermissionType.CREATE_CONNECTION, + PermissionSet.hasConnectionPermission, + identifier); + + }, requestService.WARN); + + /** + * Cancels all pending edits, returning to the main list of connections + * within the selected data source. + */ + $scope.returnToConnectionList = function returnToConnectionList() { $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }; - + /** - * Saves the connection group, creating a new connection group or updating - * the existing connection group. + * Cancels all pending edits, opening an edit page for a new connection + * group which is prepopulated with the data from the connection group + * currently being edited. + */ + $scope.cloneConnectionGroup = function cloneConnectionGRoup() { + $location.path('/manage/' + encodeURIComponent($scope.selectedDataSource) + '/connectionGroups').search('clone', identifier); + }; + + /** + * Saves the current connection group, creating a new connection group or + * updating the existing connection group, returning a promise which is + * resolved if the save operation succeeds and rejected if the save + * operation fails. + * + * @returns {Promise} + * A promise which is resolved if the save operation succeeds and is + * rejected with an {@link Error} if the save operation fails. */ $scope.saveConnectionGroup = function saveConnectionGroup() { - - // Save the connection - connectionGroupService.saveConnectionGroup($scope.selectedDataSource, $scope.connectionGroup) - .then(function savedConnectionGroup() { - $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }, guacNotification.SHOW_REQUEST_ERROR); - + return connectionGroupService.saveConnectionGroup($scope.selectedDataSource, $scope.connectionGroup); }; /** - * An action to be provided along with the object sent to showStatus which - * immediately deletes the current connection group. - */ - var DELETE_ACTION = { - name : "MANAGE_CONNECTION_GROUP.ACTION_DELETE", - className : "danger", - // Handle action - callback : function deleteCallback() { - deleteConnectionGroupImmediately(); - guacNotification.showStatus(false); - } - }; - - /** - * An action to be provided along with the object sent to showStatus which - * closes the currently-shown status dialog. - */ - var CANCEL_ACTION = { - name : "MANAGE_CONNECTION_GROUP.ACTION_CANCEL", - // Handle action - callback : function cancelCallback() { - guacNotification.showStatus(false); - } - }; - - /** - * Immediately deletes the current connection group, without prompting the - * user for confirmation. - */ - var deleteConnectionGroupImmediately = function deleteConnectionGroupImmediately() { - - // Delete the connection group - connectionGroupService.deleteConnectionGroup($scope.selectedDataSource, $scope.connectionGroup) - .then(function deletedConnectionGroup() { - $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }, guacNotification.SHOW_REQUEST_ERROR); - - }; - - /** - * Deletes the connection group, prompting the user first to confirm that - * deletion is desired. + * Deletes the current connection group, returning a promise which is + * resolved if the delete operation succeeds and rejected if the delete + * operation fails. + * + * @returns {Promise} + * A promise which is resolved if the delete operation succeeds and is + * rejected with an {@link Error} if the delete operation fails. */ $scope.deleteConnectionGroup = function deleteConnectionGroup() { - - // Confirm deletion request - guacNotification.showStatus({ - 'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_CONFIRM_DELETE', - 'text' : { - key : 'MANAGE_CONNECTION_GROUP.TEXT_CONFIRM_DELETE' - }, - 'actions' : [ DELETE_ACTION, CANCEL_ACTION] - }); - + return connectionGroupService.deleteConnectionGroup($scope.selectedDataSource, $scope.connectionGroup); }; }]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manageConnectionGroup.html b/guacamole/src/main/webapp/app/manage/templates/manageConnectionGroup.html index d4c6613a7..926dc1152 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageConnectionGroup.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageConnectionGroup.html @@ -41,14 +41,16 @@
+ model="connectionGroup.attributes" model-only="!managementPermissions.canChangeAllAttributes">
-
- - - -
+ +
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index a97406eb2..ca8acb8e1 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -234,6 +234,7 @@ "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", From c1f5ad407504c42f42158ea38e9da04e3842f3c1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 12:24:33 -0700 Subject: [PATCH 08/20] GUACAMOLE-220: Migrate sharing profile management screen to common buttons and permission logic. --- .../manageSharingProfileController.js | 363 ++++++++---------- .../templates/manageSharingProfile.html | 15 +- 2 files changed, 165 insertions(+), 213 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js index 7414e653a..a0a683c32 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js @@ -24,33 +24,22 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', function manageSharingProfileController($scope, $injector) { // Required types - var SharingProfile = $injector.get('SharingProfile'); - var PermissionSet = $injector.get('PermissionSet'); + var ManagementPermissions = $injector.get('ManagementPermissions'); + var SharingProfile = $injector.get('SharingProfile'); + var PermissionSet = $injector.get('PermissionSet'); // Required services var $location = $injector.get('$location'); + var $q = $injector.get('$q'); var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); var connectionService = $injector.get('connectionService'); - var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var requestService = $injector.get('requestService'); var schemaService = $injector.get('schemaService'); var sharingProfileService = $injector.get('sharingProfileService'); var translationStringService = $injector.get('translationStringService'); - /** - * An action to be provided along with the object sent to showStatus which - * closes the currently-shown status dialog, effectively canceling the - * operation which was pending user confirmation. - */ - var CANCEL_ACTION = { - name : "MANAGE_SHARING_PROFILE.ACTION_CANCEL", - callback : function cancelCallback() { - guacNotification.showStatus(false); - } - }; - /** * The unique identifier of the data source containing the sharing profile * being edited. @@ -98,34 +87,13 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', $scope.parameters = null; /** - * Whether the user can save the sharing profile being edited. This could be - * updating an existing sharing profile, or creating a new sharing profile. + * The managment-related actions that the current user may perform on the + * sharing profile currently being created/modified, or null if the current + * user's permissions have not yet been loaded. * - * @type Boolean + * @type ManagementPermissions */ - $scope.canSaveSharingProfile = null; - - /** - * Whether the user can delete the sharing profile being edited. - * - * @type Boolean - */ - $scope.canDeleteSharingProfile = null; - - /** - * Whether the user can clone the sharing profile being edited. - * - * @type Boolean - */ - $scope.canCloneSharingProfile = null; - - /** - * All permissions associated with the current user, or null if the user's - * permissions have not yet been loaded. - * - * @type PermissionSet - */ - $scope.permissions = null; + $scope.managementPermissions = null; /** * All available sharing profile attributes. This is only the set of @@ -149,146 +117,157 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', && $scope.sharingProfile !== null && $scope.primaryConnection !== null && $scope.parameters !== null - && $scope.permissions !== null - && $scope.attributes !== null - && $scope.canSaveSharingProfile !== null - && $scope.canDeleteSharingProfile !== null - && $scope.canCloneSharingProfile !== null; + && $scope.managementPermissions !== null + && $scope.attributes !== null; }; - // Pull sharing profile attribute schema - schemaService.getSharingProfileAttributes($scope.selectedDataSource) - .then(function attributesReceived(attributes) { - $scope.attributes = attributes; - }, requestService.WARN); + /** + * Loads the data associated with the sharing profile having the given + * identifier, preparing the interface for making modifications to that + * existing sharing profile. + * + * @param {String} dataSource + * The unique identifier of the data source containing the sharing + * profile to load. + * + * @param {String} identifier + * The identifier of the sharing profile to load. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * editing the given sharing profile. + */ + var loadExistingSharingProfile = function loadExistingSharingProfile(dataSource, identifier) { + return $q.all({ + sharingProfile : sharingProfileService.getSharingProfile(dataSource, identifier), + parameters : sharingProfileService.getSharingProfileParameters(dataSource, identifier) + }) + .then(function sharingProfileDataRetrieved(values) { - // Query the user's permissions for the current sharing profile - permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) - .then(function permissionsReceived(permissions) { + $scope.sharingProfile = values.sharingProfile; + $scope.parameters = values.parameters; - $scope.permissions = permissions; + // Load connection object for associated primary connection + return connectionService.getConnection( + dataSource, + values.sharingProfile.primaryConnectionIdentifier + ) + .then(function connectionRetrieved(connection) { + $scope.primaryConnection = connection; + }); - // The sharing profile can be saved if it is new or if the user has - // UPDATE permission for that sharing profile (either explicitly or by - // virtue of being an administrator) - $scope.canSaveSharingProfile = - !identifier - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSharingProfilePermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); + }); + }; - // The sharing profile can be saved only if it exists and the user has - // DELETE permission (either explicitly or by virtue of being an - // administrator) - $scope.canDeleteSharingProfile = - !!identifier && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSharingProfilePermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier) - ); + /** + * Loads the data associated with the sharing profile having the given + * identifier, preparing the interface for cloning that existing + * sharing profile. + * + * @param {String} dataSource + * The unique identifier of the data source containing the sharing + * profile to be cloned. + * + * @param {String} identifier + * The identifier of the sharing profile being cloned. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * cloning the given sharing profile. + */ + var loadClonedSharingProfile = function loadClonedSharingProfile(dataSource, identifier) { + return $q.all({ + sharingProfile : sharingProfileService.getSharingProfile(dataSource, identifier), + parameters : sharingProfileService.getSharingProfileParameters(dataSource, identifier) + }) + .then(function sharingProfileDataRetrieved(values) { - // The sharing profile can be cloned only if it exists, the user has - // UPDATE permission on the sharing profile being cloned (is able to - // read parameters), and the user can create new sharing profiles - $scope.canCloneSharingProfile = - !!identifier && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || ( - PermissionSet.hasSharingProfilePermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier) - && PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_SHARING_PROFILE) - ) - ); - - }, requestService.WARN); - - // Get protocol metadata - schemaService.getProtocols($scope.selectedDataSource) - .then(function protocolsReceived(protocols) { - $scope.protocols = protocols; - }, requestService.WARN); - - // If we are editing an existing sharing profile, pull its data - if (identifier) { - - // Pull data from existing sharing profile - sharingProfileService.getSharingProfile($scope.selectedDataSource, identifier) - .then(function sharingProfileRetrieved(sharingProfile) { - $scope.sharingProfile = sharingProfile; - }, requestService.WARN); - - // Pull sharing profile parameters - sharingProfileService.getSharingProfileParameters($scope.selectedDataSource, identifier) - .then(function parametersReceived(parameters) { - $scope.parameters = parameters; - }, requestService.WARN); - - } - - // If we are cloning an existing sharing profile, pull its data instead - else if (cloneSourceIdentifier) { - - // Pull data from cloned sharing profile - sharingProfileService.getSharingProfile($scope.selectedDataSource, cloneSourceIdentifier) - .then(function sharingProfileRetrieved(sharingProfile) { - - // Store data of sharing profile being cloned - $scope.sharingProfile = sharingProfile; + $scope.sharingProfile = values.sharingProfile; + $scope.parameters = values.parameters; // Clear the identifier field because this sharing profile is new delete $scope.sharingProfile.identifier; - }, requestService.WARN); + // Load connection object for associated primary connection + return connectionService.getConnection( + dataSource, + values.sharingProfile.primaryConnectionIdentifier + ) + .then(function connectionRetrieved(connection) { + $scope.primaryConnection = connection; + }); - // Pull sharing profile parameters from cloned sharing profile - sharingProfileService.getSharingProfileParameters($scope.selectedDataSource, cloneSourceIdentifier) - .then(function parametersReceived(parameters) { - $scope.parameters = parameters; - }, requestService.WARN); + }); + }; - } - - // If we are creating a new sharing profile, populate skeleton sharing - // profile data - else { + /** + * Loads skeleton sharing profile data, preparing the interface for + * creating a new sharing profile. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared for + * creating a new sharing profile. + */ + var loadSkeletonSharingProfile = function loadSkeletonSharingProfile() { + // Use skeleton sharing profile object with no associated parameters $scope.sharingProfile = new SharingProfile({ primaryConnectionIdentifier : $location.search().parent }); - $scope.parameters = {}; - } - - // Populate primary connection once its identifier is known - $scope.$watch('sharingProfile.primaryConnectionIdentifier', - function retrievePrimaryConnection(identifier) { - - if (identifier) { - connectionService.getConnection($scope.selectedDataSource, identifier) - .then(function connectionRetrieved(connection) { - $scope.primaryConnection = connection; - }, requestService.WARN); - } - - }); - - /** - * Returns whether the current user can change/set all sharing profile - * attributes for the sharing profile being edited, regardless of whether - * those attributes are already explicitly associated with that sharing - * profile. - * - * @returns {Boolean} - * true if the current user can change all attributes for the sharing - * profile being edited, regardless of whether those attributes are - * already explicitly associated with that sharing profile, false - * otherwise. - */ - $scope.canChangeAllAttributes = function canChangeAllAttributes() { - - // All attributes can be set if we are creating the sharing profile - return !identifier; + return $q.resolve(); }; + /** + * Loads the data requred for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing sharing profile, cloning an + * existing sharing profile, or creating an entirely new sharing profile. + * + * @returns {Promise} + * A promise which is resolved when the interface has been prepared + * for performing the requested management task. + */ + var loadRequestedSharingProfile = function loadRequestedSharingProfile() { + + // If we are editing an existing sharing profile, pull its data + if (identifier) + return loadExistingSharingProfile($scope.selectedDataSource, identifier); + + // If we are cloning an existing sharing profile, pull its data instead + if (cloneSourceIdentifier) + return loadClonedSharingProfile($scope.selectedDataSource, cloneSourceIdentifier); + + // If we are creating a new sharing profile, populate skeleton sharing + // profile data + return loadSkeletonSharingProfile(); + + }; + + + // Query the user's permissions for the current sharing profile + $q.all({ + sharingProfileData : loadRequestedSharingProfile(), + attributes : schemaService.getSharingProfileAttributes($scope.selectedDataSource), + protocols : schemaService.getProtocols($scope.selectedDataSource), + permissions : permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) + }) + .then(function sharingProfileDataRetrieved(values) { + + $scope.attributes = values.attributes; + $scope.protocols = values.protocols; + + $scope.managementPermissions = ManagementPermissions.fromPermissionSet( + values.permissions, + PermissionSet.SystemPermissionType.CREATE_CONNECTION, + PermissionSet.hasConnectionPermission, + identifier); + + }, requestService.WARN); + /** * Returns the translation string namespace for the protocol having the * given name. The namespace will be of the form: @@ -316,9 +295,10 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', }; /** - * Cancels all pending edits, returning to the management page. + * Cancels all pending edits, returning to the main list of connections + * within the selected data source. */ - $scope.cancel = function cancel() { + $scope.returnToConnectionList = function returnToConnectionList() { $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }; @@ -332,64 +312,35 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', }; /** - * Saves the sharing profile, creating a new sharing profile or updating - * the existing sharing profile. + * Saves the current sharing profile, creating a new sharing profile or + * updating the existing sharing profile, returning a promise which is + * resolved if the save operation succeeds and rejected if the save + * operation fails. + * + * @returns {Promise} + * A promise which is resolved if the save operation succeeds and is + * rejected with an {@link Error} if the save operation fails. */ $scope.saveSharingProfile = function saveSharingProfile() { $scope.sharingProfile.parameters = $scope.parameters; // Save the sharing profile - sharingProfileService.saveSharingProfile($scope.selectedDataSource, $scope.sharingProfile) - .then(function savedSharingProfile() { - $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }, guacNotification.SHOW_REQUEST_ERROR); + return sharingProfileService.saveSharingProfile($scope.selectedDataSource, $scope.sharingProfile); }; /** - * An action to be provided along with the object sent to showStatus which - * immediately deletes the current sharing profile. - */ - var DELETE_ACTION = { - name : "MANAGE_SHARING_PROFILE.ACTION_DELETE", - className : "danger", - // Handle action - callback : function deleteCallback() { - deleteSharingProfileImmediately(); - guacNotification.showStatus(false); - } - }; - - /** - * Immediately deletes the current sharing profile, without prompting the - * user for confirmation. - */ - var deleteSharingProfileImmediately = function deleteSharingProfileImmediately() { - - // Delete the sharing profile - sharingProfileService.deleteSharingProfile($scope.selectedDataSource, $scope.sharingProfile) - .then(function deletedSharingProfile() { - $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }, guacNotification.SHOW_REQUEST_ERROR); - - }; - - /** - * Deletes the sharing profile, prompting the user first to confirm that - * deletion is desired. + * Deletes the current sharing profile, returning a promise which is + * resolved if the delete operation succeeds and rejected if the delete + * operation fails. + * + * @returns {Promise} + * A promise which is resolved if the delete operation succeeds and is + * rejected with an {@link Error} if the delete operation fails. */ $scope.deleteSharingProfile = function deleteSharingProfile() { - - // Confirm deletion request - guacNotification.showStatus({ - 'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_CONFIRM_DELETE', - 'text' : { - 'key' : 'MANAGE_SHARING_PROFILE.TEXT_CONFIRM_DELETE' - }, - 'actions' : [ DELETE_ACTION, CANCEL_ACTION] - }); - + return sharingProfileService.deleteSharingProfile($scope.selectedDataSource, $scope.sharingProfile); }; }]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html b/guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html index d6c704332..ac52fa356 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageSharingProfile.html @@ -22,7 +22,7 @@
+ model="sharingProfile.attributes" model-only="!managementPermissions.canChangeAllAttributes">
@@ -34,11 +34,12 @@ -
- - - - -
+ + From 82803c914831bd860f1c9c045e0c5177042c6e24 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 12:42:36 -0700 Subject: [PATCH 09/20] GUACAMOLE-220: Move common protocol namespace/name retrieval to Protocol class. --- .../controllers/manageConnectionController.js | 47 ++-------------- .../manageSharingProfileController.js | 27 ++-------- .../src/main/webapp/app/rest/restModule.js | 3 +- .../main/webapp/app/rest/types/Protocol.js | 54 ++++++++++++++++++- 4 files changed, 62 insertions(+), 69 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js index 7cd08ac01..ac55f11ca 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js @@ -29,6 +29,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i var HistoryEntryWrapper = $injector.get('HistoryEntryWrapper'); var ManagementPermissions = $injector.get('ManagementPermissions'); var PermissionSet = $injector.get('PermissionSet'); + var Protocol = $injector.get('Protocol'); // Required services var $location = $injector.get('$location'); @@ -41,7 +42,6 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i var permissionService = $injector.get('permissionService'); var requestService = $injector.get('requestService'); var schemaService = $injector.get('schemaService'); - var translationStringService = $injector.get('translationStringService'); /** * The unique identifier of the data source containing the connection being @@ -295,51 +295,14 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }, angular.noop); /** - * Returns the translation string namespace for the protocol having the - * given name. The namespace will be of the form: - * - * PROTOCOL_NAME - * - * where NAME is the protocol name transformed via - * translationStringService.canonicalize(). - * - * @param {String} protocolName - * The name of the protocol. - * - * @returns {String} - * The translation namespace for the protocol specified, or null if no - * namespace could be generated. + * @borrows Protocol.getNamespace */ - $scope.getNamespace = function getNamespace(protocolName) { - - // Do not generate a namespace if no protocol is selected - if (!protocolName) - return null; - - return 'PROTOCOL_' + translationStringService.canonicalize(protocolName); - - }; + $scope.getNamespace = Protocol.getNamespace; /** - * Given the internal name of a protocol, produces the translation string - * for the localized version of that protocol's name. The translation - * string will be of the form: - * - * NAMESPACE.NAME - * - * where NAMESPACE is the namespace generated from - * $scope.getNamespace(). - * - * @param {String} protocolName - * The name of the protocol. - * - * @returns {String} - * The translation string which produces the localized name of the - * protocol specified. + * @borrows Protocol.getName */ - $scope.getProtocolName = function getProtocolName(protocolName) { - return $scope.getNamespace(protocolName) + '.NAME'; - }; + $scope.getProtocolName = Protocol.getName; /** * Cancels all pending edits, returning to the main list of connections diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js index a0a683c32..940c30509 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js @@ -27,6 +27,7 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', var ManagementPermissions = $injector.get('ManagementPermissions'); var SharingProfile = $injector.get('SharingProfile'); var PermissionSet = $injector.get('PermissionSet'); + var Protocol = $injector.get('Protocol'); // Required services var $location = $injector.get('$location'); @@ -38,7 +39,6 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', var requestService = $injector.get('requestService'); var schemaService = $injector.get('schemaService'); var sharingProfileService = $injector.get('sharingProfileService'); - var translationStringService = $injector.get('translationStringService'); /** * The unique identifier of the data source containing the sharing profile @@ -269,30 +269,9 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', }, requestService.WARN); /** - * Returns the translation string namespace for the protocol having the - * given name. The namespace will be of the form: - * - * PROTOCOL_NAME - * - * where NAME is the protocol name transformed via - * translationStringService.canonicalize(). - * - * @param {String} protocolName - * The name of the protocol. - * - * @returns {String} - * The translation namespace for the protocol specified, or null if no - * namespace could be generated. + * @borrows Protocol.getNamespace */ - $scope.getNamespace = function getNamespace(protocolName) { - - // Do not generate a namespace if no protocol is selected - if (!protocolName) - return null; - - return 'PROTOCOL_' + translationStringService.canonicalize(protocolName); - - }; + $scope.getNamespace = Protocol.getNamespace; /** * Cancels all pending edits, returning to the main list of connections diff --git a/guacamole/src/main/webapp/app/rest/restModule.js b/guacamole/src/main/webapp/app/rest/restModule.js index f409e9545..23901c07c 100644 --- a/guacamole/src/main/webapp/app/rest/restModule.js +++ b/guacamole/src/main/webapp/app/rest/restModule.js @@ -22,5 +22,6 @@ * Guacamole web application. */ angular.module('rest', [ - 'auth' + 'auth', + 'locale' ]); diff --git a/guacamole/src/main/webapp/app/rest/types/Protocol.js b/guacamole/src/main/webapp/app/rest/types/Protocol.js index cfb26f02d..0b86d5b2d 100644 --- a/guacamole/src/main/webapp/app/rest/types/Protocol.js +++ b/guacamole/src/main/webapp/app/rest/types/Protocol.js @@ -20,8 +20,11 @@ /** * Service which defines the Protocol class. */ -angular.module('rest').factory('Protocol', [function defineProtocol() { - +angular.module('rest').factory('Protocol', ['$injector', function defineProtocol($injector) { + + // Required services + var translationStringService = $injector.get('translationStringService'); + /** * The object returned by REST API calls when representing the data * associated with a supported remote desktop protocol. @@ -64,6 +67,53 @@ angular.module('rest').factory('Protocol', [function defineProtocol() { }; + /** + * Returns the translation string namespace for the protocol having the + * given name. The namespace will be of the form: + * + * PROTOCOL_NAME + * + * where NAME is the protocol name transformed via + * translationStringService.canonicalize(). + * + * @param {String} protocolName + * The name of the protocol. + * + * @returns {String} + * The translation namespace for the protocol specified, or null if no + * namespace could be generated. + */ + Protocol.getNamespace = function getNamespace(protocolName) { + + // Do not generate a namespace if no protocol is selected + if (!protocolName) + return null; + + return 'PROTOCOL_' + translationStringService.canonicalize(protocolName); + + }; + + /** + * Given the internal name of a protocol, produces the translation string + * for the localized version of that protocol's name. The translation + * string will be of the form: + * + * NAMESPACE.NAME + * + * where NAMESPACE is the namespace generated from + * $scope.getNamespace(). + * + * @param {String} protocolName + * The name of the protocol. + * + * @returns {String} + * The translation string which produces the localized name of the + * protocol specified. + */ + Protocol.getName = function getProtocolName(protocolName) { + return Protocol.getNamespace(protocolName) + '.NAME'; + }; + return Protocol; }]); \ No newline at end of file From 708c255b8373167806a439332d029ae0f07ef4e5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 16:29:25 -0700 Subject: [PATCH 10/20] GUACAMOLE-220: "Management", not "managment". --- .../webapp/app/manage/controllers/manageConnectionController.js | 2 +- .../app/manage/controllers/manageConnectionGroupController.js | 2 +- .../app/manage/controllers/manageSharingProfileController.js | 2 +- .../main/webapp/app/manage/controllers/manageUserController.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js index ac55f11ca..6139781d2 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js @@ -111,7 +111,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i $scope.historyEntryWrappers = null; /** - * The managment-related actions that the current user may perform on the + * The management-related actions that the current user may perform on the * connection currently being created/modified, or null if the current * user's permissions have not yet been loaded. * diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js index 842c3954d..1a4b64e32 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js @@ -95,7 +95,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' $scope.connectionGroup = null; /** - * The managment-related actions that the current user may perform on the + * The management-related actions that the current user may perform on the * connection group currently being created/modified, or null if the current * user's permissions have not yet been loaded. * diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js index 940c30509..7385aa16f 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js @@ -87,7 +87,7 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', $scope.parameters = null; /** - * The managment-related actions that the current user may perform on the + * The management-related actions that the current user may perform on the * sharing profile currently being created/modified, or null if the current * user's permissions have not yet been loaded. * diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index b1f4f2ec3..dcb87e156 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -136,7 +136,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissionsRemoved = new PermissionSet(); /** - * The managment-related actions that the current user may perform on the + * The management-related actions that the current user may perform on the * user currently being created/modified, or null if the current user's * permissions have not yet been loaded. * From 1989c11dd98ac76258d44c001bdf6e362be4f871 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 16:39:02 -0700 Subject: [PATCH 11/20] GUACAMOLE-220: Include the identifier of the associated object within ManagementPermissions. --- .../webapp/app/manage/types/ManagementPermissions.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js b/guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js index f9a78b7dc..5d02a76f0 100644 --- a/guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js +++ b/guacamole/src/main/webapp/app/manage/types/ManagementPermissions.js @@ -37,6 +37,14 @@ angular.module('manage').factory('ManagementPermissions', ['$injector', */ var ManagementPermissions = function ManagementPermissions(template) { + /** + * The identifier of the associated object, or null if the object does + * not yet exist. + * + * @type String + */ + this.identifier = template.identifier || null; + /** * Whether the user can save the associated object. This could be * updating an existing object, or creating a new object. @@ -129,6 +137,8 @@ angular.module('manage').factory('ManagementPermissions', ['$injector', return new ManagementPermissions({ + identifier : identifier, + // A user can save (create or update) an object if they are a // system-level administrator, OR the object does not yet exist and // the user has explicit permission to create such objects, OR the From 6ca076f7fcc59e4d499fa6b15277907f9a88bf8a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 17:18:48 -0700 Subject: [PATCH 12/20] GUACAMOLE-220: Move data source tabs to separate directive. --- .../controllers/manageUserController.js | 70 ++++------- .../app/manage/directives/dataSourceTabs.js | 111 ++++++++++++++++++ .../app/manage/templates/dataSourceTabs.html | 3 + .../app/manage/templates/manageUser.html | 22 ++-- 4 files changed, 152 insertions(+), 54 deletions(-) create mode 100644 guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js create mode 100644 guacamole/src/main/webapp/app/manage/templates/dataSourceTabs.html diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index dcb87e156..27cb9337f 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -26,7 +26,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // Required types var Error = $injector.get('Error'); var ManagementPermissions = $injector.get('ManagementPermissions'); - var PageDefinition = $injector.get('PageDefinition'); var PermissionFlagSet = $injector.get('PermissionFlagSet'); var PermissionSet = $injector.get('PermissionSet'); var User = $injector.get('User'); @@ -40,7 +39,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto var permissionService = $injector.get('permissionService'); var requestService = $injector.get('requestService'); var schemaService = $injector.get('schemaService'); - var translationStringService = $injector.get('translationStringService'); var userService = $injector.get('userService'); /** @@ -136,11 +134,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissionsRemoved = new PermissionSet(); /** - * The management-related actions that the current user may perform on the - * user currently being created/modified, or null if the current user's - * permissions have not yet been loaded. + * For each applicable data source, the management-related actions that the + * current user may perform on the user account currently being created + * or modified, as a map of data source identifier to the + * {@link ManagementPermissions} object describing the actions available + * within that data source, or null if the current user's permissions have + * not yet been loaded. * - * @type ManagementPermissions + * @type Object. */ $scope.managementPermissions = null; @@ -153,14 +154,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ $scope.attributes = null; - /** - * The pages associated with each user account having the given username. - * Each user account will be associated with a particular data source. - * - * @type PageDefinition[] - */ - $scope.accountPages = []; - /** * Returns whether critical data has completed being loaded. * @@ -349,53 +342,42 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }) .then(function dataReceived(values) { - var managementPermissions = {}; - $scope.attributes = values.attributes; - // Generate pages for each applicable data source - $scope.accountPages = []; + $scope.managementPermissions = {}; angular.forEach(dataSources, function addAccountPage(dataSource) { // Determine whether data source contains this user var exists = (dataSource in $scope.users); // Calculate management actions available for this specific account - managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet( + $scope.managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet( values.permissions[dataSource], PermissionSet.SystemPermissionType.CREATE_USER, PermissionSet.hasUserPermission, exists ? username : null); - // Account is not relevant if it does not exist and cannot be - // created - var readOnly = !managementPermissions[dataSource].canSaveObject; - if (!exists && readOnly) - return; - - // Only the selected data source is relevant when cloning - if (cloneSourceUsername && dataSource !== $scope.dataSource) - return; - - // Determine class name based on read-only / linked status - var className; - if (readOnly) className = 'read-only'; - else if (exists) className = 'linked'; - else className = 'unlinked'; - - // Add page entry - $scope.accountPages.push(new PageDefinition({ - name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', - url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username || ''), - className : className - })); - }); - $scope.managementPermissions = managementPermissions[$scope.dataSource]; - }, requestService.WARN); + /** + * Returns the URL for the page which manages the user account currently + * being edited under the given data source. The given data source need not + * be the same as the data source currently selected. + * + * @param {String} dataSource + * The unique identifier of the data source that the URL is being + * generated for. + * + * @returns {String} + * The URL for the page which manages the user account currently being + * edited under the given data source. + */ + $scope.getUserURL = function getUserURL(dataSource) { + return '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username || ''); + }; + /** * Cancels all pending edits, returning to the main list of users. */ diff --git a/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js b/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js new file mode 100644 index 000000000..cf7068f27 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js @@ -0,0 +1,111 @@ +/* + * 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. + */ + +/** + * Directive which displays a set of tabs pointing to the same object within + * different data sources, such as user accounts which span multiple data + * sources. + */ +angular.module('manage').directive('dataSourceTabs', ['$injector', + function dataSourceTabs($injector) { + + // Required types + var PageDefinition = $injector.get('PageDefinition'); + + // Required services + var translationStringService = $injector.get('translationStringService'); + + var directive = { + + restrict : 'E', + replace : true, + templateUrl : 'app/manage/templates/dataSourceTabs.html', + + scope : { + + /** + * The permissions which dictate the management actions available + * to the current user. + * + * @type Object. + */ + permissions : '=', + + /** + * A function which returns the URL of the object within a given + * data source. The relevant data source will be made available to + * the Angular expression defining this function as the + * "dataSource" variable. No other values will be made available, + * including values from the scope. + * + * @type Function + */ + url : '&' + + } + + }; + + directive.controller = ['$scope', function dataSourceTabsController($scope) { + + /** + * The set of pages which each manage the same object within different + * data sources. + * + * @type PageDefinition[] + */ + $scope.pages = null; + + // Keep pages synchronized with permissions + $scope.$watch('permissions', function permissionsChanged(permissions) { + + $scope.pages = []; + angular.forEach(permissions, function addDataSourcePage(managementPermissions, dataSource) { + + // Determine whether data source contains this object + var exists = !!managementPermissions.identifier; + + // Data source is not relevant if the associated object does not + // exist and cannot be created + var readOnly = !managementPermissions.canSaveObject; + if (!exists && readOnly) + return; + + // Determine class name based on read-only / linked status + var className; + if (readOnly) className = 'read-only'; + else if (exists) className = 'linked'; + else className = 'unlinked'; + + // Add page entry + $scope.pages.push(new PageDefinition({ + name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + url : $scope.url({ dataSource : dataSource }), + className : className + })); + + }); + + }); + + }]; + + return directive; + +}]); diff --git a/guacamole/src/main/webapp/app/manage/templates/dataSourceTabs.html b/guacamole/src/main/webapp/app/manage/templates/dataSourceTabs.html new file mode 100644 index 000000000..a8a08431b --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/templates/dataSourceTabs.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index 02b3ddce5..6770d3103 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -6,17 +6,18 @@

{{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}

-
- -
+ + -
+

{{'MANAGE_USER.INFO_READ_ONLY' | translate}}

-
+
@@ -40,13 +41,14 @@
-
+
+ model="user.attributes" + model-only="!managementPermissions[dataSource].canChangeAllAttributes">
- - Date: Tue, 1 May 2018 17:19:40 -0700 Subject: [PATCH 13/20] GUACAMOLE-220: Remove unnecessary userExists() function. --- .../controllers/manageUserController.js | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 27cb9337f..800ad00b9 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -170,31 +170,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; - /** - * Returns whether the user being edited already exists within the data - * source specified. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the user being edited already exists, false otherwise. - */ - $scope.userExists = function userExists(dataSource) { - - // Do not check if users are not yet loaded - if (!$scope.users) - return false; - - // Use currently-selected data source if unspecified - dataSource = dataSource || $scope.dataSource; - - // Account exists only if it was successfully retrieved - return (dataSource in $scope.users); - - }; - /** * Returns whether the current user can edit the username of the user being * edited within the given data source. @@ -415,7 +390,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto // Save or create the user, depending on whether the user exists var saveUserPromise; - if ($scope.userExists($scope.dataSource)) + if ($scope.dataSource in $scope.users) saveUserPromise = userService.saveUser($scope.dataSource, $scope.user); else saveUserPromise = userService.createUser($scope.dataSource, $scope.user); From 5028d85bb1d5de40d3da70e3a263e8a2d9f35a62 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 May 2018 21:09:51 -0700 Subject: [PATCH 14/20] GUACAMOLE-220: "required", not "requred". --- .../webapp/app/manage/controllers/manageConnectionController.js | 2 +- .../app/manage/controllers/manageConnectionGroupController.js | 2 +- .../app/manage/controllers/manageSharingProfileController.js | 2 +- .../main/webapp/app/manage/controllers/manageUserController.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js index 6139781d2..b84886f10 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js @@ -243,7 +243,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i }; /** - * Loads the data requred for performing the management task requested + * Loads the data required for performing the management task requested * through the route parameters given at load time, automatically preparing * the interface for editing an existing connection, cloning an existing * connection, or creating an entirely new connection. diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js index 1a4b64e32..250ebc102 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js @@ -201,7 +201,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope' }; /** - * Loads the data requred for performing the management task requested + * Loads the data required for performing the management task requested * through the route parameters given at load time, automatically preparing * the interface for editing an existing connection group, cloning an * existing connection group, or creating an entirely new connection group. diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js index 7385aa16f..8b57182cd 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js @@ -222,7 +222,7 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', }; /** - * Loads the data requred for performing the management task requested + * Loads the data required for performing the management task requested * through the route parameters given at load time, automatically preparing * the interface for editing an existing sharing profile, cloning an * existing sharing profile, or creating an entirely new sharing profile. diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 800ad00b9..9a90441a8 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -286,7 +286,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }; /** - * Loads the data requred for performing the management task requested + * Loads the data required for performing the management task requested * through the route parameters given at load time, automatically preparing * the interface for editing an existing user, cloning an existing user, or * creating an entirely new user. From f5f516d82a2ebcee654185acb38f798f71a28c81 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 3 May 2018 23:16:14 -0700 Subject: [PATCH 15/20] GUACAMOLE-220: Pull primary connection for new sharing profiles. --- .../manageSharingProfileController.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js index 8b57182cd..61bfbe93f 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js @@ -205,11 +205,15 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', * Loads skeleton sharing profile data, preparing the interface for * creating a new sharing profile. * + * @param {String} dataSource + * The unique identifier of the data source containing the sharing + * profile to be created. + * * @returns {Promise} * A promise which is resolved when the interface has been prepared for * creating a new sharing profile. */ - var loadSkeletonSharingProfile = function loadSkeletonSharingProfile() { + var loadSkeletonSharingProfile = function loadSkeletonSharingProfile(dataSource) { // Use skeleton sharing profile object with no associated parameters $scope.sharingProfile = new SharingProfile({ @@ -217,7 +221,14 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', }); $scope.parameters = {}; - return $q.resolve(); + // Load connection object for associated primary connection + return connectionService.getConnection( + dataSource, + $scope.sharingProfile.primaryConnectionIdentifier + ) + .then(function connectionRetrieved(connection) { + $scope.primaryConnection = connection; + }); }; @@ -243,7 +254,7 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope', // If we are creating a new sharing profile, populate skeleton sharing // profile data - return loadSkeletonSharingProfile(); + return loadSkeletonSharingProfile($scope.selectedDataSource); }; From 7ba9c32a0767893860645b9ee22cfe3c1885c355 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 3 May 2018 23:32:07 -0700 Subject: [PATCH 16/20] GUACAMOLE-220: Document context of skeleton user creation. --- .../main/webapp/app/manage/controllers/manageUserController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index 9a90441a8..167988623 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -305,6 +305,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto if (cloneSourceUsername) return loadClonedUser($scope.dataSource, cloneSourceUsername); + // If we are creating a new user, populate skeleton user data return loadSkeletonUser(); }; From e8f0c967585d9d576e1c07dbd6528ce6a8ce4beb Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 3 May 2018 23:33:41 -0700 Subject: [PATCH 17/20] GUACAMOLE-220: Correct documentation of permissionsRemoved attribute (the permissions have been removed, not added). --- .../webapp/app/manage/directives/connectionPermissionEditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js index c4be07105..da85c1172 100644 --- a/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js +++ b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js @@ -69,7 +69,7 @@ angular.module('manage').directive('connectionPermissionEditor', ['$injector', permissionsAdded : '=', /** - * The set of permissions that have been added, relative to the + * The set of permissions that have been removed, relative to the * initial state of the permissions being manipulated. * * @type PermissionSet From 2c41e38e557e4f56183ebafca8ec7d5c8632f329 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 3 May 2018 23:34:34 -0700 Subject: [PATCH 18/20] GUACAMOLE-220: removeConnectionGroupPermission() affects connection group permissions, not connection permissions. --- .../app/manage/directives/connectionPermissionEditor.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js index da85c1172..43e80c2db 100644 --- a/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js +++ b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js @@ -260,10 +260,11 @@ angular.module('manage').directive('connectionPermissionEditor', ['$injector', /** * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the removal of the given connection permission. + * to reflect the removal of the given connection group permission. * * @param {String} identifier - * The identifier of the connection to remove READ permission for. + * The identifier of the connection group to remove READ permission + * for. */ var removeConnectionGroupPermission = function removeConnectionGroupPermission(identifier) { From ae0512c2667b261405016a7d164239de8e96610c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 3 May 2018 23:37:19 -0700 Subject: [PATCH 19/20] GUACAMOLE-220: Convert "namespace" attribute of managementButtons directive to string binding. --- .../src/main/webapp/app/manage/directives/managementButtons.js | 2 +- .../src/main/webapp/app/manage/templates/manageConnection.html | 2 +- .../main/webapp/app/manage/templates/manageConnectionGroup.html | 2 +- .../main/webapp/app/manage/templates/manageSharingProfile.html | 2 +- guacamole/src/main/webapp/app/manage/templates/manageUser.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/guacamole/src/main/webapp/app/manage/directives/managementButtons.js b/guacamole/src/main/webapp/app/manage/directives/managementButtons.js index 407454881..a83b82c22 100644 --- a/guacamole/src/main/webapp/app/manage/directives/managementButtons.js +++ b/guacamole/src/main/webapp/app/manage/directives/managementButtons.js @@ -50,7 +50,7 @@ angular.module('manage').directive('managementButtons', ['$injector', * * @type String */ - namespace : '=', + namespace : '@', /** * The permissions which dictate the management actions available diff --git a/guacamole/src/main/webapp/app/manage/templates/manageConnection.html b/guacamole/src/main/webapp/app/manage/templates/manageConnection.html index e78051373..17a1108c6 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageConnection.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageConnection.html @@ -53,7 +53,7 @@
- - - - Date: Thu, 3 May 2018 23:38:01 -0700 Subject: [PATCH 20/20] GUACAMOLE-220: Correct grammar of saveObject() documentation ("to save", not "to saves"). --- .../src/main/webapp/app/manage/directives/managementButtons.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/manage/directives/managementButtons.js b/guacamole/src/main/webapp/app/manage/directives/managementButtons.js index a83b82c22..f44515aa6 100644 --- a/guacamole/src/main/webapp/app/manage/directives/managementButtons.js +++ b/guacamole/src/main/webapp/app/manage/directives/managementButtons.js @@ -167,7 +167,7 @@ angular.module('manage').directive('managementButtons', ['$injector', }; /** - * Invokes the provided save function to saves the current object. If + * Invokes the provided save function to save the current object. If * saving is successful, the user is navigated back to the page they * started from. If saving fails, an error notification is displayed. */