GUACAMOLE-926: Merge further batch import fixes and improvements

This commit is contained in:
Virtually Nick
2023-05-01 07:49:40 -04:00
committed by GitHub
4 changed files with 162 additions and 26 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

@@ -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 || [];
};

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",
@@ -225,12 +231,12 @@
"FIELD_HEADER_EXISTING_PERMISSION_MODE" : "Reset permissions",
"HELP_CSV_DESCRIPTION" : "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.",
"HELP_CSV_EXAMPLE" : "name,protocol,hostname,group,users,groups,guacd-encryption (attribute)\nconn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none\nconn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl\nconn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,\nconn4,kubernetes,,,,,",
"HELP_CSV_EXAMPLE" : "name,protocol,username,password,hostname,group,users,groups,guacd-encryption (attribute)\nconn1,vnc,alice,pass1,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none\nconn2,rdp,bob,pass2,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl\nconn3,ssh,carol,pass3,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,\nconn4,kubernetes,,,,,,,",
"HELP_CSV_MORE_DETAILS" : "The CSV header for each row specifies the connection field. The connection group ID that the connection should be imported into may be directly specified with \"parentIdentifier\", or the path to the parent group may be specified using \"group\" as shown below. In most cases, there should be no conflict between fields, but if needed, an \" (attribute)\" or \" (parameter)\" suffix may be added to disambiguate. Lists of user or user group identifiers must be semicolon-separated.¹",
"HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created. Note that any existing connection permissions will not be removed for updated connections, unless \"Reset permissions\" is checked.",
"HELP_FILE_TYPE_HEADER" : "File Types",
"HELP_JSON_DESCRIPTION" : "A connection import JSON file is a list of connection objects. At minimum the connection name and protocol must be specified in each connection object.",
"HELP_JSON_EXAMPLE" : "[\n \\{\n \"name\": \"conn1\",\n \"protocol\": \"vnc\",\n \"parameters\": \\{ \"hostname\": \"conn1.web.com\" \\},\n \"parentIdentifier\": \"ROOT\",\n \"users\": [ \"guac user 1\", \"guac user 2\" ],\n \"groups\": [ \"Connection 1 Users\" ],\n \"attributes\": \\{ \"guacd-encryption\": \"none\" \\}\n \\},\n \\{\n \"name\": \"conn2\",\n \"protocol\": \"rdp\",\n \"parameters\": \\{ \"hostname\": \"conn2.web.com\" \\},\n \"group\": \"ROOT/Parent Group\",\n \"users\": [ \"guac user 1\" ],\n \"attributes\": \\{ \"guacd-encryption\": \"none\" \\}\n \\},\n \\{\n \"name\": \"conn3\",\n \"protocol\": \"ssh\",\n \"parameters\": \\{ \"hostname\": \"conn3.web.com\" \\},\n \"group\": \"ROOT/Parent Group/Child Group\",\n \"users\": [ \"guac user 2\", \"guac user 3\" ]\n \\},\n \\{\n \"name\": \"conn4\",\n \"protocol\": \"kubernetes\"\n \\}\n]",
"HELP_JSON_EXAMPLE" : "[\n \\{\n \"name\": \"conn1\",\n \"protocol\": \"vnc\",\n \"parameters\": \\{ \"username\": \"alice\", \"password\": \"pass1\", \"hostname\": \"conn1.web.com\" \\},\n \"parentIdentifier\": \"ROOT\",\n \"users\": [ \"guac user 1\", \"guac user 2\" ],\n \"groups\": [ \"Connection 1 Users\" ],\n \"attributes\": \\{ \"guacd-encryption\": \"none\" \\}\n \\},\n \\{\n \"name\": \"conn2\",\n \"protocol\": \"rdp\",\n \"parameters\": \\{ \"username\": \"bob\", \"password\": \"pass2\", \"hostname\": \"conn2.web.com\" \\},\n \"group\": \"ROOT/Parent Group\",\n \"users\": [ \"guac user 1\" ],\n \"attributes\": \\{ \"guacd-encryption\": \"none\" \\}\n \\},\n \\{\n \"name\": \"conn3\",\n \"protocol\": \"ssh\",\n \"parameters\": \\{ \"username\": \"carol\", \"password\": \"pass3\", \"hostname\": \"conn3.web.com\" \\},\n \"group\": \"ROOT/Parent Group/Child Group\",\n \"users\": [ \"guac user 2\", \"guac user 3\" ]\n \\},\n \\{\n \"name\": \"conn4\",\n \"protocol\": \"kubernetes\"\n \\}\n]",
"HELP_JSON_MORE_DETAILS" : "The connection group ID that the connection should be imported into may be directly specified with a \"parentIdentifier\" field, or the path to the parent group may be specified using a \"group\" field as shown below. An array of user and user group identifiers to grant access to may be specified per connection.",
"HELP_EXISTING_CONNECTION_MODE" : "Entirely replace/update existing connections if their names and parent connection groups match the values in the provided file. If unchecked, attempting to import a connection with the same name and parent connection group of an existing connection will be considered an error.",
"HELP_EXISTING_PERMISSION_MODE" : "Fully reset the permissions granted for all connections in the provided file to the permissions specified in that file. If no permissions are specified, all relevant connection permissions will be revoked. If unchecked, existing permissions are preserved, and any permissions specified in the file will be added.",
@@ -238,7 +244,7 @@
"HELP_UPLOAD_DROP_TITLE" : "Drop a File Here",
"HELP_UPLOAD_FILE_TYPES" : "CSV, JSON, or YAML",
"HELP_YAML_DESCRIPTION" : "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.",
"HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes",
"HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n username: alice\n password: pass1\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n username: bob\n password: pass2\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n username: carol\n password: pass3\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes",
"INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{connection} other{connections}} imported successfully.",