From 6ff2ec0e72bcff07c85dffcdeccbec902cc77a6c Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 14 Apr 2023 18:42:29 +0000 Subject: [PATCH 1/4] GUACAMOLE-926: Fix attribute disambiguation suffix handling. --- .../frontend/src/app/import/services/connectionCSVService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9ab6a46d0..07af37e89 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -304,7 +304,7 @@ angular.module('import').factory('connectionCSVService', transformConfig.parameterOrAttributeGetters.push( row => ({ type: 'attributes', - name: parameterName, + name: attributeName, value: fetchFieldAtIndex(row) }) ); From d3f4bf06cc608aa401cd5a709aff76159334c95f Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 14 Apr 2023 18:42:48 +0000 Subject: [PATCH 2/4] GUACAMOLE-926: Better handling for invalid import files. --- .../importConnectionsController.js | 32 ++++++++++++++++++- .../import/services/connectionParseService.js | 27 ++++++++++------ .../main/frontend/src/translations/en.json | 6 +++- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index e30344e52..f78f262e6 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -50,6 +50,21 @@ const YAML_MIME_TYPES = [ 'application/yml' ]; +/** + * Possible signatures for zip files (which include most modern Microsoft office + * documents - most notable excel). If any file, regardless of extension, has + * these starting bytes, it's invalid and must be rejected. + * For more, see https://en.wikipedia.org/wiki/List_of_file_signatures and + * https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files. + * + * @type String[] + */ +const ZIP_SIGNATURES = [ + 'PK\u0003\u0004', + 'PK\u0005\u0006', + 'PK\u0007\u0008' +]; + /* * All file types supported for connection import. * @@ -640,8 +655,23 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ else { + const fileData = e.target.result; + + // Check if the file has a header of a known-bad type + if (_.some(ZIP_SIGNATURES, + signature => fileData.startsWith(signature))) { + + // Throw an error and abort processing + handleError(new ParseError({ + message: "Invalid file type detected", + key: 'IMPORT.ERROR_DETECTED_INVALID_TYPE' + })); + return; + + } + // Save the uploaded data - $scope.fileData = e.target.result; + $scope.fileData = fileData; // Mark the data as ready $scope.dataReady = true; 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 4951a5f83..61abd848e 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -354,12 +354,15 @@ angular.module('import').factory('connectionParseService', parsedData = parseCSVData(csvData, {skip_empty_lines: true}); } - // If the CSV parser throws an error, reject with that error. No - // translation key will be available here. + // If the CSV parser throws an error, reject with that error catch(error) { console.error(error); const deferred = $q.defer(); - deferred.reject(new ParseError({ message: error.message })); + deferred.reject(new ParseError({ + message: "CSV Parse Failure: "+ error.message, + key: "IMPORT.ERROR_PARSE_FAILURE_CSV", + variables: { ERROR: error.message } + })); return deferred.promise; } @@ -400,12 +403,15 @@ angular.module('import').factory('connectionParseService', connectionData = parseYAMLData(yamlData); } - // If the YAML parser throws an error, reject with that error. No - // translation key will be available here. + // If the YAML parser throws an error, reject with that error catch(error) { console.error(error); const deferred = $q.defer(); - deferred.reject(new ParseError({ message: error.message })); + deferred.reject(new ParseError({ + message: "YAML Parse Failure: "+ error.message, + key: "IMPORT.ERROR_PARSE_FAILURE_YAML", + variables: { ERROR: error.message } + })); return deferred.promise; } @@ -434,12 +440,15 @@ angular.module('import').factory('connectionParseService', connectionData = JSON.parse(jsonData); } - // If the JSON parse attempt throws an error, reject with that error. - // No translation key will be available here. + // If the JSON parse attempt throws an error, reject with that error catch(error) { console.error(error); const deferred = $q.defer(); - deferred.reject(new ParseError({ message: error.message })); + deferred.reject(new ParseError({ + message: "JSON Parse Failure: "+ error.message, + key: "IMPORT.ERROR_PARSE_FAILURE_JSON", + variables: { ERROR: error.message } + })); return deferred.promise; } diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 0dd455b04..12f126c9e 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -207,9 +207,13 @@ "ERROR_EMPTY_FILE": "The provided file is empty", "ERROR_INVALID_CSV_HEADER": "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter", "ERROR_INVALID_MIME_TYPE": "Unsupported file type: \"{TYPE}\"", + "ERROR_DETECTED_INVALID_TYPE": "Unsupported file type. Please make sure the file is valid CSV, JSON, or YAML.", "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_GROUP_IDENTIFIER": "No connection group with identifier \"{IDENTIFIER}\" found", "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_REQUIRED_NAME": "No connection name found in the provided file", "ERROR_REQUIRED_PROTOCOL": "No connection protocol found in the provided file", @@ -217,7 +221,7 @@ "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_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-seperated.¹", + "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.", "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.", From 3417b612c408cbf2d63d1b360f67ccf4620f5372 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Sat, 15 Apr 2023 04:15:24 +0000 Subject: [PATCH 3/4] GUACAMOLE-926: Explicitly catch expected CSV binary parse error and display generic error instead. --- .../import/services/connectionParseService.js | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) 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 61abd848e..e332ab781 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -22,6 +22,17 @@ import { parse as parseCSVData } from 'csv-parse/lib/sync' import { parse as parseYAMLData } from 'yaml' +/** + * A particularly unfriendly looking error that the CSV parser throws if a + * binary file parse attempt is made. If at all possible, this message should + * never be displayed to the user since it makes it look like the application + * is broken. As such, the code will attempt to filter out this error and print + * something a bit more generic. Lowercased for slightly fuzzier matching. + * + * @type String + */ +const BINARY_CSV_ERROR_MESSAGE = "Argument must be a Buffer".toLowerCase(); + /** * A service for parsing user-provided JSON, YAML, or JSON connection data into * an appropriate format for bulk uploading using the PATCH REST endpoint. @@ -356,13 +367,29 @@ angular.module('import').factory('connectionParseService', // If the CSV parser throws an error, reject with that error catch(error) { + + const message = error.message; console.error(error); + const deferred = $q.defer(); - deferred.reject(new ParseError({ - message: "CSV Parse Failure: "+ error.message, - key: "IMPORT.ERROR_PARSE_FAILURE_CSV", - variables: { ERROR: error.message } - })); + + // If the error message looks like the expected (and ugly) message + // that's thrown when a binary file is provided, throw a more + // friendy error. + if (_.trim(message).toLowerCase() == BINARY_CSV_ERROR_MESSAGE) + deferred.reject(new ParseError({ + message: "CSV binary parse attempt error: " + error.message, + key: "IMPORT.ERROR_DETECTED_INVALID_TYPE" + })); + + // Otherwise, pass the error from the library through to the user + else + deferred.reject(new ParseError({ + message: "CSV Parse Failure: " + error.message, + key: "IMPORT.ERROR_PARSE_FAILURE_CSV", + variables: { ERROR: error.message } + })); + return deferred.promise; } @@ -408,7 +435,7 @@ angular.module('import').factory('connectionParseService', console.error(error); const deferred = $q.defer(); deferred.reject(new ParseError({ - message: "YAML Parse Failure: "+ error.message, + message: "YAML Parse Failure: " + error.message, key: "IMPORT.ERROR_PARSE_FAILURE_YAML", variables: { ERROR: error.message } })); @@ -445,7 +472,7 @@ angular.module('import').factory('connectionParseService', console.error(error); const deferred = $q.defer(); deferred.reject(new ParseError({ - message: "JSON Parse Failure: "+ error.message, + message: "JSON Parse Failure: " + error.message, key: "IMPORT.ERROR_PARSE_FAILURE_JSON", variables: { ERROR: error.message } })); From a736e062c7fbf13b705205e97a052ae6a952579f Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 17 Apr 2023 15:42:15 +0000 Subject: [PATCH 4/4] GUACAMOLE-926: Correct spacing in import translations. --- .../main/frontend/src/translations/en.json | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 12f126c9e..e8013e93d 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -189,61 +189,61 @@ "IMPORT": { - "ACTION_ACKNOWLEDGE": "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_BROWSE": "Browse for File", - "ACTION_CANCEL": "@:APP.ACTION_CANCEL", - "ACTION_CLEAR": "@:APP.ACTION_CLEAR", - "ACTION_VIEW_FORMAT_HELP": "View Format Tips", - "ACTION_IMPORT": "@:APP.ACTION_IMPORT", - "ACTION_IMPORT_CONNECTIONS": "Import Connections", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_BROWSE" : "Browse for File", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLEAR" : "@:APP.ACTION_CLEAR", + "ACTION_VIEW_FORMAT_HELP" : "View Format Tips", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_IMPORT_CONNECTIONS" : "Import Connections", - "DIALOG_HEADER_ERROR": "@:APP.DIALOG_HEADER_ERROR", - "DIALOG_HEADER_SUCCESS": "Success", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_SUCCESS" : "Success", - "ERROR_AMBIGUOUS_CSV_HEADER": "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", - "ERROR_AMBIGUOUS_PARENT_GROUP": "Both group and parentIdentifier may be not specified at the same time", - "ERROR_ARRAY_REQUIRED": "The provided file must contain a list of connections", - "ERROR_DUPLICATE_CSV_HEADER": "Duplicate CSV Header: {HEADER}", - "ERROR_EMPTY_FILE": "The provided file is empty", - "ERROR_INVALID_CSV_HEADER": "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter", - "ERROR_INVALID_MIME_TYPE": "Unsupported file type: \"{TYPE}\"", - "ERROR_DETECTED_INVALID_TYPE": "Unsupported file type. Please make sure the file is valid CSV, JSON, or YAML.", - "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", - "ERROR_INVALID_GROUP_IDENTIFIER": "No connection group with identifier \"{IDENTIFIER}\" found", - "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_REQUIRED_NAME": "No connection name found in the provided file", - "ERROR_REQUIRED_PROTOCOL": "No connection protocol found in the provided file", + "ERROR_AMBIGUOUS_CSV_HEADER" : "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", + "ERROR_AMBIGUOUS_PARENT_GROUP" : "Both group and parentIdentifier may be not specified at the same time", + "ERROR_ARRAY_REQUIRED" : "The provided file must contain a list of connections", + "ERROR_DUPLICATE_CSV_HEADER" : "Duplicate CSV Header: {HEADER}", + "ERROR_EMPTY_FILE" : "The provided file is empty", + "ERROR_INVALID_CSV_HEADER" : "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter", + "ERROR_INVALID_MIME_TYPE" : "Unsupported file type: \"{TYPE}\"", + "ERROR_DETECTED_INVALID_TYPE" : "Unsupported file type. Please make sure the file is valid CSV, JSON, or YAML.", + "ERROR_INVALID_GROUP" : "No group matching \"{GROUP}\" found", + "ERROR_INVALID_GROUP_IDENTIFIER" : "No connection group with identifier \"{IDENTIFIER}\" found", + "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_REQUIRED_NAME" : "No connection name found in the provided file", + "ERROR_REQUIRED_PROTOCOL" : "No connection protocol found in the provided file", - "FIELD_PLACEHOLDER_FILTER": "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - "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_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.", - "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_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_SEMICOLON_FOOTNOTE": "If present, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", - "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", - "INFO_CONNECTIONS_IMPORTED_SUCCESS": "{NUMBER} {NUMBER, plural, one{connection} other{connections}} imported successfully.", + "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_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.", + "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_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_SEMICOLON_FOOTNOTE" : "If present, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", + "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", + "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{connection} other{connections}} imported successfully.", - "SECTION_HEADER_CONNECTION_IMPORT": "Connection Import", - "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE": "Connection Import File Format", - "SECTION_HEADER_CSV": "CSV Format", - "SECTION_HEADER_JSON": "JSON Format", - "SECTION_HEADER_YAML": "YAML Format", + "SECTION_HEADER_CONNECTION_IMPORT" : "Connection Import", + "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE" : "Connection Import File Format", + "SECTION_HEADER_CSV" : "CSV Format", + "SECTION_HEADER_JSON" : "JSON Format", + "SECTION_HEADER_YAML" : "YAML Format", - "TABLE_HEADER_ERRORS": "Errors", - "TABLE_HEADER_NAME": "Name", - "TABLE_HEADER_PROTOCOL": "Protocol", - "TABLE_HEADER_ROW_NUMBER": "Row #" + "TABLE_HEADER_ERRORS" : "Errors", + "TABLE_HEADER_NAME" : "Name", + "TABLE_HEADER_PROTOCOL" : "Protocol", + "TABLE_HEADER_ROW_NUMBER" : "Row #" }, "DATA_SOURCE_DEFAULT" : {