From 51e0fb8c661373afadb7e09e8965076db8057bf8 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 14 Feb 2023 02:12:20 +0000 Subject: [PATCH] GUACAMOLE-926: Grant permissions to all users / groups. --- .../importConnectionsController.js | 165 ++++++++++++++++-- .../import/services/connectionParseService.js | 34 +++- .../src/app/import/types/ParseResult.js | 27 +-- .../src/app/rest/services/userGroupService.js | 33 ++++ .../src/app/rest/services/userService.js | 33 ++++ 5 files changed, 253 insertions(+), 39 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 2d7cdab22..8cd235e6e 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -17,6 +17,8 @@ * under the License. */ +/* global _ */ + /** * The controller for the connection import page. */ @@ -24,14 +26,21 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function importConnectionsController($scope, $injector) { // Required services + const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); + const permissionService = $injector.get('permissionService'); + const userService = $injector.get('userService'); + const userGroupService = $injector.get('userGroupService'); // Required types const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); + const PermissionSet = $injector.get('PermissionSet'); const TranslatableMessage = $injector.get('TranslatableMessage'); + const User = $injector.get('User'); + const UserGroup = $injector.get('UserGroup'); /** * Given a successful response to an import PATCH request, make another @@ -56,10 +65,133 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(deletionResponse => console.log("Deletion response", deletionResponse)) - .catch(handleParseError); + .catch(handleError); } + /** + * Create all users and user groups mentioned in the import file that don't + * already exist in the current data source. + * + * @param {ParseResult} parseResult + * The result of parsing the user-supplied import file. + * + * @return {Object} + * An object containing the results of the calls to create the users + * and groups. + */ + function createUsersAndGroups(parseResult) { + + const dataSource = $routeParams.dataSource; + + return $q.all({ + existingUsers : userService.getUsers(dataSource), + existingGroups : userGroupService.getUserGroups(dataSource) + }).then(({existingUsers, existingGroups}) => { + + const userPatches = Object.keys(parseResult.users) + + // Filter out any existing users + .filter(identifier => !existingUsers[identifier]) + + // A patch to create each new user + .map(username => new DirectoryPatch({ + op: 'add', + path: '/', + value: new User({ username }) + })); + + const groupPatches = Object.keys(parseResult.groups) + + // Filter out any existing groups + .filter(identifier => !existingGroups[identifier]) + + // A patch to create each new user group + .map(identifier => new DirectoryPatch({ + op: 'add', + path: '/', + value: new UserGroup({ identifier }) + })); + + return $q.all({ + createdUsers: userService.patchUsers(dataSource, userPatches), + createdGroups: userGroupService.patchUserGroups(dataSource, groupPatches) + }); + + }); + + } + + /** + * Grant read permissions for each user and group in the supplied parse + * result to each connection in their connection list. Note that there will + * be a seperate request for each user and group. + * + * @param {ParseResult} parseResult + * The result of successfully parsing a user-supplied import file. + * + * @param {Object} response + * The response from the PATCH API request. + * + * @returns {Promise.} + * A promise that will resolve with the result of every permission + * granting request. + */ + function grantConnectionPermissions(parseResult, response) { + + const dataSource = $routeParams.dataSource; + + // All connection grant requests, one per user/group + const userRequests = {}; + const groupRequests = {}; + + // Create a PermissionSet granting access to all connections at + // the provided indices within the provided parse result + const createPermissionSet = indices => + new PermissionSet({ connectionPermissions: indices.reduce( + (permissions, index) => { + const connectionId = response.patches[index].identifier; + permissions[connectionId] = [ + PermissionSet.ObjectPermissionType.READ]; + return permissions; + }, {}) }); + + // Now that we've created all the users, grant access to each + _.forEach(parseResult.users, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `false` + userRequests[identifier] = permissionService.patchPermissions( + dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is not for a group + false)); + + // Now that we've created all the groups, grant access to each + _.forEach(parseResult.groups, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `true` + groupRequests[identifier] = permissionService.patchPermissions( + dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is for a group + true)); + + // Return the result from all the permission granting calls + return $q.all({ ...userRequests, ...groupRequests }); + } + /** * Process a successfully parsed import file, creating any specified * connections, creating and granting permissions to any specified users @@ -78,19 +210,32 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * */ function handleParseSuccess(parseResult) { - connectionService.patchConnections( - $routeParams.dataSource, parseResult.patches) - - .then(response => { - console.log("Creation Response", response); - // TODON'T: Delete connections so we can test over and over - cleanUpConnections(response); + const dataSource = $routeParams.dataSource; + + console.log("parseResult", parseResult); + + // First, attempt to create the connections + connectionService.patchConnections(dataSource, parseResult.patches) + .then(response => { + + // If connection creation is successful, create users and groups + createUsersAndGroups(parseResult).then(() => { + + grantConnectionPermissions(parseResult, response).then(results => { + console.log("permission requests", results); + + // TODON'T: Delete connections so we can test over and over + cleanUpConnections(response); + }) + + + }); }); } // Set any caught error message to the scope for display - const handleParseError = error => { + const handleError = error => { console.error(error); $scope.error = error; } @@ -150,7 +295,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(handleParseSuccess) // Display any error found while parsing the file - .catch(handleParseError); + .catch(handleError); } $scope.upload = function() { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 7c4f6c754..4b9f50c2d 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -199,9 +199,9 @@ angular.module('import').factory('connectionParseService', } return getGroupTransformer().then(groupTransformer => - connectionData.reduce((parseResult, data) => { + connectionData.reduce((parseResult, data, index) => { - const { patches, users, groups, allUsers, allGroups } = parseResult; + const { patches, users, groups } = parseResult; // Run the array data through each provided transform let connectionObject = data; @@ -223,15 +223,33 @@ angular.module('import').factory('connectionParseService', connectionErrors.push(error); } - // Add the user and group identifiers for this connection + // The users and user groups that should be granted access const connectionUsers = connectionObject.users || []; const connectionGroups = connectionObject.groups || []; - users.push(connectionUsers); - groups.push(connectionGroups); - // Add all user and user group identifiers to the overall sets - connectionUsers.forEach(identifier => allUsers[identifier] = true); - connectionGroups.forEach(identifier => allGroups[identifier] = true); + // Add this connection index to the list for each user + connectionUsers.forEach(identifier => { + + // If there's an existing list, add the index to that + if (users[identifier]) + users[identifier].push(index); + + // Otherwise, create a new list with just this index + else + users[identifier] = [index]; + }); + + // Add this connection index to the list for each group + connectionGroups.forEach(identifier => { + + // If there's an existing list, add the index to that + if (groups[identifier]) + groups[identifier].push(index); + + // Otherwise, create a new list with just this index + else + groups[identifier] = [index]; + }); // Translate to a full-fledged Connection const connection = new Connection(connectionObject); diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js index 77c49eb31..1a5abd14d 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -48,28 +48,13 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { this.patches = template.patches || []; /** - * A list of user identifiers that should be granted read access to the - * the corresponding connection (at the same array index). + * An object whose keys are the user identifiers of users specified + * in the batch import. and the keys are an array of indices of + * connections to which those users should be granted access. * - * @type {String[]} + * @type {Object.} */ - this.users = template.users || []; - - /** - * A list of user group identifiers that should be granted read access - * to the corresponding connection (at the same array index). - * - * @type {String[]} - */ - this.groups = template.groups || []; - - /** - * An object whose keys are the user identifiers of every user specified - * in the batch import. i.e. a set of all user identifiers. - * - * @type {Object.} - */ - this.allUsers = template.allUsers || {}; + this.users = template.users || {}; /** * An object whose keys are the user group identifiers of every user @@ -78,7 +63,7 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { * * @type {Object.} */ - this.allGroups = template.allGroups || {}; + this.groups = template.users || {}; /** * An array of errors encountered while parsing the corresponding diff --git a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js b/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js index 090efa944..987f8383a 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js @@ -190,6 +190,39 @@ angular.module('rest').factory('userGroupService', ['$injector', }; + /** + * Makes a request to the REST API to apply a supplied list of user group + * patches, returning a promise that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchUserGroups = function patchUserGroups(dataSource, patches) { + + // Make the PATCH request + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + data : patches + }) + + // Clear the cache + .then(function userGroupsPatched(patchResponse){ + cacheService.users.removeAll(); + return patchResponse; + }); + + } + return service; }]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/userService.js b/guacamole/src/main/frontend/src/app/rest/services/userService.js index faa26b40a..252aead3a 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/userService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/userService.js @@ -235,6 +235,39 @@ angular.module('rest').factory('userService', ['$injector', }); }; + + /** + * Makes a request to the REST API to apply a supplied list of user patches, + * returning a promise that can be used for processing the results of the + * call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchUsers = function patchUsers(dataSource, patches) { + + // Make the PATCH request + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users', + data : patches + }) + + // Clear the cache + .then(function usersPatched(patchResponse){ + cacheService.users.removeAll(); + return patchResponse; + }); + + } return service;