From 761438e02d4a86a4b12269f2494a431860cfa794 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 9 Feb 2023 01:51:14 +0000 Subject: [PATCH] GUACAMOLE-926: Also accept user and group identifiers to post in a seperate request. --- .../import/services/connectionCSVService.js | 138 ++++++-- .../import/services/connectionParseService.js | 322 +++++++++++------- .../src/app/import/types/ParseError.js | 2 +- .../src/app/import/types/ParseResult.js | 87 +++++ .../src/app/import/types/ProtoConnection.js | 111 ++++++ .../main/frontend/src/translations/en.json | 4 + 6 files changed, 522 insertions(+), 142 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/types/ParseResult.js create mode 100644 guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js 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 ee14f809b..a104cdd46 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -33,6 +33,7 @@ angular.module('import').factory('connectionCSVService', // Required types const ParseError = $injector.get('ParseError'); + const ProtoConnection = $injector.get('ProtoConnection'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -43,7 +44,7 @@ angular.module('import').factory('connectionCSVService', const service = {}; /** - * Returns a promise that resolves to an object detailing the connection + * Returns a promise that resolves to a object detailing the connection * attributes for the current data source, as well as the connection * paremeters for every protocol, for the current data source. * @@ -96,12 +97,66 @@ angular.module('import').factory('connectionCSVService', }; }); } + + /** + * Split a raw user-provided, semicolon-seperated list of identifiers into + * an array of identifiers. If identifiers contain semicolons, they can be + * escaped with backslashes, and backslashes can also be escaped using other + * backslashes. + * + * @param {type} rawIdentifiers + * The raw string value as fetched from the CSV. + * + * @returns {Array.} + * An array of identifier values. + */ + function splitIdentifiers(rawIdentifiers) { + + // Keep track of whether a backslash was seen + let escaped = false; + + return _.reduce(rawIdentifiers, (identifiers, ch) => { + + // The current identifier will be the last one in the final list + let identifier = identifiers[identifiers.length - 1]; + + // If a semicolon is seen, set the "escaped" flag and continue + // to the next character + if (!escaped && ch == '\\') { + escaped = true; + return identifiers; + } + + // End the current identifier and start a new one if there's an + // unescaped semicolon + else if (!escaped && ch == ';') { + identifiers.push(''); + return identifiers; + } + + // In all other cases, just append to the identifier + else { + identifier += ch; + escaped = false; + } + + // Save the updated identifier to the list + identifiers[identifiers.length - 1] = identifier; + + return identifiers; + + }, ['']) + + // Filter out any 0-length (empty) identifiers + .filter(identifier => identifier.length); + + } /** * Given a CSV header row, create and return a promise that will resolve to - * a function that can take a CSV data row and return a connection object. - * If an error occurs while parsing a particular row, the resolved function - * will throw a ParseError describing the failure. + * a function that can take a CSV data row and return a ProtoConnection + * object. If an error occurs while parsing a particular row, the resolved + * function will throw a ParseError describing the failure. * * The provided CSV must contain columns for name and protocol. Optionally, * the parentIdentifier of the target parent connection group, or a connection @@ -120,14 +175,17 @@ angular.module('import').factory('connectionCSVService', * required. * * This returned object will be very similar to the Connection type, with - * the exception that a human-readable "group" field may be present. + * the exception that a human-readable "group" field may be present, in + * addition to "user" and "userGroup" fields containing arrays of user and + * user group identifiers for whom read access should be granted to this + * connection. * * If a failure occurs while attempting to create the transformer function, * the promise will be rejected with a ParseError describing the failure. * * @returns {Promise.>} * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a connection object. + * row (array of strings) to a ProtoConnection object. */ service.getCSVTransformer = function getCSVTransformer(headerRow) { @@ -149,8 +207,12 @@ angular.module('import').factory('connectionCSVService', protocolGetter: undefined, // Callbacks for a parent group ID or group path - groupGetter: _.noop, - parentIdentifierGetter: _.noop, + groupGetter: undefined, + parentIdentifierGetter: undefined, + + // Callbacks for user and user group identifiers + usersGetter: () => [], + userGroupsGetter: () => [], // Callbacks that will generate either connection attributes or // parameters. These callbacks will return a {type, name, value} @@ -188,6 +250,11 @@ angular.module('import').factory('connectionCSVService', // A callback that returns the field at the current index const fetchFieldAtIndex = row => row[index]; + + // A callback that splits identifier lists by semicolon + // characters into a javascript list of identifiers + const identifierListCallback = row => + splitIdentifiers(fetchFieldAtIndex(row)); // Set up the name callback if (header == 'name') @@ -203,7 +270,18 @@ angular.module('import').factory('connectionCSVService', // Set up the group parent ID callback else if (header == 'parentIdentifier') - transformConfig.parentIdentifierGetter = fetchFieldAtIndex; + transformConfig.parentIdentifierGetter = ( + identifierListCallback); + + // Set the user identifiers callback + else if (header == 'users') + transformConfig.usersGetter = ( + identifierListCallback); + + // Set the user group identifiers callback + else if (header == 'groups') + transformConfig.userGroupsGetter = ( + identifierListCallback); // At this point, any other header might refer to a connection // parameter or to an attribute @@ -282,47 +360,61 @@ angular.module('import').factory('connectionCSVService', return { type, name, value }; }); }); + + const { + nameGetter, protocolGetter, + parentIdentifierGetter, groupGetter, + usersGetter, userGroupsGetter, + parameterOrAttributeGetters + } = transformConfig; // Fail if the name wasn't provided - if (!transformConfig.nameGetter) + if (!nameGetter) return deferred.reject(new ParseError({ message: 'The connection name must be provided', key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' })); // Fail if the protocol wasn't provided - if (!transformConfig.protocolGetter) + if (!protocolGetter) return deferred.reject(new ParseError({ message: 'The connection protocol must be provided', key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' })); + + // If both are specified, the parent group is ambigious + if (parentIdentifierGetter && groupGetter) + throw new ParseError({ + message: 'Only one of group or parentIdentifier can be set', + key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + }); // The function to transform a CSV row into a connection object deferred.resolve(function transformCSVRow(row) { - - const { - nameGetter, protocolGetter, - parentIdentifierGetter, groupGetter, - parameterOrAttributeGetters - } = transformConfig; - // Set name and protocol + // Get name and protocol const name = nameGetter(row); const protocol = protocolGetter(row); + + // Get any users or user groups who should be granted access + const users = usersGetter(row); + const groups = userGroupsGetter(row); - // Set the parent group ID and/or group path + // Get the parent group ID and/or group path const group = groupGetter && groupGetter(row); const parentIdentifier = ( parentIdentifierGetter && parentIdentifierGetter(row)); - return { + return new ProtoConnection({ - // Simple fields that are not protocol-specific + // Fields that are not protocol-specific ...{ name, protocol, parentIdentifier, - group + group, + users, + groups }, // Fields that might potentially be either attributes or @@ -339,7 +431,7 @@ angular.module('import').factory('connectionCSVService', }, {parameters: {}, attributes: {}}) - } + }); }); 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 322e22c15..f2f6c1daa 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -33,6 +33,7 @@ angular.module('import').factory('connectionParseService', const Connection = $injector.get('Connection'); const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); + const ParseResult = $injector.get('ParseResult'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -41,8 +42,81 @@ angular.module('import').factory('connectionParseService', const schemaService = $injector.get('schemaService'); const connectionCSVService = $injector.get('connectionCSVService'); const connectionGroupService = $injector.get('connectionGroupService'); + const userService = $injector.get('userService'); + const userGroupService = $injector.get('userGroupService'); const service = {}; + + /** + * Resolves to an object whose keys are all valid identifiers in the current + * data source. The provided `requestFunction` should resolve to such an + * object when provided with the current data source. + * + * @param {Function.>} requestFunction + * A function that, given a data source, will return a promise resolving + * to an object with keys that are unique identifiers for entities in + * that data source. + * + * @returns {Promise.>} + * A promise that will resolve to an function that, given an array of + * identifiers, will return all identifiers that do not exist as keys in + * the object returned by `requestFunction`, i.e. all identifiers that + * do not exist in the current data source. + */ + function getIdentifierMap(requestFunction) { + + // The current data source to which all the identifiers will belong + const dataSource = $routeParams.dataSource; + + // Make the request and return the response, which should be an object + // whose keys are all valid identifiers for the current data source + return requestFunction(dataSource); + } + + /** + * Return a promise that resolves to a function that takes an array of + * identifiers, returning a ParseError describing the invalid identifiers + * from the list. The provided `requestFunction` should resolve to such an + * object whose keys are all valid identifiers when provided with the + * current data source. + * + * @param {Function.>} requestFunction + * A function that, given a data source, will return a promise resolving + * to an object with keys that are unique identifiers for entities in + * that data source. + * + * @returns {Promise.>} + * A promise that will resolve to a function to check the validity of + * each identifier in a provided array, returning a ParseError + * describing the problem if any are not valid. + */ + function getInvalidIdentifierErrorChecker(requestFunction) { + + // Fetch all the valid user identifiers in the system, and + return getIdentifierMap(requestFunction).then(validIdentifiers => + + // The resolved function that takes a list of user group identifiers + allIdentifiers => { + + // Filter to only include invalid identifiers + const invalidIdentifiers = _.filter(allIdentifiers, + identifier => !validIdentifiers[identifier]); + + if (invalidIdentifiers.length) { + + // Quote and comma-seperate for display + const identifierList = invalidIdentifiers.map( + identifier => '"' + identifier + '"').join(', '); + + return new ParseError({ + message: 'Invalid User Group Identifiers: ' + identifierList, + key: 'CONNECTION_IMPORT.ERROR_INVALID_USER_GROUP_IDENTIFIERS', + variables: { IDENTIFIER_LIST: identifierList } + }); + } + + }); + } /** * Perform basic checks, common to all file types - namely that the parsed @@ -127,9 +201,9 @@ angular.module('import').factory('connectionParseService', /** * Returns a promise that will resolve to a transformer function that will - * take an object that may a "group" field, replacing it if present with a - * "parentIdentifier". If both a "group" and "parentIdentifier" field are - * present on the provided object, or if no group exists at the specified + * take an object that may contain a "group" field, replacing it if present + * with a "parentIdentifier". If both a "group" and "parentIdentifier" field + * are present on the provided object, or if no group exists at the specified * path, the function will throw a ParseError describing the failure. * * @returns {Promise.>} @@ -170,32 +244,109 @@ angular.module('import').factory('connectionParseService', }); } - // Translate a given javascript object to a full-fledged Connection - const connectionTransformer = connection => new Connection(connection); + /** + * Convert a provided ProtoConnection array into a ParseResult. Any provided + * transform functions will be run on each entry in `connectionData` before + * any other processing is done. + * + * @param {*[]} connectionData + * An arbitrary array of data. This must evaluate to a ProtoConnection + * object after being run through all functions in `transformFunctions`. + * + * @param {Function[]} transformFunctions + * An array of transformation functions to run on each entry in + * `connection` data. + * + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. + */ + function parseConnectionData(connectionData, transformFunctions) { + + // Check that the provided connection data array is not empty + const checkError = performBasicChecks(connectionData); + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } - // Translate a Connection object to a patch requesting the creation of said - // Connection - const patchTransformer = connection => new DirectoryPatch({ - op: 'add', - path: '/', - value: connection - }); + return $q.all({ + groupTransformer : getGroupTransformer(), + invalidUserIdErrorDetector : getInvalidIdentifierErrorChecker( + userService.getUsers), + invalidGroupIDErrorDetector : getInvalidIdentifierErrorChecker( + userGroupService.getUserGroups), + }) + + // Transform the rows from the CSV file to an array of API patches + // and lists of user and group identifiers + .then(({groupTransformer, + invalidUserIdErrorDetector, invalidGroupIDErrorDetector}) => + connectionData.reduce((parseResult, data) => { + + const { patches, identifiers, users, groups } = parseResult; + + // Run the array data through each provided transform + let connectionObject = data; + _.forEach(transformFunctions, transform => { + connectionObject = transform(connectionObject); + }); + + // All errors found while parsing this connection + const connectionErrors = []; + parseResult.errors.push(connectionErrors); + + // Translate the group on the object to a parentIdentifier + try { + connectionObject = groupTransformer(connectionObject); + } + + // If there was a problem with the group or parentIdentifier + catch (error) { + connectionErrors.push(error); + } + + // Push any errors for invalid user or user group identifiers + const pushError = error => error && connectionErrors.push(error); + pushError(invalidUserIdErrorDetector(connectionObject.users)); + pushError(invalidGroupIDErrorDetector(connectionObject.userGroups)); + + // Add the user and group identifiers for this connection + users.push(connectionObject.users); + groups.push(connectionObject.groups); + + // Translate to a full-fledged Connection + const connection = new Connection(connectionObject); + + // Finally, add a patch for creating the connection + patches.push(new DirectoryPatch({ + op: 'add', + path: '/', + value: connection + })); + + // If there are any errors for this connection fail the whole batch + if (connectionErrors.length) + parseResult.hasErrors = true; + + return parseResult; + + }, new ParseResult())); + } /** * Convert a provided CSV representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. - * - * TODO: Describe disambiguation suffixes, e.g. hostname (parameter), and - * that we will accept without the suffix if it's unambigous. (or not? how about not?) + * object to be submitted to the PATCH REST endpoint, as well as a list of + * objects containing lists of user and user group identifiers to be granted + * to each connection. * * @param {String} csvData - * The JSON-encoded connection list to convert to a PATCH request body. + * The CSV-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseCSV = function parseCSV(csvData) { @@ -211,134 +362,69 @@ angular.module('import').factory('connectionParseService', catch(error) { console.error(error); const deferred = $q.defer(); - deferred.reject(error); + deferred.reject(new ParseError({ message: error.message })); return deferred.promise; } + // The header row - an array of string header values + const header = parsedData.length ? parsedData[0] : []; + // Slice off the header row to get the data rows const connectionData = parsedData.slice(1); - // Check that the provided CSV is not empty (the parser always - // returns an array) - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } - - // The header row - an array of string header values - const header = parsedData[0]; + // Generate the CSV transform function, and apply it to every row + // before applying all the rest of the standard transforms + return connectionCSVService.getCSVTransformer(header).then( + csvTransformer => - return $q.all({ - csvTransformer : connectionCSVService.getCSVTransformer(header), - groupTransformer : getGroupTransformer() - }) - - // Transform the rows from the CSV file to an array of API patches - .then(({csvTransformer, groupTransformer}) => connectionData.map( - dataRow => { - - // Translate the raw CSV data to a javascript object - let connectionObject = csvTransformer(dataRow); - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); + // Apply the CSV transform to every row + parseConnectionData(connectionData, [csvTransformer])); }; /** * Convert a provided YAML representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. + * object to be submitted to the PATCH REST endpoint, as well as a list of + * objects containing lists of user and user group identifiers to be granted + * to each connection. * * @param {String} yamlData - * The YAML-encoded connection list to convert to a PATCH request body. + * The YAML-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided YAML. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array - const parsedData = parseYAMLData(yamlData); + const connectionData = parseYAMLData(yamlData); - // Check that the data is the correct format, and not empty - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } - - // Transform the data from the YAML file to an array of API patches - return getGroupTransformer().then( - groupTransformer => parsedData.map(connectionObject => { - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); - + // Produce a ParseResult + return parseConnectionData(connectionData); }; /** - * Convert a provided JSON representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. + * Convert a provided JSON-encoded representation of a connection list into + * an array of patches to be submitted to the PATCH REST endpoint, as well + * as a list of objects containing lists of user and user group identifiers + * to be granted to each connection. * * @param {String} jsonData - * The JSON-encoded connection list to convert to a PATCH request body. + * The JSON-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided JSON. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array - const parsedData = JSON.parse(yamlData); + const connectionData = JSON.parse(jsonData); - // Check that the data is the correct format, and not empty - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } + // Produce a ParseResult + return parseConnectionData(connectionData); - // Transform the data from the YAML file to an array of API patches - return getGroupTransformer().then( - groupTransformer => parsedData.map(connectionObject => { - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); - }; return service; diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js index 1e4ce478e..33fd07765 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -31,7 +31,7 @@ angular.module('import').factory('ParseError', [function defineParseError() { * The object whose properties should be copied within the new * ParseError. */ - var ParseError = function ParseError(template) { + const ParseError = function ParseError(template) { // Use empty object by default template = template || {}; diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js new file mode 100644 index 000000000..e6aa59229 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service which defines the ParseResult class. + */ +angular.module('import').factory('ParseResult', [function defineParseResult() { + + /** + * The result of parsing a connection import file - containing a list of + * API patches ready to be submitted to the PATCH REST API for batch + * connection creation, a set of users and user groups to grant access to + * each connection, and any errors that may have occured while parsing + * each connection. + * + * @constructor + * @param {ParseResult|Object} [template={}] + * The object whose properties should be copied within the new + * ParseResult. + */ + const ParseResult = function ParseResult(template) { + + // Use empty object by default + template = template || {}; + + /** + * An array of patches, ready to be submitted to the PATCH REST API for + * batch connection creation. + * + * @type {DirectoryPatch[]} + */ + this.patches = template.patches || []; + + /** + * A list of user identifiers that should be granted read access to the + * the corresponding connection (at the same array index). + * + * @type {String[]} + */ + this.users = template.users || []; + + /** + * A list of user group identifiers that should be granted read access + * to the corresponding connection (at the same array index). + * + * @type {String[]} + */ + this.groups = template.groups || []; + + /** + * An array of errors encountered while parsing the corresponding + * connection (at the same array index). Each connection should have a + * an array of errors. If empty, no errors occured for this connection. + * + * @type {ParseError[][]} + */ + this.errors = template.errors || []; + + /** + * True if any errors were encountered while parsing the connections + * represented by this ParseResult. This should always be true if there + * are a non-zero number of elements in the errors list for any + * connection, or false otherwise. + */ + this.hasErrors = template.hasErrors || false; + + }; + + return ParseResult; + +}]); diff --git a/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js b/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js new file mode 100644 index 000000000..c853934d0 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service which defines the Connection class. + */ +angular.module('import').factory('ProtoConnection', [ + function defineProtoConnection() { + + /** + * A representation of a connection to be imported, as parsed from an + * user-supplied import file. + * + * @constructor + * @param {Connection|Object} [template={}] + * The object whose properties should be copied within the new + * Connection. + */ + var ProtoConnection = function ProtoConnection(template) { + + // Use empty object by default + template = template || {}; + + /** + * The unique identifier of the connection group that contains this + * connection. + * + * @type String + */ + this.parentIdentifier = template.parentIdentifier; + + /** + * The path to the connection group that contains this connection, + * written as e.g. "ROOT/parent/child/group". + * + * @type String + */ + this.group = template.group; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + * + * @type String + */ + this.name = template.name; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + * + * @type String + */ + this.protocol = template.protocol; + + /** + * Connection configuration parameters, as dictated by the protocol in + * use, arranged as name/value pairs. This information may not be + * available until directly queried. If this information is + * unavailable, this property will be null or undefined. + * + * @type Object. + */ + this.parameters = template.parameters || {}; + + /** + * Arbitrary name/value pairs which further describe this connection. + * The semantics and validity of these attributes are dictated by the + * extension which defines them. + * + * @type Object. + */ + this.attributes = template.attributes || {}; + + /** + * The identifiers of all users who should be granted read access to + * this connection. + * + * @type String[] + */ + this.users = template.users || []; + + /** + * The identifiers of all user groups who should be granted read access + * to this connection. + * + * @type String[] + */ + this.groups = template.groups || []; + + }; + + return ProtoConnection; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 98b983b9f..978ffc0e2 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -200,6 +200,10 @@ "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_FILE_TYPE": "Invalid import file type \"{TYPE}\"", + "ERROR_INVALID_USER_IDENTIFIERS": + "Users not found: {IDENTIFIER_LIST}", + "ERROR_INVALID_USER_GROUP_IDENTIFIERS": + "User Groups not found: {IDENTIFIER_LIST}", "ERROR_NO_FILE_SUPPLIED": "Please select a file to import", "ERROR_AMBIGUOUS_PARENT_GROUP": "Both group and parentIdentifier may be not specified at the same time",