GUACAMOLE-926: Correct parameter values in import file to correct case if possible.

This commit is contained in:
James Muehlner
2023-05-02 21:16:11 +00:00
parent 00fc4b4270
commit d82508ffe7

View File

@@ -200,7 +200,8 @@ angular.module('import').factory('connectionParseService',
/** /**
* Returns a promise that will resolve to a transformer function that will * Returns a promise that will resolve to a transformer function that will
* perform various checks and transforms relating to the connection group * perform various checks and transforms relating to the connection group
* tree heirarchy. It will: * tree hierarchy, pushing any errors into the resolved connection object.
* It will:
* - Ensure that a connection specifies either a valid group path (no path * - Ensure that a connection specifies either a valid group path (no path
* defaults to ROOT), or a valid parent group identifier, but not both * defaults to ROOT), or a valid parent group identifier, but not both
* - Ensure that this connection does not duplicate another connection * - Ensure that this connection does not duplicate another connection
@@ -243,19 +244,24 @@ angular.module('import').factory('connectionParseService',
let op = DirectoryPatch.Operation.ADD; let op = DirectoryPatch.Operation.ADD;
// If both are specified, the parent group is ambigious // If both are specified, the parent group is ambigious
if (providedIdentifier && connection.group) if (providedIdentifier && connection.group) {
throw new ParseError({ connection.errors.push(new ParseError({
message: 'Only one of group or parentIdentifier can be set', message: 'Only one of group or parentIdentifier can be set',
key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP'
}); }));
return connection;
}
// If a parent group identifier is present, but not valid // If a parent group identifier is present, but not valid
else if (providedIdentifier && !groupPathsByIdentifier[providedIdentifier]) else if (providedIdentifier
throw new ParseError({ && !groupPathsByIdentifier[providedIdentifier]) {
connection.errors.push(new ParseError({
message: 'No group with identifier: ' + providedIdentifier, message: 'No group with identifier: ' + providedIdentifier,
key: 'IMPORT.ERROR_INVALID_GROUP_IDENTIFIER', key: 'IMPORT.ERROR_INVALID_GROUP_IDENTIFIER',
variables: { IDENTIFIER: providedIdentifier } variables: { IDENTIFIER: providedIdentifier }
}); }));
return connection;
}
// If the parent identifier is valid, use it to determine the path // If the parent identifier is valid, use it to determine the path
else if (providedIdentifier) { else if (providedIdentifier) {
@@ -272,11 +278,13 @@ angular.module('import').factory('connectionParseService',
group = connection.group; group = connection.group;
// If the provided group isn't a string, it can never be valid // If the provided group isn't a string, it can never be valid
if (typeof group !== 'string') if (typeof group !== 'string') {
throw new ParseError({ connection.errors.push(new ParseError({
message: 'Invalid group type - must be a string', message: 'Invalid group type - must be a string',
key: 'IMPORT.ERROR_INVALID_GROUP_TYPE' key: 'IMPORT.ERROR_INVALID_GROUP_TYPE'
}); }));
return connection;
}
// Allow the group to start with a leading slash instead instead // Allow the group to start with a leading slash instead instead
// of explicitly requiring the root connection group // of explicitly requiring the root connection group
@@ -295,12 +303,14 @@ angular.module('import').factory('connectionParseService',
parentIdentifier = groupPathsByIdentifier[group]; parentIdentifier = groupPathsByIdentifier[group];
// If the group doesn't match anything in the tree // If the group doesn't match anything in the tree
if (!parentIdentifier) if (!parentIdentifier) {
throw new ParseError({ connection.errors.push(new ParseError({
message: 'No group found named: ' + connection.group, message: 'No group found named: ' + connection.group,
key: 'IMPORT.ERROR_INVALID_GROUP', key: 'IMPORT.ERROR_INVALID_GROUP',
variables: { GROUP: connection.group } variables: { GROUP: connection.group }
}); }));
return connection;
}
} }
@@ -316,11 +326,11 @@ angular.module('import').factory('connectionParseService',
// Error out if this is a duplicate of a connection already in the // Error out if this is a duplicate of a connection already in the
// file // file
if (!!_.get(connectionsInFile, path)) if (!!_.get(connectionsInFile, path))
throw new ParseError({ connection.errors.push(new ParseError({
message: 'Duplicate connection in file: ' + path, message: 'Duplicate connection in file: ' + path,
key: 'IMPORT.ERROR_DUPLICATE_CONNECTION_IN_FILE', key: 'IMPORT.ERROR_DUPLICATE_CONNECTION_IN_FILE',
variables: { NAME: connection.name, PATH: group } variables: { NAME: connection.name, PATH: group }
}); }));
// Mark the current path as already seen in the file // Mark the current path as already seen in the file
_.setWith(connectionsInFile, path, connection, Object); _.setWith(connectionsInFile, path, connection, Object);
@@ -329,17 +339,18 @@ angular.module('import').factory('connectionParseService',
const existingIdentifier = _.get(connectionIdsByGroupAndName, const existingIdentifier = _.get(connectionIdsByGroupAndName,
[parentIdentifier, connection.name]); [parentIdentifier, connection.name]);
let importMode; // The default behavior is to create connections if no conflict
let importMode = ImportConnection.ImportMode.CREATE;
let identifier; let identifier;
// If updates to existing connections are disallowed // If updates to existing connections are disallowed
if (existingIdentifier && importConfig.existingConnectionMode === if (existingIdentifier && importConfig.existingConnectionMode ===
ConnectionImportConfig.ExistingConnectionMode.REJECT) ConnectionImportConfig.ExistingConnectionMode.REJECT)
throw new ParseError({ connection.errors.push(new ParseError({
message: 'Rejecting update to existing connection: ' + path, message: 'Rejecting update to existing connection: ' + path,
key: 'IMPORT.ERROR_REJECT_UPDATE_CONNECTION', key: 'IMPORT.ERROR_REJECT_UPDATE_CONNECTION',
variables: { NAME: connection.name, PATH: group } variables: { NAME: connection.name, PATH: group }
}); }));
// If the connection is being replaced, set the existing identifer // If the connection is being replaced, set the existing identifer
else if (existingIdentifier) { else if (existingIdentifier) {
@@ -347,7 +358,6 @@ angular.module('import').factory('connectionParseService',
importMode = ImportConnection.ImportMode.REPLACE; importMode = ImportConnection.ImportMode.REPLACE;
} }
// Otherwise, just create a new connection
else else
importMode = ImportConnection.ImportMode.CREATE; importMode = ImportConnection.ImportMode.CREATE;
@@ -359,13 +369,22 @@ angular.module('import').factory('connectionParseService',
} }
/** /**
* Returns a promise that resolves to a map of all valid protocols to the * Returns a promise that resolves to a map of all valid protocols to a map
* boolean value "true", i.e. a set of all valid protocols. * of connection parameter names to a map of lower-cased and trimmed option
* values for that parameter to the actual valid option value.
* *
* @returns {Promise.<Object.<String, Boolean>>} * This format is designed for easy retrieval of corrected parameter values
* A promise that resolves to a set of all valid protocols. * if the user-provided value matches a valid option except for case or
* leading/trailing whitespace.
*
* If a parameter has no options (i.e. any string value is allowed), the
* parameter name will map to a null value.
*
* @returns {Promise.<Object.<String, Object.<String, Object.<String, String>>>>}
* A promise that resolves to a map of all valid protocols to parameter
* names to valid values.
*/ */
function getValidProtocols() { function getProtocolParameterOptions() {
// The current data source - the one that the connections will be // The current data source - the one that the connections will be
// imported into // imported into
@@ -373,67 +392,109 @@ angular.module('import').factory('connectionParseService',
// Fetch the protocols and convert to a set of valid protocol names // Fetch the protocols and convert to a set of valid protocol names
return schemaService.getProtocols(dataSource).then( return schemaService.getProtocols(dataSource).then(
protocols => _.mapValues(protocols, () => true)); protocols => _.mapValues(protocols, ({connectionForms}) => {
const fieldMap = {};
// Go through all the connection forms and get the fields for each
connectionForms.forEach(({fields}) => fields.forEach(field => {
const { name, options } = field;
// Set the value to null to indicate that there are no options
if (!options)
fieldMap[name] = null;
// Set the value to a map of lowercased/trimmed option values
// to actual option values
else
fieldMap[name] = _.mapKeys(
options, option => option.trim().toLowerCase());
}));
return fieldMap;
}));
} }
/** /**
* Return a list of field-level errors for the provided connection, * Resolves to function that will perform field-level (not connection
* such as missing or invalid fields that are not dependant on the * hierarchy dependent) checks and transforms to a provided connection,
* connection group heirarchy. * returning the transformed connection.
* *
* @param {ImportConnection} connection * @returns {Promise.<Function.<ImportConnection, ImportConnection>>}
* The connection object to check field values on. * A promise resolving to a function that will apply field-level
* * transforms and checks to a provided connection, returning the
* @param {Object.<String, Boolean>} protocols * transformed connection.
* A set of valid protocols, such as the one returned by
* getValidProtocols().
*
* @returns {ParseError[]}
* A list of field-level errors for the provided connection.
*/ */
function getFieldErrors(connection, protocols) { function getFieldTransformer() {
const connectionErrors = [];
// Ensure that a protocol was specified for this connection return getProtocolParameterOptions().then(protocols => connection => {
const protocol = connection.protocol;
if (!protocol)
connectionErrors.push(new ParseError({
message: 'Missing required protocol field',
key: 'IMPORT.ERROR_REQUIRED_PROTOCOL_CONNECTION'
}));
// Ensure that a valid protocol was specified for this connection // Ensure that a protocol was specified for this connection
if (!protocols[protocol]) const protocol = connection.protocol;
connectionErrors.push(new ParseError({ if (!protocol)
message: 'Invalid protocol: ' + protocol, connection.errors.push(new ParseError({
key: 'IMPORT.ERROR_INVALID_PROTOCOL', message: 'Missing required protocol field',
variables: { PROTOCOL: protocol } key: 'IMPORT.ERROR_REQUIRED_PROTOCOL_CONNECTION'
})); }));
// Ensure that a name was specified for this connection // Ensure that a valid protocol was specified for this connection
if (!connection.name) if (!protocols[protocol])
connectionErrors.push(new ParseError({ connection.errors.push(new ParseError({
message: 'Missing required name field', message: 'Invalid protocol: ' + protocol,
key: 'IMPORT.ERROR_REQUIRED_NAME_CONNECTION' key: 'IMPORT.ERROR_INVALID_PROTOCOL',
})); variables: { PROTOCOL: protocol }
}));
// Ensure that the specified user list, if any, is an array // Ensure that a name was specified for this connection
const users = connection.users; if (!connection.name)
if (users && !Array.isArray(users)) connection.errors.push(new ParseError({
connectionErrors.push(new ParseError({ message: 'Missing required name field',
message: 'Invalid users list - must be an array', key: 'IMPORT.ERROR_REQUIRED_NAME_CONNECTION'
key: 'IMPORT.ERROR_INVALID_USERS_TYPE' }));
}));
// Ensure that the specified user list, if any, is an array // Ensure that the specified user list, if any, is an array
const groups = connection.groups; const users = connection.users;
if (groups && !Array.isArray(groups)) if (users && !Array.isArray(users))
connectionErrors.push(new ParseError({ connection.errors.push(new ParseError({
message: 'Invalid groups list - must be an array', message: 'Invalid users list - must be an array',
key: 'IMPORT.ERROR_INVALID_USER_GROUPS_TYPE' key: 'IMPORT.ERROR_INVALID_USERS_TYPE'
})); }));
return connectionErrors; // Ensure that the specified user list, if any, is an array
const groups = connection.groups;
if (groups && !Array.isArray(groups))
connection.errors.push(new ParseError({
message: 'Invalid groups list - must be an array',
key: 'IMPORT.ERROR_INVALID_USER_GROUPS_TYPE'
}));
// If the protocol is not valid, there's no point in trying to check
// parameter case sensitivity
if (!protocols[protocol])
return connection;
_.forEach(connection.parameters, (value, name) => {
// Convert the provided value to the format that would match
// the lookup object format
const comparisonValue = value.toLowerCase().trim();
// The validated / corrected option value for this connection
// parameter, if any
const validOptionValue = _.get(
protocols, [protocol, name, comparisonValue]);
// If the provided value fuzzily matches a valid option value,
// use the valid option value instead
if (validOptionValue)
connection.parameters[name] = validOptionValue;
});
return connection;
});
} }
/** /**
@@ -469,12 +530,12 @@ angular.module('import').factory('connectionParseService',
let index = 0; let index = 0;
// Get the tree transformer and valid protocol set // Get the tree transformer and relevant protocol information
return $q.all({ return $q.all({
treeTransformer : getTreeTransformer(importConfig), fieldTransformer : getFieldTransformer(),
protocols : getValidProtocols() treeTransformer : getTreeTransformer(importConfig),
}) })
.then(({treeTransformer, protocols}) => .then(({fieldTransformer, treeTransformer}) =>
connectionData.reduce((parseResult, data) => { connectionData.reduce((parseResult, data) => {
const { patches, users, groups, groupPaths } = parseResult; const { patches, users, groups, groupPaths } = parseResult;
@@ -485,26 +546,14 @@ angular.module('import').factory('connectionParseService',
connectionObject = transform(connectionObject); connectionObject = transform(connectionObject);
}); });
// All errors encountered while running the connection through the // Apply the field level transforms
// provided transform, starting with those encountered during connectionObject = fieldTransformer(connectionObject);
// the provided transforms, and any errors from missing fields
const connectionErrors = [
..._.get(connectionObject, 'errors', []),
...getFieldErrors(connectionObject, protocols)
];
// Determine the connection's place in the connection group tree // Apply the connection group hierarchy transforms
try { connectionObject = treeTransformer(connectionObject);
connectionObject = treeTransformer(connectionObject);
}
// If there was a problem with the connection group heirarchy
catch (error) {
connectionErrors.push(error);
}
// If there are any errors for this connection, fail the whole batch // If there are any errors for this connection, fail the whole batch
if (connectionErrors.length) if (connectionObject.errors.length)
parseResult.hasErrors = true; parseResult.hasErrors = true;
// The value for the patch is a full-fledged Connection // The value for the patch is a full-fledged Connection
@@ -559,7 +608,7 @@ angular.module('import').factory('connectionParseService',
groupPaths[index] = connectionObject.group; groupPaths[index] = connectionObject.group;
// Save the errors for this connection into the parse result // Save the errors for this connection into the parse result
parseResult.errors[index] = connectionErrors; parseResult.errors[index] = connectionObject.errors;
// Add this connection index to the list for each user // Add this connection index to the list for each user
_.forEach(connectionObject.users, identifier => { _.forEach(connectionObject.users, identifier => {
@@ -700,8 +749,10 @@ angular.module('import').factory('connectionParseService',
return deferred.promise; return deferred.promise;
} }
// Produce a ParseResult // Produce a ParseResult, making sure that each record is converted to
return parseConnectionData(importConfig, connectionData); // the ImportConnection type before further parsing
return parseConnectionData(importConfig, connectionData,
[connection => new ImportConnection(connection)]);
}; };
/** /**
@@ -740,8 +791,10 @@ angular.module('import').factory('connectionParseService',
return deferred.promise; return deferred.promise;
} }
// Produce a ParseResult // Produce a ParseResult, making sure that each record is converted to
return parseConnectionData(importConfig, connectionData); // the ImportConnection type before further parsing
return parseConnectionData(importConfig, connectionData,
[connection => new ImportConnection(connection)]);
}; };