GUACAMOLE-926: Roll back created users and user groups as well.

This commit is contained in:
James Muehlner
2023-02-21 23:58:15 +00:00
parent 690a8a34ca
commit 5a2a4a79a7
6 changed files with 146 additions and 67 deletions

View File

@@ -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 * Create all users and user groups mentioned in the import file that don't
* already exist in the current data source. * already exist in the current data source.
@@ -178,8 +151,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
})); }));
return $q.all({ return $q.all({
createdUsers: userService.patchUsers(dataSource, userPatches), userResponse: userService.patchUsers(dataSource, userPatches),
createdGroups: userGroupService.patchUserGroups(dataSource, groupPatches) groupResponse: userGroupService.patchUserGroups(dataSource, groupPatches)
}); });
}); });
@@ -256,22 +229,126 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
return $q.all({ ...userRequests, ...groupRequests }); 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 * Process a successfully parsed import file, creating any specified
* connections, creating and granting permissions to any specified users * connections, creating and granting permissions to any specified users
* and user 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
* TODO: * option??? to roll back any already-created entities.
* - 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
* *
* @param {ParseResult} parseResult * @param {ParseResult} parseResult
* The result of parsing the user-supplied import file. * The result of parsing the user-supplied import file.
*
*/ */
function handleParseSuccess(parseResult) { function handleParseSuccess(parseResult) {
@@ -281,21 +358,20 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
// First, attempt to create the connections // First, attempt to create the connections
connectionService.patchConnections(dataSource, parseResult.patches) connectionService.patchConnections(dataSource, parseResult.patches)
.then(response => { .then(connectionResponse => {
// If connection creation is successful, create users and groups // If connection creation is successful, create users and groups
createUsersAndGroups(parseResult).then(() => { createUsersAndGroups(parseResult).then(
({userResponse, groupResponse}) =>
grantConnectionPermissions(parseResult, response).then(results => { grantConnectionPermissions(parseResult, connectionResponse)
console.log("permission requests", results); .then(() =>
// TODON'T: Delete connections so we can test over and over // TODON'T: Delete the stuff so we can test over and over
cleanUpConnections(response); cleanUpAll(connectionResponse, userResponse, groupResponse)
.then(resetUploadState)
resetUploadState(); ));
})
});
}); });
} }
@@ -315,7 +391,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
console.error(error); console.error(error);
$scope.error = error; $scope.error = error;
} };
/** /**
* Clear the current displayed 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 // have already have filtered out any invalid file types
else { else {
handleError(new ParseError({ handleError(new ParseError({
message: 'Invalid file type: ' + type, message: 'Invalid file type: ' + mimeType,
key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE',
variables: { TYPE: type } variables: { TYPE: mimeType }
})); }));
return; return;
} }
// Make the call to process the data into a series of patches // 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) processDataCallback(data)
// Send the data off to be imported if parsing is successful // 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 // Read all the data into memory
$scope.fileReader.readAsBinaryString(file); $scope.fileReader.readAsBinaryString(file);
} };
}]); }]);

View File

@@ -19,17 +19,16 @@
/* global _ */ /* 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 * All legal import file types. Any file not belonging to one of these types
* must be rejected. * must be rejected.
*/ */
const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; 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', [ angular.module('import').directive('connectionImportFileUpload', [
function connectionImportFileUpload() { function connectionImportFileUpload() {

View File

@@ -142,7 +142,7 @@ angular.module('import').factory('connectionParseService',
// If there's no group to translate, do nothing // If there's no group to translate, do nothing
if (!connection.group) if (!connection.group)
return; return connection;
// If both are specified, the parent group is ambigious // If both are specified, the parent group is ambigious
if (connection.parentIdentifier) if (connection.parentIdentifier)
@@ -333,8 +333,9 @@ angular.module('import').factory('connectionParseService',
service.parseYAML = function parseYAML(yamlData) { service.parseYAML = function parseYAML(yamlData) {
// Parse from YAML into a javascript array // Parse from YAML into a javascript array
let connectionData;
try { try {
const connectionData = parseYAMLData(yamlData); connectionData = parseYAMLData(yamlData);
} }
// If the YAML parser throws an error, reject with that error. No // 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) { service.parseJSON = function parseJSON(jsonData) {
// Parse from JSON into a javascript array // Parse from JSON into a javascript array
let connectionData;
try { try {
const connectionData = JSON.parse(jsonData); connectionData = JSON.parse(jsonData);
} }
// If the JSON parse attempt throws an error, reject with that error. // If the JSON parse attempt throws an error, reject with that error.

View File

@@ -31,7 +31,6 @@
.import.help h2 { .import.help h2 {
padding-top: 0px;
padding-bottom: 0px; padding-bottom: 0px;
} }

View File

@@ -43,11 +43,11 @@ conn4,kubernetes,,,,,</pre>
"protocol": "ssh", "protocol": "ssh",
"parameters": { "hostname": "conn3.web.com" }, "parameters": { "hostname": "conn3.web.com" },
"group": "ROOT/Parent Group/Child Group", "group": "ROOT/Parent Group/Child Group",
"users": [ "guac user 2", "guac user 3" ], "users": [ "guac user 2", "guac user 3" ]
}, },
{ {
"name": "conn4", "name": "conn4",
"protocol": "kubernetes", "protocol": "kubernetes"
} }
]</pre> ]</pre>
@@ -63,7 +63,7 @@ conn4,kubernetes,,,,,</pre>
- guac user 1 - guac user 1
- guac user 2 - guac user 2
groups: groups:
- AWS EC2 Administrators - Connection 1 Users
attributes: attributes:
guacd-encryption: none guacd-encryption: none
- name: conn2 - name: conn2

View File

@@ -194,7 +194,8 @@
"HELP_HEADER": "Connection Import File Format", "HELP_HEADER": "Connection Import File Format",
"HELP_FILE_TYPE_HEADER": "File Types", "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_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.", "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_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_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": "ERROR_AMBIGUOUS_CSV_HEADER":
"Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter",