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) {
// Required services
const $routeParams = $injector.get('$routeParams');
const connectionParseService = $injector.get('connectionParseService');
const connectionService = $injector.get('connectionService');
// Required types
const DirectoryPatch = $injector.get('DirectoryPatch');
const ParseError = $injector.get('ParseError');
const TranslatableMessage = $injector.get('TranslatableMessage');
function handleSuccess(data) {
console.log("OMG SUCCESS: ", data)
/**
* 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(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
const handleError = error => {
const handleParseError = error => {
console.error(error);
$scope.error = error;
}
@@ -44,14 +98,25 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
// Clear the current 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
// patches to be submitted to the API
let processDataCallback;
// Parse the data based on the provided mimetype
switch(type) {
switch(mimeType) {
case "application/json":
case "text/json":
@@ -82,10 +147,10 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
processDataCallback(data)
// Send the data off to be imported if parsing is successful
.then(handleSuccess)
.then(handleParseSuccess)
// Display any error found while parsing the file
.catch(handleError);
.catch(handleParseError);
}
$scope.upload = function() {

View File

@@ -33,7 +33,7 @@ angular.module('import').factory('connectionCSVService',
// Required types
const ParseError = $injector.get('ParseError');
const ProtoConnection = $injector.get('ProtoConnection');
const ImportConnection = $injector.get('ImportConnection');
const TranslatableMessage = $injector.get('TranslatableMessage');
// 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
* 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
* function will throw a ParseError describing the failure.
*
@@ -185,7 +185,7 @@ angular.module('import').factory('connectionCSVService',
*
* @returns {Promise.<Function.<String[], Object>>}
* 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) {
@@ -405,7 +405,7 @@ angular.module('import').factory('connectionCSVService',
const parentIdentifier = (
parentIdentifierGetter && parentIdentifierGetter(row));
return new ProtoConnection({
return new ImportConnection({
// Fields that are not protocol-specific
...{

View File

@@ -42,82 +42,9 @@ angular.module('import').factory('connectionParseService',
const schemaService = $injector.get('schemaService');
const connectionCSVService = $injector.get('connectionCSVService');
const connectionGroupService = $injector.get('connectionGroupService');
const userService = $injector.get('userService');
const userGroupService = $injector.get('userGroupService');
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
* data is an array, and contains at least one connection entry. Returns an
@@ -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
* any other processing is done.
*
* @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`.
*
* @param {Function[]} transformFunctions
@@ -271,21 +198,10 @@ angular.module('import').factory('connectionParseService',
return deferred.promise;
}
return $q.all({
groupTransformer : getGroupTransformer(),
invalidUserIdErrorDetector : getInvalidIdentifierErrorChecker(
userService.getUsers),
invalidGroupIDErrorDetector : getInvalidIdentifierErrorChecker(
userGroupService.getUserGroups),
})
return getGroupTransformer().then(groupTransformer =>
connectionData.reduce((parseResult, data) => {
// Transform the rows from the CSV file to an array of API patches
// and lists of user and group identifiers
.then(({groupTransformer,
invalidUserIdErrorDetector, invalidGroupIDErrorDetector}) =>
connectionData.reduce((parseResult, data) => {
const { patches, identifiers, users, groups } = parseResult;
const { patches, users, groups, allUsers, allGroups } = parseResult;
// Run the array data through each provided transform
let connectionObject = data;
@@ -307,14 +223,15 @@ angular.module('import').factory('connectionParseService',
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
users.push(connectionObject.users);
groups.push(connectionObject.groups);
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);
// Translate to a full-fledged Connection
const connection = new Connection(connectionObject);
@@ -398,7 +315,18 @@ angular.module('import').factory('connectionParseService',
service.parseYAML = function parseYAML(yamlData) {
// 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
return parseConnectionData(connectionData);
@@ -420,7 +348,18 @@ angular.module('import').factory('connectionParseService',
service.parseJSON = function parseJSON(jsonData) {
// 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
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', [
function defineProtoConnection() {
angular.module('import').factory('ImportConnection', [
function defineImportConnection() {
/**
* A representation of a connection to be imported, as parsed from an
* user-supplied import file.
*
* @constructor
* @param {Connection|Object} [template={}]
* @param {ImportConnection|Object} [template={}]
* The object whose properties should be copied within the new
* Connection.
*/
var ProtoConnection = function ProtoConnection(template) {
var ImportConnection = function ImportConnection(template) {
// Use empty object by default
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 || [];
/**
* 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
* 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
* 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
* connection patching process, the entire request will fail, and no
@@ -181,13 +181,15 @@ angular.module('rest').factory('connectionService', ['$injector',
})
// Clear the cache
.then(function connectionsPatched(){
.then(function connectionsPatched(patchResponse){
cacheService.connections.removeAll();
// Clear users cache to force reload of permissions for any
// newly created or replaced connections
cacheService.users.removeAll();
return patchResponse;
});
}

View File

@@ -59,10 +59,9 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch(
this.path = template.path || '/';
/**
* The object being added or replaced, or the identifier of the object
* being removed.
* The object being added, or undefined if deleting.
*
* @type {DirectoryObject|String}
* @type {DirectoryObject}
*/
this.value = template.value;
@@ -78,11 +77,6 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch(
*/
ADD : "add",
/**
* Removes the specified object from the relation.
*/
REPLACE : "replace",
/**
* 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.
* @constructor
*
* @template DirectoryObject
* The directory-based object type that this DirectoryPatchOutcome
* represents a patch outcome for.
*
* @param {DirectoryObject|Object} [template={}]
* @param {DirectoryPatchOutcome|Object} [template={}]
* The object whose properties should be copied within the new
* DirectoryPatchOutcome.
*/
var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) {
const DirectoryPatchOutcome = function DirectoryPatchOutcome(template) {
// Use empty object by default
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;
}]);