diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index 07af37e89..18a024865 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -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: []}) }); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index a9b2b7241..acf444d5c 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -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.>} + * 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.} 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 { diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js index 9a519dedb..47c13a8eb 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js @@ -113,6 +113,13 @@ angular.module('import').factory('ImportConnection', ['$injector', * a brand new connection should be created. */ this.importMode = template.importMode || ImportConnection.ImportMode.CREATE; + + /** + * Any errors specific to this connection encountered while parsing. + * + * @type ParseError[] + */ + this.errors = template.errors || []; }; diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 972bab93d..30782fc67 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -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",