From fac76ef0cb1a3ff52bd5c03ba4aa632630175c02 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 1 Feb 2023 23:25:12 +0000 Subject: [PATCH] GUACAMOLE-926: Migrate import code to a dedicated module. --- .../importConnectionsController.js | 10 +- .../frontend/src/app/import/importModule.js | 24 ++++ .../import/services/connectionCSVService.js | 103 ++++++++++++++++++ .../services/connectionParseService.js} | 62 +++++------ .../templates/connectionImport.html} | 0 .../{settings => import}/types/ParseError.js | 2 +- .../src/app/index/config/indexRouteConfig.js | 4 +- .../app/rest/services/connectionService.js | 37 +++---- .../src/app/rest/types/DirectoryPatch.js | 95 ++++++++++++++++ .../app/rest/types/DirectoryPatchOutcome.js | 83 ++++++++++++++ 10 files changed, 361 insertions(+), 59 deletions(-) rename guacamole/src/main/frontend/src/app/{settings => import}/controllers/importConnectionsController.js (83%) create mode 100644 guacamole/src/main/frontend/src/app/import/importModule.js create mode 100644 guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js rename guacamole/src/main/frontend/src/app/{settings/services/connectionImportParseService.js => import/services/connectionParseService.js} (98%) rename guacamole/src/main/frontend/src/app/{settings/templates/settingsImport.html => import/templates/connectionImport.html} (100%) rename guacamole/src/main/frontend/src/app/{settings => import}/types/ParseError.js (95%) create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js similarity index 83% rename from guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js rename to guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 33b8914ad..178d6c69d 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -20,11 +20,11 @@ /** * The controller for the connection import page. */ -angular.module('settings').controller('importConnectionsController', ['$scope', '$injector', +angular.module('import').controller('importConnectionsController', ['$scope', '$injector', function importConnectionsController($scope, $injector) { // Required services - const connectionImportParseService = $injector.get('connectionImportParseService'); + const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); function processData(type, data) { @@ -36,18 +36,18 @@ angular.module('settings').controller('importConnectionsController', ['$scope', case "application/json": case "text/json": - requestBody = connectionImportParseService.parseJSON(data); + requestBody = connectionParseService.parseJSON(data); break; case "text/csv": - requestBody = connectionImportParseService.parseCSV(data); + requestBody = connectionParseService.parseCSV(data); break; case "application/yaml": case "application/x-yaml": case "text/yaml": case "text/x-yaml": - requestBody = connectionImportParseService.parseYAML(data); + requestBody = connectionParseService.parseYAML(data); break; } diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js new file mode 100644 index 000000000..6480d62fc --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * The module for code supporting importing user-supplied files. Currently, only + * connection import is supported. + */ +angular.module('import', ['rest']); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js new file mode 100644 index 000000000..361a15283 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -0,0 +1,103 @@ +/* + * 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. + */ + +/* global _ */ + +/** + * A service for parsing user-provided CSV connection data for bulk import. + */ +angular.module('import').factory('connectionCSVService', + ['$injector', function connectionCSVService($injector) { + + // Required services + const $q = $injector.get('$q'); + const $routeParams = $injector.get('$routeParams'); + const schemaService = $injector.get('schemaService'); + + const service = {}; + + /** + * Returns a promise that resolves to an object detailing the connection + * attributes for the current data source, as well as the connection + * paremeters for every protocol, for the current data source. + * + * The object that the promise will contain an "attributes" key that maps to + * a set of attribute names, and a "protocolParameters" key that maps to an + * object mapping protocol names to sets of parameter names for that protocol. + * + * The intended use case for this object is to determine if there is a + * connection parameter or attribute with a given name, by e.g. checking the + * path `.protocolParameters[protocolName]` to see if a protocol exists, + * checking the path `.protocolParameters[protocolName][fieldName]` to see + * if a parameter exists for a given protocol, or checking the path + * `.attributes[fieldName]` to check if a connection attribute exists. + * + * @returns {Promise.} + */ + function getFieldLookups() { + + // The current data source - the one that the connections will be + // imported into + const dataSource = $routeParams.dataSource; + + // Fetch connection attributes and protocols for the current data source + return $q.all({ + attributes : schemaService.getConnectionAttributes(dataSource), + protocols : schemaService.getProtocols(dataSource) + }) + .then(function connectionStructureRetrieved({attributes, protocols}) { + + return { + + // Translate the forms and fields into a flat map of attribute + // name to `true` boolean value + attributes: attributes.reduce( + (attributeMap, form) => { + form.fields.forEach( + field => attributeMap[field.name] = true); + return attributeMap + }, {}), + + // Translate the protocol definitions into a map of protocol + // name to map of field name to `true` boolean value + protocolParameters: _.mapValues( + protocols, protocol => protocol.connectionForms.reduce( + (protocolFieldMap, form) => { + form.fields.forEach( + field => protocolFieldMap[field.name] = true); + return protocolFieldMap; + }, {})) + }; + }); + } + + /** + * + * + * @returns {Promise.>} + * A promise that will resolve to a function that translates a CSV data + * row (array of strings) to a connection object. + */ + service.getCSVTransformer = function getCSVTransformer(headerRow) { + + }; + + return service; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js similarity index 98% rename from guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js rename to guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index f2735acd6..1051b800b 100644 --- a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -26,8 +26,8 @@ import { parse as parseYAMLData } from 'yaml' * A service for parsing user-provided JSON, YAML, or JSON connection data into * an appropriate format for bulk uploading using the PATCH REST endpoint. */ -angular.module('settings').factory('connectionImportParseService', - ['$injector', function connectionImportParseService($injector) { +angular.module('import').factory('connectionParseService', + ['$injector', function connectionParseService($injector) { // Required types const Connection = $injector.get('Connection'); @@ -71,35 +71,6 @@ angular.module('settings').factory('connectionImportParseService', }) }); } - - /** - * 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. - * - * @param {String} yamlData - * The YAML-encoded connection list to convert to a PATCH request body. - * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. - */ - service.parseYAML = function parseYAML(yamlData) { - - // Parse from YAML into a javascript array - const parsedData = parseYAMLData(yamlData); - - // Check that the data is the correct format, and not empty - performBasicChecks(parsedData); - - // Convert to an array of Connection objects and return - const deferredConnections = $q.defer(); - deferredConnections.resolve( - parsedData.map(connection => new Connection(connection))); - return deferredConnections.promise; - - }; /** * Returns a promise that resolves to an object detailing the connection @@ -321,6 +292,35 @@ angular.module('settings').factory('connectionImportParseService', }; + /** + * 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. + * + * @param {String} yamlData + * The YAML-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseYAML = function parseYAML(yamlData) { + + // Parse from YAML into a javascript array + const parsedData = parseYAMLData(yamlData); + + // Check that the data is the correct format, and not empty + performBasicChecks(parsedData); + + // Convert to an array of Connection objects and return + const deferredConnections = $q.defer(); + deferredConnections.resolve( + parsedData.map(connection => new Connection(connection))); + return deferredConnections.promise; + + }; + /** * Convert a provided JSON representation of a connection list into a JSON * string to be submitted to the PATCH REST endpoint. The returned JSON diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html rename to guacamole/src/main/frontend/src/app/import/templates/connectionImport.html diff --git a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js similarity index 95% rename from guacamole/src/main/frontend/src/app/settings/types/ParseError.js rename to guacamole/src/main/frontend/src/app/import/types/ParseError.js index b3d3da03a..21cd545d7 100644 --- a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -20,7 +20,7 @@ /** * Service which defines the ParseError class. */ -angular.module('settings').factory('ParseError', [function defineParseError() { +angular.module('import').factory('ParseError', [function defineParseError() { /** * An error representing a parsing failure when attempting to convert diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index 51c423932..dd9cc0637 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -127,10 +127,10 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Connection import page - .when('/settings/:dataSource/import', { + .when('/import/:dataSource/connection', { title : 'APP.NAME', bodyClassName : 'settings', - templateUrl : 'app/settings/templates/settingsImport.html', + templateUrl : 'app/import/templates/connectionImport.html', controller : 'importConnectionsController', resolve : { updateCurrentToken: updateCurrentToken } }) diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index a91c8d05d..053559530 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -156,41 +156,38 @@ angular.module('rest').factory('connectionService', ['$injector', }; /** - * Makes a request to the REST API to create multiple connections, returning a - * a promise that can be used for processing the results of the call. This - * operation is atomic - if any errors are encountered during the connection - * creation process, the entire request will fail, and no connections will be - * created. + * Makes a request to the REST API to apply a supplied list of connection + * patches, returning a promise that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. * - * @param {Connection[]} connections The connections to create. + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the - * create operation is successful. + * patch operation is successful. */ - service.createConnections = function createConnections(dataSource, connections) { + service.patchConnections = function patchConnections(dataSource, patches) { - // An object containing a PATCH operation to create each connection - const patchBody = connections.map(connection => ({ - op: "add", - path: "/", - value: connection - })); - - // Make a PATCH request to create the connections + // Make the PATCH request return authenticationService.request({ method : 'PATCH', url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', - data : patchBody + data : patches }) // Clear the cache - .then(function connectionUpdated(){ + .then(function connectionsPatched(){ cacheService.connections.removeAll(); - // Clear users cache to force reload of permissions for this - // newly updated connection + // Clear users cache to force reload of permissions for any + // newly created or replaced connections cacheService.users.removeAll(); + }); } diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js new file mode 100644 index 000000000..96a2cb093 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js @@ -0,0 +1,95 @@ +/* + * 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 DirectoryPatch class. + */ +angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch() { + + /** + * The object consumed by REST API calls when representing changes to an + * arbitrary set of directory-based objects. + * @constructor + * + * @template DirectoryObject + * The directory-based object type that this DirectoryPatch will + * operate on. + * + * @param {DirectoryObject|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatch. + */ + var DirectoryPatch = function DirectoryPatch(template) { + + // Use empty object by default + template = template || {}; + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + * + * @type {String} + */ + this.op = template.op; + + /** + * The path of the objects to modify. For creation of new objects, this + * should be "/". Otherwise, it should be "/{identifier}", specifying + * the identifier of the existing object being modified. + * + * @type {String} + * @default '/' + */ + this.path = template.path || '/'; + + /** + * The object being added or replaced, or the identifier of the object + * being removed. + * + * @type {DirectoryObject|String} + */ + this.value = template.value; + + }; + + /** + * All valid patch operations for directory-based objects. + */ + DirectoryPatch.Operation = { + + /** + * Adds the specified object to the relation. + */ + ADD : "add", + + /** + * Removes the specified object from the relation. + */ + REPLACE : "replace", + + /** + * Removes the specified object from the relation. + */ + REMOVE : "remove" + + }; + + return DirectoryPatch; + +}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js new file mode 100644 index 000000000..4534dae13 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js @@ -0,0 +1,83 @@ +/* + * 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 DirectoryPatchOutcome class. + */ +angular.module('rest').factory('DirectoryPatchOutcome', [ + function defineDirectoryPatchOutcome() { + + /** + * An object returned by a PATCH request to a directory REST API, + * representing the outcome associated with a particular patch in the + * request. This object can indicate either a successful or unsuccessful + * response. The error field is only meaningful for unsuccessful patches. + * @constructor + * + * @template DirectoryObject + * The directory-based object type that this DirectoryPatchOutcome + * represents a patch outcome for. + * + * @param {DirectoryObject|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatchOutcome. + */ + var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { + + // Use empty object by default + template = template || {}; + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + * + * @type {String} + */ + this.op = template.op; + + /** + * The path of the object operated on by the corresponding patch in the + * request. + * + * @type {String} + */ + this.path = template.path; + + /** + * The identifier of the object operated on by the corresponding patch + * in the request. If the object was newly created and the PATCH request + * did not fail, this will be the identifier of the newly created object. + * + * @type {String} + */ + this.identifier = template.identifier; + + /** + * The error message associated with the failure, if the patch failed to + * apply. + * + * @type {String} + */ + this.error = template.error; + + }; + + return DirectoryPatch; + +}]);