GUACAMOLE-926: Create users and groups; don't require them to exist beforehand.

This commit is contained in:
James Muehlner
2023-02-13 19:02:26 +00:00
parent 761438e02d
commit bfb7f3b78a
9 changed files with 195 additions and 132 deletions

View File

@@ -24,19 +24,73 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
function importConnectionsController($scope, $injector) { function importConnectionsController($scope, $injector) {
// Required services // Required services
const $routeParams = $injector.get('$routeParams');
const connectionParseService = $injector.get('connectionParseService'); const connectionParseService = $injector.get('connectionParseService');
const connectionService = $injector.get('connectionService'); const connectionService = $injector.get('connectionService');
// Required types // Required types
const DirectoryPatch = $injector.get('DirectoryPatch');
const ParseError = $injector.get('ParseError'); const ParseError = $injector.get('ParseError');
const TranslatableMessage = $injector.get('TranslatableMessage'); const TranslatableMessage = $injector.get('TranslatableMessage');
/**
* 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)
function handleSuccess(data) { .then(deletionResponse =>
console.log("OMG SUCCESS: ", data) console.log("Deletion response", deletionResponse))
.catch(handleParseError);
}
/**
* 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
*
* @param {ParseResult} parseResult
* The result of parsing the user-supplied import file.
*
*/
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);
});
} }
// Set any caught error message to the scope for display // Set any caught error message to the scope for display
const handleError = error => { const handleParseError = error => {
console.error(error); console.error(error);
$scope.error = error; $scope.error = error;
} }
@@ -44,14 +98,25 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
// Clear the current error // Clear the current error
const clearError = () => delete $scope.error; const clearError = () => delete $scope.error;
function processData(type, data) { /**
* Process the uploaded import file, importing the connections, granting
* connection permissions, or displaying errors to the user if there are
* problems with the provided file.
*
* @param {String} mimeType
* The MIME type of the uploaded data file.
*
* @param {String} data
* The raw string contents of the import file.
*/
function processData(mimeType, data) {
// The function that will process all the raw data and return a list of // The function that will process all the raw data and return a list of
// patches to be submitted to the API // patches to be submitted to the API
let processDataCallback; let processDataCallback;
// Parse the data based on the provided mimetype // Parse the data based on the provided mimetype
switch(type) { switch(mimeType) {
case "application/json": case "application/json":
case "text/json": case "text/json":
@@ -82,10 +147,10 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
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
.then(handleSuccess) .then(handleParseSuccess)
// Display any error found while parsing the file // Display any error found while parsing the file
.catch(handleError); .catch(handleParseError);
} }
$scope.upload = function() { $scope.upload = function() {

View File

@@ -33,7 +33,7 @@ angular.module('import').factory('connectionCSVService',
// Required types // Required types
const ParseError = $injector.get('ParseError'); const ParseError = $injector.get('ParseError');
const ProtoConnection = $injector.get('ProtoConnection'); const ImportConnection = $injector.get('ImportConnection');
const TranslatableMessage = $injector.get('TranslatableMessage'); const TranslatableMessage = $injector.get('TranslatableMessage');
// Required services // Required services
@@ -154,7 +154,7 @@ angular.module('import').factory('connectionCSVService',
/** /**
* Given a CSV header row, create and return a promise that will resolve to * Given a CSV header row, create and return a promise that will resolve to
* a function that can take a CSV data row and return a ProtoConnection * a function that can take a CSV data row and return a ImportConnection
* object. If an error occurs while parsing a particular row, the resolved * object. If an error occurs while parsing a particular row, the resolved
* function will throw a ParseError describing the failure. * function will throw a ParseError describing the failure.
* *
@@ -185,7 +185,7 @@ angular.module('import').factory('connectionCSVService',
* *
* @returns {Promise.<Function.<String[], Object>>} * @returns {Promise.<Function.<String[], Object>>}
* A promise that will resolve to a function that translates a CSV data * A promise that will resolve to a function that translates a CSV data
* row (array of strings) to a ProtoConnection object. * row (array of strings) to a ImportConnection object.
*/ */
service.getCSVTransformer = function getCSVTransformer(headerRow) { service.getCSVTransformer = function getCSVTransformer(headerRow) {
@@ -405,7 +405,7 @@ angular.module('import').factory('connectionCSVService',
const parentIdentifier = ( const parentIdentifier = (
parentIdentifierGetter && parentIdentifierGetter(row)); parentIdentifierGetter && parentIdentifierGetter(row));
return new ProtoConnection({ return new ImportConnection({
// Fields that are not protocol-specific // Fields that are not protocol-specific
...{ ...{

View File

@@ -42,81 +42,8 @@ angular.module('import').factory('connectionParseService',
const schemaService = $injector.get('schemaService'); const schemaService = $injector.get('schemaService');
const connectionCSVService = $injector.get('connectionCSVService'); const connectionCSVService = $injector.get('connectionCSVService');
const connectionGroupService = $injector.get('connectionGroupService'); const connectionGroupService = $injector.get('connectionGroupService');
const userService = $injector.get('userService');
const userGroupService = $injector.get('userGroupService');
const service = {}; const service = {};
/**
* Resolves to an object whose keys are all valid identifiers in the current
* data source. The provided `requestFunction` should resolve to such an
* object when provided with the current data source.
*
* @param {Function.<String<Object.<String, *>>} requestFunction
* A function that, given a data source, will return a promise resolving
* to an object with keys that are unique identifiers for entities in
* that data source.
*
* @returns {Promise.<Function[String[], String[]>>}
* A promise that will resolve to an function that, given an array of
* identifiers, will return all identifiers that do not exist as keys in
* the object returned by `requestFunction`, i.e. all identifiers that
* do not exist in the current data source.
*/
function getIdentifierMap(requestFunction) {
// The current data source to which all the identifiers will belong
const dataSource = $routeParams.dataSource;
// Make the request and return the response, which should be an object
// whose keys are all valid identifiers for the current data source
return requestFunction(dataSource);
}
/**
* Return a promise that resolves to a function that takes an array of
* identifiers, returning a ParseError describing the invalid identifiers
* from the list. The provided `requestFunction` should resolve to such an
* object whose keys are all valid identifiers when provided with the
* current data source.
*
* @param {Function.<String<Object.<String, *>>} requestFunction
* A function that, given a data source, will return a promise resolving
* to an object with keys that are unique identifiers for entities in
* that data source.
*
* @returns {Promise.<Function.<String[], ParseError?>>}
* A promise that will resolve to a function to check the validity of
* each identifier in a provided array, returning a ParseError
* describing the problem if any are not valid.
*/
function getInvalidIdentifierErrorChecker(requestFunction) {
// Fetch all the valid user identifiers in the system, and
return getIdentifierMap(requestFunction).then(validIdentifiers =>
// The resolved function that takes a list of user group identifiers
allIdentifiers => {
// Filter to only include invalid identifiers
const invalidIdentifiers = _.filter(allIdentifiers,
identifier => !validIdentifiers[identifier]);
if (invalidIdentifiers.length) {
// Quote and comma-seperate for display
const identifierList = invalidIdentifiers.map(
identifier => '"' + identifier + '"').join(', ');
return new ParseError({
message: 'Invalid User Group Identifiers: ' + identifierList,
key: 'CONNECTION_IMPORT.ERROR_INVALID_USER_GROUP_IDENTIFIERS',
variables: { IDENTIFIER_LIST: identifierList }
});
}
});
}
/** /**
* Perform basic checks, common to all file types - namely that the parsed * Perform basic checks, common to all file types - namely that the parsed
@@ -245,12 +172,12 @@ angular.module('import').factory('connectionParseService',
} }
/** /**
* Convert a provided ProtoConnection array into a ParseResult. Any provided * Convert a provided ImportConnection array into a ParseResult. Any provided
* transform functions will be run on each entry in `connectionData` before * transform functions will be run on each entry in `connectionData` before
* any other processing is done. * any other processing is done.
* *
* @param {*[]} connectionData * @param {*[]} connectionData
* An arbitrary array of data. This must evaluate to a ProtoConnection * An arbitrary array of data. This must evaluate to a ImportConnection
* object after being run through all functions in `transformFunctions`. * object after being run through all functions in `transformFunctions`.
* *
* @param {Function[]} transformFunctions * @param {Function[]} transformFunctions
@@ -271,21 +198,10 @@ angular.module('import').factory('connectionParseService',
return deferred.promise; return deferred.promise;
} }
return $q.all({ return getGroupTransformer().then(groupTransformer =>
groupTransformer : getGroupTransformer(), connectionData.reduce((parseResult, data) => {
invalidUserIdErrorDetector : getInvalidIdentifierErrorChecker(
userService.getUsers),
invalidGroupIDErrorDetector : getInvalidIdentifierErrorChecker(
userGroupService.getUserGroups),
})
// Transform the rows from the CSV file to an array of API patches const { patches, users, groups, allUsers, allGroups } = parseResult;
// and lists of user and group identifiers
.then(({groupTransformer,
invalidUserIdErrorDetector, invalidGroupIDErrorDetector}) =>
connectionData.reduce((parseResult, data) => {
const { patches, identifiers, users, groups } = parseResult;
// Run the array data through each provided transform // Run the array data through each provided transform
let connectionObject = data; let connectionObject = data;
@@ -307,14 +223,15 @@ angular.module('import').factory('connectionParseService',
connectionErrors.push(error); connectionErrors.push(error);
} }
// Push any errors for invalid user or user group identifiers
const pushError = error => error && connectionErrors.push(error);
pushError(invalidUserIdErrorDetector(connectionObject.users));
pushError(invalidGroupIDErrorDetector(connectionObject.userGroups));
// Add the user and group identifiers for this connection // Add the user and group identifiers for this connection
users.push(connectionObject.users); const connectionUsers = connectionObject.users || [];
groups.push(connectionObject.groups); 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);
// Translate to a full-fledged Connection // Translate to a full-fledged Connection
const connection = new Connection(connectionObject); const connection = new Connection(connectionObject);
@@ -398,7 +315,18 @@ 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
const connectionData = parseYAMLData(yamlData); try {
const connectionData = parseYAMLData(yamlData);
}
// If the YAML parser throws an error, reject with that error. No
// translation key will be available here.
catch(error) {
console.error(error);
const deferred = $q.defer();
deferred.reject(new ParseError({ message: error.message }));
return deferred.promise;
}
// Produce a ParseResult // Produce a ParseResult
return parseConnectionData(connectionData); return parseConnectionData(connectionData);
@@ -420,7 +348,18 @@ 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
const connectionData = JSON.parse(jsonData); try {
const connectionData = JSON.parse(jsonData);
}
// If the JSON parse attempt throws an error, reject with that error.
// No translation key will be available here.
catch(error) {
console.error(error);
const deferred = $q.defer();
deferred.reject(new ParseError({ message: error.message }));
return deferred.promise;
}
// Produce a ParseResult // Produce a ParseResult
return parseConnectionData(connectionData); return parseConnectionData(connectionData);

View File

@@ -18,21 +18,21 @@
*/ */
/** /**
* Service which defines the Connection class. * Service which defines the ImportConnection class.
*/ */
angular.module('import').factory('ProtoConnection', [ angular.module('import').factory('ImportConnection', [
function defineProtoConnection() { function defineImportConnection() {
/** /**
* A representation of a connection to be imported, as parsed from an * A representation of a connection to be imported, as parsed from an
* user-supplied import file. * user-supplied import file.
* *
* @constructor * @constructor
* @param {Connection|Object} [template={}] * @param {ImportConnection|Object} [template={}]
* The object whose properties should be copied within the new * The object whose properties should be copied within the new
* Connection. * Connection.
*/ */
var ProtoConnection = function ProtoConnection(template) { var ImportConnection = function ImportConnection(template) {
// Use empty object by default // Use empty object by default
template = template || {}; template = template || {};
@@ -106,6 +106,6 @@ angular.module('import').factory('ProtoConnection', [
}; };
return ProtoConnection; return ImportConnection;
}]); }]);

View File

@@ -63,6 +63,23 @@ angular.module('import').factory('ParseResult', [function defineParseResult() {
*/ */
this.groups = template.groups || []; 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.<String, Boolean>}
*/
this.allUsers = template.allUsers || {};
/**
* An object whose keys are the user group identifiers of every user
* group specified in the batch import. i.e. a set of all user group
* identifiers.
*
* @type {Object.<String, Boolean>}
*/
this.allGroups = template.allGroups || {};
/** /**
* An array of errors encountered while parsing the corresponding * An array of errors encountered while parsing the corresponding
* connection (at the same array index). Each connection should have a * connection (at the same array index). Each connection should have a

View File

@@ -158,7 +158,7 @@ angular.module('rest').factory('connectionService', ['$injector',
/** /**
* Makes a request to the REST API to apply a supplied list of connection * Makes a request to the REST API to apply a supplied list of connection
* patches, returning a promise that can be used for processing the results * patches, returning a promise that can be used for processing the results
* of the call. * of the call.
* *
* This operation is atomic - if any errors are encountered during the * This operation is atomic - if any errors are encountered during the
* connection patching process, the entire request will fail, and no * connection patching process, the entire request will fail, and no
@@ -181,12 +181,14 @@ angular.module('rest').factory('connectionService', ['$injector',
}) })
// Clear the cache // Clear the cache
.then(function connectionsPatched(){ .then(function connectionsPatched(patchResponse){
cacheService.connections.removeAll(); cacheService.connections.removeAll();
// Clear users cache to force reload of permissions for any // Clear users cache to force reload of permissions for any
// newly created or replaced connections // newly created or replaced connections
cacheService.users.removeAll(); cacheService.users.removeAll();
return patchResponse;
}); });

View File

@@ -59,10 +59,9 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch(
this.path = template.path || '/'; this.path = template.path || '/';
/** /**
* The object being added or replaced, or the identifier of the object * The object being added, or undefined if deleting.
* being removed.
* *
* @type {DirectoryObject|String} * @type {DirectoryObject}
*/ */
this.value = template.value; this.value = template.value;
@@ -78,11 +77,6 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch(
*/ */
ADD : "add", ADD : "add",
/**
* Removes the specified object from the relation.
*/
REPLACE : "replace",
/** /**
* Removes the specified object from the relation. * Removes the specified object from the relation.
*/ */

View File

@@ -30,15 +30,11 @@ angular.module('rest').factory('DirectoryPatchOutcome', [
* response. The error field is only meaningful for unsuccessful patches. * response. The error field is only meaningful for unsuccessful patches.
* @constructor * @constructor
* *
* @template DirectoryObject * @param {DirectoryPatchOutcome|Object} [template={}]
* The directory-based object type that this DirectoryPatchOutcome
* represents a patch outcome for.
*
* @param {DirectoryObject|Object} [template={}]
* The object whose properties should be copied within the new * The object whose properties should be copied within the new
* DirectoryPatchOutcome. * DirectoryPatchOutcome.
*/ */
var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { const DirectoryPatchOutcome = function DirectoryPatchOutcome(template) {
// Use empty object by default // Use empty object by default
template = template || {}; template = template || {};
@@ -78,6 +74,6 @@ angular.module('rest').factory('DirectoryPatchOutcome', [
}; };
return DirectoryPatch; return DirectoryPatchOutcome;
}]); }]);

View File

@@ -0,0 +1,50 @@
/*
* 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.
*/
/**
* Service which defines the DirectoryPatchResponse class.
*/
angular.module('rest').factory('DirectoryPatchResponse', [
function defineDirectoryPatchResponse() {
/**
* An object returned by a PATCH request to a directory REST API,
* representing the successful response to a patch request.
*
* @param {DirectoryPatchResponse|Object} [template={}]
* The object whose properties should be copied within the new
* DirectoryPatchResponse.
*/
const DirectoryPatchResponse = function DirectoryPatchResponse(template) {
// Use empty object by default
template = template || {};
/**
* An outcome for each patch in the corresponding patch request.
*
* @type {DirectoryPatchOutcome[]}
*/
this.patches = template.patches;
};
return DirectoryPatchResponse;
}]);