From fc211628218d4ae8eba3d6bd528f7fba4d37c0c3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 30 Aug 2015 22:20:07 -0700 Subject: [PATCH] GUAC-586: Expand handling of permissions within user editor. Allow users to be created through editor. Display tabs for each possible account. --- .../controllers/manageUserController.js | 339 +++++++++++++++--- .../webapp/app/manage/styles/manage-user.css | 36 ++ .../app/manage/templates/manageUser.html | 103 +++--- .../src/main/webapp/images/checkmark.png | Bin 0 -> 569 bytes guacamole/src/main/webapp/images/plus.png | Bin 0 -> 299 bytes .../src/main/webapp/translations/en.json | 4 +- 6 files changed, 382 insertions(+), 100 deletions(-) create mode 100644 guacamole/src/main/webapp/images/checkmark.png create mode 100644 guacamole/src/main/webapp/images/plus.png diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js index a31a5c707..76394900f 100644 --- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -31,10 +31,12 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto 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 connectionGroupService = $injector.get('connectionGroupService'); var guacNotification = $injector.get('guacNotification'); @@ -55,6 +57,21 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto } }; + /** + * 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(); + /** * The unique identifier of the data source containing the user being * edited. @@ -70,6 +87,16 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto */ var username = $routeParams.id; + /** + * Whether the user being modified actually exists. If the user does not + * yet exist, a different REST service call must be made to create that + * user rather than update an existing user. If the user has not yet been + * loaded, this will be null. + * + * @type Boolean + */ + var userExists = null; + /** * The user being modified. * @@ -85,26 +112,13 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto $scope.permissionFlags = null; /** - * The root connection group of the connection group hierarchy. + * The root connection group of the connection group hierarchy within the + * data source containing the user being edited/created. * * @type ConnectionGroup */ $scope.rootGroup = null; - /** - * Whether the authenticated user has UPDATE permission for the user being edited. - * - * @type Boolean - */ - $scope.hasUpdatePermission = null; - - /** - * Whether the authenticated user has DELETE permission for the user being edited. - * - * @type Boolean - */ - $scope.hasDeletePermission = null; - /** * All permissions associated with the current user, or null if the user's * permissions have not yet been loaded. @@ -128,21 +142,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto * * @type PageDefinition[] */ - $scope.accountPages = (function getAccountPages(dataSources) { - - var accountPages = []; - - // Add an account page for each applicable data source - angular.forEach(dataSources, function addAccountPage(dataSource) { - accountPages.push(new PageDefinition( - translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', - '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) - )); - }); - - return accountPages; - - })(authenticationService.getAvailableDataSources()); + $scope.accountPages = []; /** * Returns whether the list of all available account tabs should be shown. @@ -168,25 +168,262 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto && $scope.permissionFlags !== null && $scope.rootGroup !== null && $scope.permissions !== null - && $scope.attributes !== null - && $scope.canSaveUser !== null - && $scope.canDeleteUser !== null; + && $scope.attributes !== null; }; + /** + * Returns whether the current user can change attributes associated with + * the user being edited. + * + * @returns {Boolean} + * true if the current user can change attributes associated with the + * user being edited, false otherwise. + */ + $scope.canChangeAttributes = function canChangeAttributes() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Attributes can always be set if we are creating the user + if (!userExists) + return true; + + // The administrator can always change attributes + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, can change attributes if we have permission to update this user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.UPDATE, username); + + }; + + /** + * Returns whether the current user can change permissions of any kind + * which are associated with the user being edited. + * + * @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() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Permissions can always be set if we are creating the user + if (!userExists) + return true; + + // The administrator can always modify permissions + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, can only modify permissions if we have explicit ADMINSTER permission + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.ADMINISTER, username); + + }; + + /** + * Returns whether the current user can change the system permissions + * granted to the user being edited. + * + * @returns {Boolean} + * true if the current user can grant or revoke system permissions to + * the user 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, + PermissionSet.SystemPermissionType.ADMINISTER); + + }; + + /** + * Returns whether the current user can save the user being edited, + * creating or updating that user, depending on whether the user already + * exists. + * + * @returns {Boolean} + * true if the current user can save changes to the user being edited, + * false otherwise. + */ + $scope.canSaveUser = function canSaveUser() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // The administrator can always save users + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // If user does not exist, can only save if we have permission to create users + if (!userExists) + return PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.CREATE_USER); + + // Otherwise, can only save if we have permission to update this user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.UPDATE, username); + + }; + + /** + * Returns whether the current user can delete the user being edited. + * + * @returns {Boolean} + * true if the current user can delete the user being edited, false + * otherwise. + */ + $scope.canDeleteUser = function canDeleteUser() { + + // Do not check if permissions are not yet loaded + if (!$scope.permissions) + return false; + + // Can't delete what doesn't exist + if (!userExists) + return false; + + // The administrator can always delete users + if (PermissionSet.hasSystemPermission($scope.permissions, + PermissionSet.SystemPermissionType.ADMINISTER)) + return true; + + // Otherwise, require explicit DELETE permission on the user + return PermissionSet.hasUserPermission($scope.permissions, + PermissionSet.ObjectPermissionType.DELETE, username); + + }; + + /** + * Returns whether the user being edited is read-only, and thus cannot be + * modified by the current user. + * + * @returns {Boolean} + * true if the user being edited is actually read-only and cannot be + * edited at all, false otherwise. + */ + $scope.isReadOnly = function isReadOnly() { + return !$scope.canSaveUser(); + }; + // Pull user attribute schema schemaService.getUserAttributes().success(function attributesReceived(attributes) { $scope.attributes = attributes; }); + /** + * Retrieves all user objects having the given username from each of the + * given data sources, returning a promise which resolves to a map of those + * users by data source identifier. If any data source returns an error, + * that data source is omitted from the results. + * + * @param {String[]} dataSources + * The identifiers of the data sources to query. + * + * @param {String} username + * The username of the user to retrieve from each of the given data + * sources. + * + * @returns {Promise.>} + * A promise which resolves to a map of user objects by data source + * identifier. + */ + var getUserAccounts = function getUserAccounts(dataSources, username) { + + var deferred = $q.defer(); + + var userRequests = []; + var accounts = {}; + + // Retrieve the requested user account from all data sources + angular.forEach(dataSources, function retrieveUser(dataSource) { + + // Add promise to list of pending requests + var deferredUserRequest = $q.defer(); + userRequests.push(deferredUserRequest.promise); + + // Retrieve user from data source + userService.getUser(dataSource, username) + .success(function userRetrieved(user) { + accounts[dataSource] = user; + deferredUserRequest.resolve(); + }) + + // Ignore any errors + .error(function userRetrievalFailed() { + deferredUserRequest.resolve(); + }); + + }); + + // Resolve when all requests are completed + $q.all(userRequests) + .then(function accountsRetrieved() { + deferred.resolve(accounts); + }); + + return deferred.promise; + + }; + // Pull user data - userService.getUser(dataSource, username).success(function userReceived(user) { - $scope.user = user; + getUserAccounts(dataSources, username) + .then(function usersReceived(users) { + + // Get user for currently-selected data source + $scope.user = users[dataSource]; + + // Create skeleton user if user does not exist + if (!$scope.user) { + userExists = false; + $scope.user = new User({ + 'username' : username + }); + } + else + userExists = true; + + // Generate pages for each applicable data source + $scope.accountPages = []; + angular.forEach(dataSources, function addAccountPage(dataSource) { + + // Determine whether data source contains this user + var linked = dataSource in users; + + // Add page entry + $scope.accountPages.push(new PageDefinition( + translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), + linked ? 'linked' : 'unlinked' + )); + + }); + }); // Pull user permissions permissionService.getPermissions(dataSource, username).success(function gotPermissions(permissions) { $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + }) + + // If permissions cannot be retrieved, use empty permissions + .error(function permissionRetrievalFailed() { + $scope.permissionFlags = new PermissionFlagSet(); }); // Retrieve all connections for which we have ADMINISTER permission @@ -196,24 +433,9 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto }); // Query the user's permissions for the current user - permissionService.getPermissions(dataSource, authenticationService.getCurrentUsername()) - .success(function permissionsReceived(permissions) { - + permissionService.getPermissions(dataSource, currentUsername) + .success(function permissionsReceived(permissions) { $scope.permissions = permissions; - - // Check if the user is new or if the user has UPDATE permission - $scope.canSaveUser = - !username - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, username); - - // Check if user is not new and the user has DELETE permission - $scope.canDeleteUser = - !!username && ( - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, username) - ); - }); /** @@ -550,9 +772,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return; } - // Save the user - userService.saveUser(dataSource, $scope.user) - .success(function savedUser() { + // Save or create the user, depending on whether the user exists + var saveUserPromise; + if (userExists) + saveUserPromise = userService.saveUser(dataSource, $scope.user); + else + saveUserPromise = userService.createUser(dataSource, $scope.user); + + saveUserPromise.success(function savedUser() { // Upon success, save any changed permissions permissionService.patchPermissions(dataSource, $scope.user.username, permissionsAdded, permissionsRemoved) diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css index 48a99a4c6..a24d3e569 100644 --- a/guacamole/src/main/webapp/app/manage/styles/manage-user.css +++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css @@ -27,3 +27,39 @@ .manage-user .username.header h2 { text-transform: none; } + +.manage-user .settings-tabs .page-list li.unlinked a[href], +.manage-user .settings-tabs .page-list li.linked a[href] { + padding-right: 2.5em; + position: relative; +} + +.manage-user .settings-tabs .page-list li.unlinked a[href]:before, +.manage-user .settings-tabs .page-list li.linked a[href]:before { + content: ' '; + position: absolute; + right: 0; + bottom: 0; + top: 0; + width: 2.5em; + background-size: 1.25em; + background-repeat: no-repeat; + background-position: center; +} + +.manage-user .settings-tabs .page-list li.unlinked a[href]:before { + background-image: url('images/plus.png'); +} + +.manage-user .settings-tabs .page-list li.unlinked a[href] { + opacity: 0.5; +} + +.manage-user .settings-tabs .page-list li.unlinked a[href]:hover, +.manage-user .settings-tabs .page-list li.unlinked a[href].current { + opacity: 1; +} + +.manage-user .settings-tabs .page-list li.linked a[href]:before { + background-image: url('images/checkmark.png'); +} diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html index b0b84f496..2f28b3335 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html +++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html @@ -22,7 +22,7 @@ THE SOFTWARE.
- +

{{user.username}}

@@ -31,56 +31,73 @@ THE SOFTWARE.
-
- - - - - - - - - -
{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
+ +
+

{{'MANAGE_USER.INFO_READ_ONLY' | translate}}

- -
- -
+ +
+ + +
+ + + + + + + + + +
{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
+
+ + +
+ +
+ + +
+

{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

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

{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

+
+ +
+
- -

{{'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/images/checkmark.png b/guacamole/src/main/webapp/images/checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..c54feb2656d98aad13cbfc8fb290def723db2fda GIT binary patch literal 569 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4)&QRetZ{njVTg1wHP>X{NcbE>17TO$G!rOtu+P8Jxr;u*g4W#Y)(E^ejs_`lxuOmSn*Geh)Lbg;?xV; zcmC16pg8SZv*?@f`Y63w)sYW4i+^(O%&2>lq!-V2?8%~?<~&mj_&4Z_pY0Dm^R)ZG zf*6zUY8&f6<^oMxFT3Md%s<8#g5J*$Dt%kMUqSrrUY-vfJOA*0RDTkm#r#ZaPdfMS ztMiK_FKn%RB=oxNv*iQP;-A9&l5=0OafO^#=`Cp3S;HyuY0{O6ysx~k_8Bz&jI(Dd zt7-F%mjB>XwDAK?3-@>`EWZn(AvMpb(uAge+ zB>7@G&(quA1*861#!ouM;rhtlv8jY@jaPdBEKr%gaSFJI^D~8>j|E0EgQu&X%Q~lo FCIF|C{#O71 literal 0 HcmV?d00001 diff --git a/guacamole/src/main/webapp/images/plus.png b/guacamole/src/main/webapp/images/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..14bedbe4892bdf33ca399407b0d1ceb2ec292210 GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4J!Gv`Q^Ab%3Sp{FlmkgO)9_${wOJ`jC_17-+YVq@1QK8zopr0B{{;?f?J) literal 0 HcmV?d00001 diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index 2d968a1f6..270db30f7 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -244,7 +244,9 @@ "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Username:", - + + "INFO_READ_ONLY" : "Sorry, but this user account cannot be edited.", + "SECTION_HEADER_CONNECTIONS" : "Connections", "SECTION_HEADER_EDIT_USER" : "Edit User", "SECTION_HEADER_PERMISSIONS" : "Permissions",