GUACAMOLE-926: Improve error handling for invalid, missing connection data.

This commit is contained in:
James Muehlner
2023-04-28 18:25:32 +00:00
parent 823df2d10b
commit 4b570e9e63
4 changed files with 159 additions and 23 deletions

View File

@@ -322,10 +322,32 @@ angular.module('import').factory('connectionCSVService',
// of the header
const value = fetchFieldAtIndex(row);
// If no value is provided, do not check the validity
// of the parameter/attribute. Doing so would prevent
// the import of a list of mixed protocol types, where
// fields are only populated for protocols for which
// they are valid parameters. If a value IS provided,
// it must be a valid parameter or attribute for the
// current protocol, which will be checked below.
if (!value)
return {};
// The protocol may determine whether a field is
// a parameter or an attribute (or both)
const protocol = transformConfig.protocolGetter(row);
// Any errors encountered while processing this row
const errors = [];
// Before checking whether it's an attribute or protocol,
// make sure this is a valid protocol to start
if (!protocolParameters[protocol])
// If the protocol is invalid, do not throw an error
// here - this will be handled further downstream
// by non-CSV-specific error handling
return {};
// Determine if the field refers to an attribute or a
// parameter (or both, which is an error)
const isAttribute = !!attributes[name];
@@ -336,24 +358,24 @@ angular.module('import').factory('connectionCSVService',
// parameter with the provided name, it's impossible to
// figure out which this should be
if (isAttribute && isParameter)
throw new ParseError({
errors.push(new ParseError({
message: 'Ambiguous CSV Header: ' + header,
key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER',
variables: { HEADER: header }
});
}));
// It's neither an attribute or a parameter
else if (!isAttribute && !isParameter)
throw new ParseError({
errors.push(new ParseError({
message: 'Invalid CSV Header: ' + header,
key: 'IMPORT.ERROR_INVALID_CSV_HEADER',
variables: { HEADER: header }
});
}));
// Choose the appropriate type
const type = isAttribute ? 'attributes' : 'parameters';
return { type, name, value };
return { type, name, value, errors };
});
});
@@ -364,19 +386,20 @@ angular.module('import').factory('connectionCSVService',
parameterOrAttributeGetters
} = transformConfig;
// Fail if the name wasn't provided
// Fail if the name wasn't provided. Note that this is a file-level
// error, not specific to any connection.
if (!nameGetter)
return deferred.reject(new ParseError({
throw new ParseError({
message: 'The connection name must be provided',
key: 'IMPORT.ERROR_REQUIRED_NAME'
}));
key: 'IMPORT.ERROR_REQUIRED_NAME_FILE'
});
// Fail if the protocol wasn't provided
if (!protocolGetter)
return deferred.reject(new ParseError({
throw new ParseError({
message: 'The connection protocol must be provided',
key: 'IMPORT.ERROR_REQUIRED_PROTOCOL'
}));
key: 'IMPORT.ERROR_REQUIRED_PROTOCOL_FILE'
});
// The function to transform a CSV row into a connection object
deferred.resolve(function transformCSVRow(row) {
@@ -409,14 +432,20 @@ angular.module('import').factory('connectionCSVService',
...parameterOrAttributeGetters.reduce((values, getter) => {
// Determine the type, name, and value
const { type, name, value } = getter(row);
const { type, name, value, errors } = getter(row);
// Set the value and continue on to the next attribute
// or parameter
values[type][name] = value;
// Set the value if available
if (type && name && value)
values[type][name] = value;
// If there were errors
if (errors && errors.length)
values.errors = [...values.errors, ...errors];
// Continue on to the next attribute or parameter
return values;
}, {parameters: {}, attributes: {}})
}, {parameters: {}, attributes: {}, errors: []})
});

View File

@@ -271,6 +271,13 @@ angular.module('import').factory('connectionParseService',
// to be translated into an absolute path starting at the root
group = connection.group;
// If the provided group isn't a string, it can never be valid
if (typeof group !== 'string')
throw new ParseError({
message: 'Invalid group type - must be a string',
key: 'IMPORT.ERROR_INVALID_GROUP_TYPE'
});
// Allow the group to start with a leading slash instead instead
// of explicitly requiring the root connection group
if (group.startsWith('/'))
@@ -351,6 +358,84 @@ angular.module('import').factory('connectionParseService',
});
}
/**
* Returns a promise that resolves to a map of all valid protocols to the
* boolean value "true", i.e. a set of all valid protocols.
*
* @returns {Promise.<Object.<String, Boolean>>}
* A promise that resolves to a set of all valid protocols.
*/
function getValidProtocols() {
// The current data source - the one that the connections will be
// imported into
const dataSource = $routeParams.dataSource;
// Fetch the protocols and convert to a set of valid protocol names
return schemaService.getProtocols(dataSource).then(
protocols => _.mapValues(protocols, () => true));
}
/**
* Return a list of field-level errors for the provided connection,
* such as missing or invalid fields that are not dependant on the
* connection group heirarchy.
*
* @param {ImportConnection} connection
* The connection object to check field values on.
*
* @param {Object.<String, Boolean>} protocols
* 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) {
const connectionErrors = [];
// Ensure that a protocol was specified for this 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
if (!protocols[protocol])
connectionErrors.push(new ParseError({
message: 'Invalid protocol: ' + protocol,
key: 'IMPORT.ERROR_INVALID_PROTOCOL',
variables: { PROTOCOL: protocol }
}));
// Ensure that a name was specified for this connection
if (!connection.name)
connectionErrors.push(new ParseError({
message: 'Missing required name field',
key: 'IMPORT.ERROR_REQUIRED_NAME_CONNECTION'
}));
// Ensure that the specified user list, if any, is an array
const users = connection.users;
if (users && !Array.isArray(users))
connectionErrors.push(new ParseError({
message: 'Invalid users list - must be an array',
key: 'IMPORT.ERROR_INVALID_USERS_TYPE'
}));
// Ensure that the specified user list, if any, is an array
const groups = connection.groups;
if (groups && !Array.isArray(groups))
connectionErrors.push(new ParseError({
message: 'Invalid groups list - must be an array',
key: 'IMPORT.ERROR_INVALID_USER_GROUPS_TYPE'
}));
return connectionErrors;
}
/**
* Convert a provided connection array into a ParseResult. Any provided
* transform functions will be run on each entry in `connectionData` before
@@ -384,8 +469,12 @@ angular.module('import').factory('connectionParseService',
let index = 0;
// Get the group transformer to apply to each connection
return getTreeTransformer(importConfig).then(treeTransformer =>
// Get the tree transformer and valid protocol set
return $q.all({
treeTransformer : getTreeTransformer(importConfig),
protocols : getValidProtocols()
})
.then(({treeTransformer, protocols}) =>
connectionData.reduce((parseResult, data) => {
const { patches, users, groups, groupPaths } = parseResult;
@@ -396,8 +485,13 @@ angular.module('import').factory('connectionParseService',
connectionObject = transform(connectionObject);
});
// All errors found while parsing this connection
const connectionErrors = [];
// All errors encountered while running the connection through the
// provided transform, starting with those encountered during
// 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
try {

View File

@@ -114,6 +114,13 @@ angular.module('import').factory('ImportConnection', ['$injector',
*/
this.importMode = template.importMode || ImportConnection.ImportMode.CREATE;
/**
* Any errors specific to this connection encountered while parsing.
*
* @type ParseError[]
*/
this.errors = template.errors || [];
};
/**

View File

@@ -211,13 +211,19 @@
"ERROR_INVALID_MIME_TYPE" : "Unsupported file type: \"{TYPE}\"",
"ERROR_INVALID_GROUP" : "No group matching \"{GROUP}\" found",
"ERROR_INVALID_GROUP_IDENTIFIER" : "No connection group with identifier \"{IDENTIFIER}\" found",
"ERROR_INVALID_GROUP_TYPE" : "Invalid group - must be a string.",
"ERROR_INVALID_PROTOCOL" : "Invalid protocol \"{PROTOCOL}\"",
"ERROR_INVALID_USER_GROUPS_TYPE" : "Invalid user groups - must be an array of user group identifiers.",
"ERROR_INVALID_USERS_TYPE" : "Invalid users - must be an array of user identifiers.",
"ERROR_NO_FILE_SUPPLIED" : "Please select a file to import",
"ERROR_PARSE_FAILURE_CSV" : "Please make sure your file is valid CSV. Parsing failed with error \"{ERROR}\". ",
"ERROR_PARSE_FAILURE_JSON" : "Please make sure your file is valid JSON. Parsing failed with error \"{ERROR}\". ",
"ERROR_PARSE_FAILURE_YAML" : "Please make sure your file is valid YAML. Parsing failed with error \"{ERROR}\". ",
"ERROR_REJECT_UPDATE_CONNECTION" : "Connection \"{NAME}\" already exists at \"{PATH}\"",
"ERROR_REQUIRED_NAME" : "No connection name found in the provided file",
"ERROR_REQUIRED_PROTOCOL" : "No connection protocol found in the provided file",
"ERROR_REQUIRED_NAME_CONNECTION" : "The connection name is required",
"ERROR_REQUIRED_PROTOCOL_CONNECTION" : "The connection protocol is required",
"ERROR_REQUIRED_NAME_FILE" : "No connection name found in the provided file",
"ERROR_REQUIRED_PROTOCOL_FILE" : "No connection protocol found in the provided file",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",