From 5a2a4a79a7d45a4cc10c96e09bdd419d3938efa4 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 21 Feb 2023 23:58:15 +0000 Subject: [PATCH] GUACAMOLE-926: Roll back created users and user groups as well. --- .../importConnectionsController.js | 184 +++++++++++++----- .../directives/connectionImportFileUpload.js | 9 +- .../import/services/connectionParseService.js | 8 +- .../frontend/src/app/import/styles/help.css | 1 - .../templates/connectionImportFileHelp.html | 6 +- .../main/frontend/src/translations/en.json | 5 +- 6 files changed, 146 insertions(+), 67 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 31a49adf8..73acb1cc3 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -106,33 +106,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } - /** - * Given a successful response to an import PATCH request, make another - * request to delete every created connection in the provided request, i.e. - * clean up every connection that was created. - * - * @param {DirectoryPatchResponse} creationResponse - */ - function cleanUpConnections(creationResponse) { - - // The patches to delete - one delete per initial creation - const deletionPatches = creationResponse.patches.map(patch => - new DirectoryPatch({ - op: 'remove', - path: '/' + patch.identifier - })); - - console.log("Deletion Patches", deletionPatches); - - connectionService.patchConnections( - $routeParams.dataSource, deletionPatches) - - .then(deletionResponse => - console.log("Deletion response", deletionResponse)) - .catch(handleError); - - } - /** * Create all users and user groups mentioned in the import file that don't * already exist in the current data source. @@ -178,8 +151,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ })); return $q.all({ - createdUsers: userService.patchUsers(dataSource, userPatches), - createdGroups: userGroupService.patchUserGroups(dataSource, groupPatches) + userResponse: userService.patchUsers(dataSource, userPatches), + groupResponse: userGroupService.patchUserGroups(dataSource, groupPatches) }); }); @@ -256,22 +229,126 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ return $q.all({ ...userRequests, ...groupRequests }); } + // Given a PATCH API response, create an array of patches to delete every + // entity created in the original request that generated this response + const createDeletionPatches = creationResponse => + creationResponse.patches.map(patch => + + // Add one deletion patch per original creation patch + new DirectoryPatch({ + op: 'remove', + path: '/' + patch.identifier + })); + + /** + * Given a successful response to a connection PATCH request, make another + * request to delete every created connection in the provided request, i.e. + * clean up every connection that was created. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the connection PATCH request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpConnections(creationResponse) { + + return connectionService.patchConnections( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Given a successful response to a user PATCH request, make another + * request to delete every created user in the provided request. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the user PATCH request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpUsers(creationResponse) { + + return userService.patchUsers( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Given a successful response to a user group PATCH request, make another + * request to delete every created user group in the provided request. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the user group PATCH creation request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpUserGroups(creationResponse) { + + return userGroupService.patchUserGroups( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Make requests to delete all connections, users, and/or groups from any + * provided PATCH API responses. If any responses are not provided, no + * cleanup will be attempted. + * + * @param {DirectoryPatchResponse} connectionResponse + * The response to the connection PATCH creation request. + * + * @param {DirectoryPatchResponse} userResponse + * The response to the user PATCH creation request. + * + * @param {DirectoryPatchResponse} userGroupResponse + * The response to the user group PATCH creation request. + * + * @returns {Object} + * An object containing PATCH deletion responses corresponding to any + * provided connection, user, and/or user group creation responses. + */ + function cleanUpAll(connectionResponse, userResponse, userGroupResponse) { + + // All cleanup requests that need to be made + const requests = {}; + + // If the connection response was provided, clean up connections + if (connectionResponse) + requests.connectionCleanup = cleanUpConnections(connectionResponse); + + // If the user response was provided, clean up users + if (userResponse) + requests.userCleanup = cleanUpUsers(userResponse); + + // If the user group response was provided, clean up user groups + if (connectionResponse) + requests.userGroupCleanup = cleanUpUserGroups(userGroupResponse); + + // Return when all cleanup is complete + return $q.all(requests); + } + /** * Process a successfully parsed import file, creating any specified * connections, creating and granting permissions to any specified users - * and user groups. - * - * TODO: - * - Do batch import of connections - * - Create all users/groups not already present - * - Grant permissions to all users/groups as defined in the import file - * - On failure: Roll back everything (maybe ask the user first): - * - Attempt to delete all created connections - * - Attempt to delete any created users / groups + * and user groups. If successful, the user will be shown a success message. + * If not, any errors will be displayed, and the user will be given ???an + * option??? to roll back any already-created entities. * * @param {ParseResult} parseResult * The result of parsing the user-supplied import file. - * */ function handleParseSuccess(parseResult) { @@ -281,21 +358,20 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // First, attempt to create the connections connectionService.patchConnections(dataSource, parseResult.patches) - .then(response => { + .then(connectionResponse => { // If connection creation is successful, create users and groups - createUsersAndGroups(parseResult).then(() => { + createUsersAndGroups(parseResult).then( + ({userResponse, groupResponse}) => - grantConnectionPermissions(parseResult, response).then(results => { - console.log("permission requests", results); + grantConnectionPermissions(parseResult, connectionResponse) + .then(() => - // TODON'T: Delete connections so we can test over and over - cleanUpConnections(response); + // TODON'T: Delete the stuff so we can test over and over + cleanUpAll(connectionResponse, userResponse, groupResponse) + .then(resetUploadState) - resetUploadState(); - }) - - }); + )); }); } @@ -315,7 +391,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ console.error(error); $scope.error = error; - } + }; /** * Clear the current displayed error. @@ -356,14 +432,16 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // have already have filtered out any invalid file types else { handleError(new ParseError({ - message: 'Invalid file type: ' + type, + message: 'Invalid file type: ' + mimeType, key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', - variables: { TYPE: type } + variables: { TYPE: mimeType } })); return; } // Make the call to process the data into a series of patches + // TODO: Check if there's errors, and if so, display those rather than + // just YOLOing a create call processDataCallback(data) // Send the data off to be imported if parsing is successful @@ -476,6 +554,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Read all the data into memory $scope.fileReader.readAsBinaryString(file); - } + }; }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js index d988e7c7d..4e2c76b24 100644 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -19,17 +19,16 @@ /* global _ */ -/** - * A directive that allows for file upload, either through drag-and-drop or - * a file browser. - */ - /** * All legal import file types. Any file not belonging to one of these types * must be rejected. */ const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; +/** + * A directive that allows for file upload, either through drag-and-drop or + * a file browser. + */ angular.module('import').directive('connectionImportFileUpload', [ function connectionImportFileUpload() { 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 4b9f50c2d..cf15a374e 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -142,7 +142,7 @@ angular.module('import').factory('connectionParseService', // If there's no group to translate, do nothing if (!connection.group) - return; + return connection; // If both are specified, the parent group is ambigious if (connection.parentIdentifier) @@ -333,8 +333,9 @@ angular.module('import').factory('connectionParseService', service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array + let connectionData; try { - const connectionData = parseYAMLData(yamlData); + connectionData = parseYAMLData(yamlData); } // If the YAML parser throws an error, reject with that error. No @@ -366,8 +367,9 @@ angular.module('import').factory('connectionParseService', service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array + let connectionData; try { - const connectionData = JSON.parse(jsonData); + connectionData = JSON.parse(jsonData); } // If the JSON parse attempt throws an error, reject with that error. diff --git a/guacamole/src/main/frontend/src/app/import/styles/help.css b/guacamole/src/main/frontend/src/app/import/styles/help.css index e78acea4a..a73b43afa 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/help.css +++ b/guacamole/src/main/frontend/src/app/import/styles/help.css @@ -31,7 +31,6 @@ .import.help h2 { - padding-top: 0px; padding-bottom: 0px; } diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html index bd7f21845..e6063e89f 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -43,11 +43,11 @@ conn4,kubernetes,,,,, "protocol": "ssh", "parameters": { "hostname": "conn3.web.com" }, "group": "ROOT/Parent Group/Child Group", - "users": [ "guac user 2", "guac user 3" ], + "users": [ "guac user 2", "guac user 3" ] }, { "name": "conn4", - "protocol": "kubernetes", + "protocol": "kubernetes" } ] @@ -63,7 +63,7 @@ conn4,kubernetes,,,,, - guac user 1 - guac user 2 groups: - - AWS EC2 Administrators + - Connection 1 Users attributes: guacd-encryption: none - name: conn2 diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 06e3a4d9d..0233f8bb6 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -194,7 +194,8 @@ "HELP_HEADER": "Connection Import File Format", "HELP_FILE_TYPE_HEADER": "File Types", - "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified.", + "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created.", + "HELP_CSV_HEADER": "CSV Format", "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", @@ -207,7 +208,7 @@ "HELP_YAML_HEADER": "YAML Format", "HELP_YAML_DESCRIPTION": "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.", - "HELP_SEMICOLON_FOOTNOTE": "If needed, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", + "HELP_SEMICOLON_FOOTNOTE": "If present, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", "ERROR_AMBIGUOUS_CSV_HEADER": "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter",