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 // of the header
const value = fetchFieldAtIndex(row); 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 // The protocol may determine whether a field is
// a parameter or an attribute (or both) // a parameter or an attribute (or both)
const protocol = transformConfig.protocolGetter(row); 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 // Determine if the field refers to an attribute or a
// parameter (or both, which is an error) // parameter (or both, which is an error)
const isAttribute = !!attributes[name]; const isAttribute = !!attributes[name];
@@ -336,24 +358,24 @@ angular.module('import').factory('connectionCSVService',
// parameter with the provided name, it's impossible to // parameter with the provided name, it's impossible to
// figure out which this should be // figure out which this should be
if (isAttribute && isParameter) if (isAttribute && isParameter)
throw new ParseError({ errors.push(new ParseError({
message: 'Ambiguous CSV Header: ' + header, message: 'Ambiguous CSV Header: ' + header,
key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER',
variables: { HEADER: header } variables: { HEADER: header }
}); }));
// It's neither an attribute or a parameter // It's neither an attribute or a parameter
else if (!isAttribute && !isParameter) else if (!isAttribute && !isParameter)
throw new ParseError({ errors.push(new ParseError({
message: 'Invalid CSV Header: ' + header, message: 'Invalid CSV Header: ' + header,
key: 'IMPORT.ERROR_INVALID_CSV_HEADER', key: 'IMPORT.ERROR_INVALID_CSV_HEADER',
variables: { HEADER: header } variables: { HEADER: header }
}); }));
// Choose the appropriate type // Choose the appropriate type
const type = isAttribute ? 'attributes' : 'parameters'; const type = isAttribute ? 'attributes' : 'parameters';
return { type, name, value }; return { type, name, value, errors };
}); });
}); });
@@ -364,19 +386,20 @@ angular.module('import').factory('connectionCSVService',
parameterOrAttributeGetters parameterOrAttributeGetters
} = transformConfig; } = 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) if (!nameGetter)
return deferred.reject(new ParseError({ throw new ParseError({
message: 'The connection name must be provided', 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 // Fail if the protocol wasn't provided
if (!protocolGetter) if (!protocolGetter)
return deferred.reject(new ParseError({ throw new ParseError({
message: 'The connection protocol must be provided', 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 // The function to transform a CSV row into a connection object
deferred.resolve(function transformCSVRow(row) { deferred.resolve(function transformCSVRow(row) {
@@ -409,14 +432,20 @@ angular.module('import').factory('connectionCSVService',
...parameterOrAttributeGetters.reduce((values, getter) => { ...parameterOrAttributeGetters.reduce((values, getter) => {
// Determine the type, name, and value // 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 // Set the value if available
// or parameter if (type && name && value)
values[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; 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 // to be translated into an absolute path starting at the root
group = connection.group; 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 // 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
if (group.startsWith('/')) 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 * Convert a provided connection 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
@@ -384,8 +469,12 @@ angular.module('import').factory('connectionParseService',
let index = 0; let index = 0;
// Get the group transformer to apply to each connection // Get the tree transformer and valid protocol set
return getTreeTransformer(importConfig).then(treeTransformer => return $q.all({
treeTransformer : getTreeTransformer(importConfig),
protocols : getValidProtocols()
})
.then(({treeTransformer, protocols}) =>
connectionData.reduce((parseResult, data) => { connectionData.reduce((parseResult, data) => {
const { patches, users, groups, groupPaths } = parseResult; const { patches, users, groups, groupPaths } = parseResult;
@@ -396,8 +485,13 @@ angular.module('import').factory('connectionParseService',
connectionObject = transform(connectionObject); connectionObject = transform(connectionObject);
}); });
// All errors found while parsing this connection // All errors encountered while running the connection through the
const connectionErrors = []; // 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 // Determine the connection's place in the connection group tree
try { try {

View File

@@ -113,6 +113,13 @@ angular.module('import').factory('ImportConnection', ['$injector',
* a brand new connection should be created. * a brand new connection should be created.
*/ */
this.importMode = template.importMode || ImportConnection.ImportMode.CREATE; 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_MIME_TYPE" : "Unsupported file type: \"{TYPE}\"",
"ERROR_INVALID_GROUP" : "No group matching \"{GROUP}\" found", "ERROR_INVALID_GROUP" : "No group matching \"{GROUP}\" found",
"ERROR_INVALID_GROUP_IDENTIFIER" : "No connection group with identifier \"{IDENTIFIER}\" 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_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_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_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_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_REJECT_UPDATE_CONNECTION" : "Connection \"{NAME}\" already exists at \"{PATH}\"",
"ERROR_REQUIRED_NAME" : "No connection name found in the provided file", "ERROR_REQUIRED_NAME_CONNECTION" : "The connection name is required",
"ERROR_REQUIRED_PROTOCOL" : "No connection protocol found in the provided file", "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", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",