GUACAMOLE-926: Clean up unneeded code, trailing whitespace; fix bugs, styling, comments and licenses.

This commit is contained in:
James Muehlner
2023-03-15 18:19:44 +00:00
parent f8fb95fbc8
commit d657d2b90a
35 changed files with 373 additions and 436 deletions

View File

@@ -2,6 +2,6 @@ core-util-is (https://github.com/isaacs/core-util-is)
--------------------------------------------- ---------------------------------------------
Version: 1.0.3 Version: 1.0.3
From: 'isaacs' (https://github.com/isaacs) From: 'Node.js contributors'
License(s): License(s):
MIT (bundled/core-util-is-1.0.3/LICENSE) MIT (bundled/core-util-is-1.0.3/LICENSE)

View File

@@ -2,6 +2,6 @@ node-csv (https://github.com/adaltas/node-csv)
--------------------------------------------- ---------------------------------------------
Version: 6.2.5 Version: 6.2.5
From: 'adaltas' (https://github.com/adaltas) From: 'Adaltas' (https://github.com/adaltas)
License(s): License(s):
MIT (bundled/csv-6.2.5/LICENSE) MIT (bundled/csv-6.2.5/LICENSE)

View File

@@ -2,6 +2,6 @@ events (https://github.com/browserify/events)
--------------------------------------------- ---------------------------------------------
Version: 3.3.0 Version: 3.3.0
From: 'browserify' (https://github.com/browserify) From: 'Node.js contributors, Joyent, Inc., and other Node contributors'
License(s): License(s):
MIT (bundled/events-3.3.0/LICENSE) MIT (bundled/events-3.3.0/LICENSE)

View File

@@ -2,6 +2,6 @@ ieee754 (https://github.com/feross/ieee754)
--------------------------------------------- ---------------------------------------------
Version: 1.2.1 Version: 1.2.1
From: 'Feross Aboukhadijeh' (https://github.com/feross) From: 'Fair Oaks Labs, Inc'
License(s): License(s):
MIT (bundled/ieee754-1.2.1/LICENSE) MIT (bundled/ieee754-1.2.1/LICENSE)

View File

@@ -4,5 +4,5 @@ process-nextick-args (https://github.com/calvinmetcalf/process-nextick-args)
Version: 2.0.1 Version: 2.0.1
From: 'Calvin Metcalf' (https://github.com/calvinmetcalf) From: 'Calvin Metcalf' (https://github.com/calvinmetcalf)
License(s): License(s):
MIT (bundled/process-nextick-args-2.0.1/LICENSE) MIT (bundled/process-nextick-args-2.0.1/license.md)

View File

@@ -2,7 +2,7 @@ readable-stream (https://github.com/nodejs/readable-stream)
--------------------------------------------- ---------------------------------------------
Version: 2.3.7 Version: 2.3.7
From: 'Node.js' (https://github.com/nodejs) From: 'Node.js contributors, Joyent, Inc., and other Node contributors'
License(s): License(s):
MIT (bundled/readable-stream-2.3.7/LICENSE) MIT (bundled/readable-stream-2.3.7/LICENSE)

View File

@@ -4,5 +4,5 @@ setImmediate.js (https://github.com/YuzuJS/setImmediate)
Version: 1.0.5 Version: 1.0.5
From: 'Yuzu (by Barnes & Noble Education)' (https://github.com/YuzuJS) From: 'Yuzu (by Barnes & Noble Education)' (https://github.com/YuzuJS)
License(s): License(s):
MIT (bundled/setimmediate-1.0.5/LICENSE) MIT (bundled/setimmediate-1.0.5/LICENSE.txt)

View File

@@ -2,7 +2,7 @@ stream-browserify (https://github.com/browserify/stream-browserify)
--------------------------------------------- ---------------------------------------------
Version: 2.0.2 Version: 2.0.2
From: 'browserify' (https://github.com/browserify) From: 'James Halliday'
License(s): License(s):
MIT (bundled/stream-browserify-2.0.2/LICENSE) MIT (bundled/stream-browserify-2.0.2/LICENSE)

View File

@@ -2,7 +2,7 @@ string_decoder (https://github.com/nodejs/string_decoder)
--------------------------------------------- ---------------------------------------------
Version: 1.1.1 Version: 1.1.1
From: 'Node.js' (https://github.com/nodejs) From: 'Node.js contributors, Joyent, Inc., and other Node contributors'
License(s): License(s):
MIT (bundled/string_decoder-1.1.1/LICENSE) MIT (bundled/string_decoder-1.1.1/LICENSE)

View File

@@ -2,7 +2,7 @@ timers-browserify (https://github.com/browserify/timers-browserify)
--------------------------------------------- ---------------------------------------------
Version: 2.0.12 Version: 2.0.12
From: 'browserify' (https://github.com/browserify) From: 'J. Ryan Stinnett' (https://github.com/jryans)
License(s): License(s):
MIT (bundled/timers-browserify-2.0.12/LICENSE) MIT (bundled/timers-browserify-2.0.12/LICENSE.md)

View File

@@ -32,6 +32,7 @@ import org.mybatis.guice.transactional.Transactional;
public abstract class JDBCDirectory<ObjectType extends Identifiable> public abstract class JDBCDirectory<ObjectType extends Identifiable>
extends RestrictedObject implements Directory<ObjectType> { extends RestrictedObject implements Directory<ObjectType> {
@Override
@Transactional @Transactional
public void tryAtomically(AtomicDirectoryOperation<ObjectType> operation) public void tryAtomically(AtomicDirectoryOperation<ObjectType> operation)
throws GuacamoleException { throws GuacamoleException {

View File

@@ -23,7 +23,7 @@ import org.apache.guacamole.GuacamoleException;
/** /**
* An operation that should be attempted atomically when passed to * An operation that should be attempted atomically when passed to
* {@link Directory#tryAtomically()}, if atomic operations are supported by * {@link Directory#tryAtomically}, if atomic operations are supported by
* the Directory. * the Directory.
*/ */
public interface AtomicDirectoryOperation<ObjectType extends Identifiable> { public interface AtomicDirectoryOperation<ObjectType extends Identifiable> {
@@ -36,12 +36,12 @@ public interface AtomicDirectoryOperation<ObjectType extends Identifiable> {
* provided directory outside this function, or of the directory invoking * provided directory outside this function, or of the directory invoking
* this function are not guaranteed. * this function are not guaranteed.
* *
* NOTE: If atomicity is required for this operation, a GuacamoleException * <p>NOTE: If atomicity is required for this operation, a
* may be thrown by this function before any changes are made, ensuring the * GuacamoleException may be thrown by this function before any changes are
* operation will only ever be performed atomically. * made, ensuring the operation will only ever be performed atomically.
* *
* @param atomic * @param atomic
* True if the provided directory is guaranteed to peform the operation * True if the provided directory is guaranteed to perform the operation
* atomically within the context of this function. * atomically within the context of this function.
* *
* @param directory * @param directory

View File

@@ -19,15 +19,49 @@
/* global _ */ /* global _ */
/**
* The allowed MIME type for CSV files.
*
* @type String
*/
const CSV_MIME_TYPE = 'text/csv';
/**
* The allowed MIME type for JSON files.
*
* @type String
*/
const JSON_MIME_TYPE = 'application/json';
/**
* The allowed MIME types for YAML files.
* NOTE: There is no registered MIME type for YAML files. This may result in a
* wide variety of possible browser-supplied MIME types.
*
* @type String[]
*/
const YAML_MIME_TYPES = [
'text/x-yaml',
'text/yaml',
'text/yml',
'application/x-yaml',
'application/x-yml',
'application/yaml',
'application/yml'
];
/*
* All file types supported for connection import.
*
* @type {String[]}
*/
const LEGAL_MIME_TYPES = [CSV_MIME_TYPE, JSON_MIME_TYPE, ...YAML_MIME_TYPES];
/** /**
* The controller for the connection import page. * The controller for the connection import page.
*/ */
angular.module('import').controller('importConnectionsController', ['$scope', '$injector', angular.module('import').controller('importConnectionsController', ['$scope', '$injector',
function importConnectionsController($scope, $injector) { function importConnectionsController($scope, $injector) {
// The file types supported for connection import
const LEGAL_FILE_TYPES = ['csv', 'json', 'yaml'];
// Required services // Required services
const $document = $injector.get('$document'); const $document = $injector.get('$document');
const $location = $injector.get('$location'); const $location = $injector.get('$location');
@@ -359,7 +393,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
className : 'success', className : 'success',
title : 'IMPORT.DIALOG_HEADER_SUCCESS', title : 'IMPORT.DIALOG_HEADER_SUCCESS',
text : { text : {
key: 'IMPORT.CONNECTIONS_IMPORTED_SUCCESS', key: 'IMPORT.INFO_CONNECTIONS_IMPORTED_SUCCESS',
variables: { NUMBER: parseResult.patches.length } variables: { NUMBER: parseResult.patches.length }
}, },
@@ -388,7 +422,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
handleError(error); handleError(error);
})) }))
// If an error occured when the call to create the connections was made, // If an error occurred when the call to create the connections was made,
// skip any further processing - the user will have a chance to fix the // skip any further processing - the user will have a chance to fix the
// problems and try again // problems and try again
.catch(patchFailure => { .catch(patchFailure => {
@@ -438,7 +472,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
name : 'IMPORT.ACTION_ACKNOWLEDGE', name : 'IMPORT.ACTION_ACKNOWLEDGE',
callback : () => guacNotification.showStatus(false) callback : () => guacNotification.showStatus(false)
}] }]
}) });
}; };
@@ -463,13 +497,13 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
let processDataCallback; let processDataCallback;
// Choose the appropriate parse function based on the mimetype // Choose the appropriate parse function based on the mimetype
if (mimeType.endsWith("json")) if (mimeType === JSON_MIME_TYPE)
processDataCallback = connectionParseService.parseJSON; processDataCallback = connectionParseService.parseJSON;
else if (mimeType.endsWith("csv")) else if (mimeType === CSV_MIME_TYPE)
processDataCallback = connectionParseService.parseCSV; processDataCallback = connectionParseService.parseCSV;
else if (mimeType.endsWith("yaml")) else if (YAML_MIME_TYPES.indexOf(mimeType) >= 0)
processDataCallback = connectionParseService.parseYAML; processDataCallback = connectionParseService.parseYAML;
// The file type was validated before being uploaded - this should // The file type was validated before being uploaded - this should
@@ -498,9 +532,11 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
$scope.import = () => processData($scope.mimeType, $scope.fileData); $scope.import = () => processData($scope.mimeType, $scope.fileData);
/** /**
* @return {Boolean} * Returns true if import should be disabled, or false if import should be
* True if import should be disabled, or false if import should be
* allowed. * allowed.
*
* @return {Boolean}
* True if import should be disabled, otherwise false.
*/ */
$scope.importDisabled = () => $scope.importDisabled = () =>
@@ -560,19 +596,19 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
// The MIME type of the provided file // The MIME type of the provided file
const mimeType = file.type; const mimeType = file.type;
// Check if the mimetype ends with one of the supported types, // Check if the mimetype is one of the supported types,
// e.g. "application/json" or "text/csv" // e.g. "application/json" or "text/csv"
if (_.every(LEGAL_FILE_TYPES.map( if (LEGAL_MIME_TYPES.indexOf(mimeType) < 0) {
type => !mimeType.endsWith(type)))) {
// If the provided file is not one of the supported types, // If the provided file is not one of the supported types,
// display an error and abort processing // display an error and abort processing
handleError(new ParseError({ handleError(new ParseError({
message: "Invalid file type: " + mimeType, message: "Invalid file type: " + mimeType,
key: 'IMPORT.ERROR_INVALID_FILE_TYPE', key: 'IMPORT.ERROR_INVALID_MIME_TYPE',
variables: { TYPE: mimeType } variables: { TYPE: mimeType }
})); }));
return; return;
} }
$scope.fileName = file.name; $scope.fileName = file.name;

View File

@@ -20,7 +20,7 @@
/* global _ */ /* global _ */
/** /**
* A directive that displays errors that occured during parsing of a connection * A directive that displays errors that occurred during parsing of a connection
* import file, or errors that were returned from the API during the connection * import file, or errors that were returned from the API during the connection
* batch creation attempt. * batch creation attempt.
*/ */
@@ -140,18 +140,6 @@ angular.module('import').directive('connectionImportErrors', [
name: connection.name, name: connection.name,
protocol: connection.protocol, protocol: connection.protocol,
// The group and parent identifiers, if any are set. Include
// both since these could be a potential source of conflict.
// TODO: Should we _really_ have both of these here?
group: connection.group,
parentIdentifier: connection.parentIdentifier,
// Get the list of user and group identifiers from the parse
// result. There should one entry in each of these lists for
// each patch.
users: parseResult.users[index],
groups: parseResult.groups[index],
// The human-readable error messages // The human-readable error messages
errors: new DisplayErrorList( errors: new DisplayErrorList(
[ ...(parseResult.errors[index] || []) ]) [ ...(parseResult.errors[index] || []) ])
@@ -159,7 +147,7 @@ angular.module('import').directive('connectionImportErrors', [
}; };
// If a new connection patch failure is seen, update the display list // If a new connection patch failure is seen, update the display list
$scope.$watch('patchFailure', async function patchFailureChanged(patchFailure) { $scope.$watch('patchFailure', function patchFailureChanged(patchFailure) {
const { parseResult } = $scope; const { parseResult } = $scope;
@@ -176,8 +164,6 @@ angular.module('import').directive('connectionImportErrors', [
const connectionError = generateConnectionError(parseResult, index); const connectionError = generateConnectionError(parseResult, index);
// Set the error from the PATCH request, if there is one // Set the error from the PATCH request, if there is one
// TODO: These generally aren't translated from the backend -
// should we even bother trying to translate them?
const error = _.get(patchFailure, ['patches', index, 'error']); const error = _.get(patchFailure, ['patches', index, 'error']);
if (error) if (error)
connectionError.errors = new DisplayErrorList([error]); connectionError.errors = new DisplayErrorList([error]);
@@ -187,7 +173,7 @@ angular.module('import').directive('connectionImportErrors', [
}); });
// If a new parse result with errors is seen, update the display list // If a new parse result with errors is seen, update the display list
$scope.$watch('parseResult', async function parseResultChanged(parseResult) { $scope.$watch('parseResult', function parseResultChanged(parseResult) {
// Do not process if there are no errors in the provided result // Do not process if there are no errors in the provided result
if (!parseResult || !parseResult.hasErrors) if (!parseResult || !parseResult.hasErrors)

View File

@@ -60,6 +60,9 @@ angular.module('import').factory('connectionCSVService',
* `.attributes[fieldName]` to check if a connection attribute exists. * `.attributes[fieldName]` to check if a connection attribute exists.
* *
* @returns {Promise.<Object>} * @returns {Promise.<Object>}
* A promise that resolves to a object detailing the connection
* attributes and parameters for every protocol, for the current data
* source.
*/ */
function getFieldLookups() { function getFieldLookups() {
@@ -104,7 +107,7 @@ angular.module('import').factory('connectionCSVService',
* escaped with backslashes, and backslashes can also be escaped using other * escaped with backslashes, and backslashes can also be escaped using other
* backslashes. * backslashes.
* *
* @param {type} rawIdentifiers * @param {String} rawIdentifiers
* The raw string value as fetched from the CSV. * The raw string value as fetched from the CSV.
* *
* @returns {Array.<String>} * @returns {Array.<String>}
@@ -174,16 +177,10 @@ angular.module('import').factory('connectionCSVService',
* "name", "protocol", "group", or "parentIdentifier" fields, the suffix is * "name", "protocol", "group", or "parentIdentifier" fields, the suffix is
* required. * required.
* *
* This returned object will be very similar to the Connection type, with
* 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, * If a failure occurs while attempting to create the transformer function,
* the promise will be rejected with a ParseError describing the failure. * the promise will be rejected with a ParseError describing the failure.
* *
* @returns {Promise.<Function.<String[], Object>>} * @returns {Promise.<Function.<String[], ImportConnection>>}
* A promise that will resolve to a function that translates a CSV data * A promise that will resolve to a function that translates a CSV data
* row (array of strings) to a ImportConnection object. * row (array of strings) to a ImportConnection object.
*/ */
@@ -251,8 +248,8 @@ angular.module('import').factory('connectionCSVService',
// A callback that returns the field at the current index // A callback that returns the field at the current index
const fetchFieldAtIndex = row => row[index]; const fetchFieldAtIndex = row => row[index];
// A callback that splits identifier lists by semicolon // A callback that splits raw string identifier lists by
// characters into a javascript list of identifiers // semicolon characters into an array of identifiers
const identifierListCallback = row => const identifierListCallback = row =>
splitIdentifiers(fetchFieldAtIndex(row)); splitIdentifiers(fetchFieldAtIndex(row));
@@ -270,8 +267,7 @@ angular.module('import').factory('connectionCSVService',
// Set up the group parent ID callback // Set up the group parent ID callback
else if (header == 'parentIdentifier') else if (header == 'parentIdentifier')
transformConfig.parentIdentifierGetter = ( transformConfig.parentIdentifierGetter = fetchFieldAtIndex;
identifierListCallback);
// Set the user identifiers callback // Set the user identifiers callback
else if (header == 'users') else if (header == 'users')
@@ -293,7 +289,7 @@ angular.module('import').factory('connectionCSVService',
const parameterName = header.replace(PARAMETER_SUFFIX); const parameterName = header.replace(PARAMETER_SUFFIX);
transformConfig.parameterOrAttributeGetters.push( transformConfig.parameterOrAttributeGetters.push(
row => ({ row => ({
type: 'parameter', type: 'parameters',
name: parameterName, name: parameterName,
value: fetchFieldAtIndex(row) value: fetchFieldAtIndex(row)
}) })
@@ -307,7 +303,7 @@ angular.module('import').factory('connectionCSVService',
const attributeName = header.replace(ATTRIBUTE_SUFFIX); const attributeName = header.replace(ATTRIBUTE_SUFFIX);
transformConfig.parameterOrAttributeGetters.push( transformConfig.parameterOrAttributeGetters.push(
row => ({ row => ({
type: 'attribute', type: 'attributes',
name: parameterName, name: parameterName,
value: fetchFieldAtIndex(row) value: fetchFieldAtIndex(row)
}) })
@@ -382,13 +378,6 @@ angular.module('import').factory('connectionCSVService',
key: 'IMPORT.ERROR_REQUIRED_PROTOCOL' key: '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: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP'
});
// The function to transform a CSV row into a connection object // The function to transform a CSV row into a connection object
deferred.resolve(function transformCSVRow(row) { deferred.resolve(function transformCSVRow(row) {
@@ -408,14 +397,12 @@ angular.module('import').factory('connectionCSVService',
return new ImportConnection({ return new ImportConnection({
// Fields that are not protocol-specific // Fields that are not protocol-specific
...{
name, name,
protocol, protocol,
parentIdentifier, parentIdentifier,
group, group,
users, users,
groups groups,
},
// Fields that might potentially be either attributes or // Fields that might potentially be either attributes or
// parameters, depending on the protocol // parameters, depending on the protocol

View File

@@ -50,7 +50,7 @@ angular.module('import').factory('connectionParseService',
* data is an array, and contains at least one connection entry. Returns an * data is an array, and contains at least one connection entry. Returns an
* error if any of these basic checks fails. * error if any of these basic checks fails.
* *
* returns {ParseError} * @returns {ParseError}
* An error describing the parsing failure, if one of the basic checks * An error describing the parsing failure, if one of the basic checks
* fails. * fails.
*/ */
@@ -84,6 +84,8 @@ angular.module('import').factory('connectionParseService',
* identifier of the appropriate group, if defined. * identifier of the appropriate group, if defined.
* *
* @returns {Promise.<Object>} * @returns {Promise.<Object>}
* A promise that resolves to an object mapping groups to group
* identifiers.
*/ */
function getGroupLookups() { function getGroupLookups() {
@@ -101,7 +103,7 @@ angular.module('import').factory('connectionParseService',
// Add the specified group to the lookup, appending all specified // Add the specified group to the lookup, appending all specified
// prefixes, and then recursively call saveLookups for all children // prefixes, and then recursively call saveLookups for all children
// of the group, appending to the prefix for each level // of the group, appending to the prefix for each level
function saveLookups(prefix, group) { const saveLookups = (prefix, group) => {
// To get the path for the current group, add the name // To get the path for the current group, add the name
const currentPath = prefix + group.name; const currentPath = prefix + group.name;
@@ -113,6 +115,7 @@ angular.module('import').factory('connectionParseService',
const nextPrefix = currentPath + "/"; const nextPrefix = currentPath + "/";
_.forEach(group.childConnectionGroups, _.forEach(group.childConnectionGroups,
childGroup => saveLookups(nextPrefix, childGroup)); childGroup => saveLookups(nextPrefix, childGroup));
} }
// Start at the root group // Start at the root group
@@ -198,6 +201,7 @@ angular.module('import').factory('connectionParseService',
return deferred.promise; return deferred.promise;
} }
// Get the group transformer to apply to each connection
return getGroupTransformer().then(groupTransformer => return getGroupTransformer().then(groupTransformer =>
connectionData.reduce((parseResult, data, index) => { connectionData.reduce((parseResult, data, index) => {
@@ -261,7 +265,7 @@ angular.module('import').factory('connectionParseService',
value: connection value: connection
})); }));
// If there are any errors for this connection fail the whole batch // If there are any errors for this connection, fail the whole batch
if (connectionErrors.length) if (connectionErrors.length)
parseResult.hasErrors = true; parseResult.hasErrors = true;

View File

@@ -27,15 +27,21 @@
} }
.import .errors table { .import .errors table {
width: 100%; width: 100%;
} }
.import .errors .error-message { .import .errors .error-message {
color: red; color: red;
} }
.import .errors .error-message ul { .import .errors .error-message ul {
margin: 0px; margin: 0px;
} }
.file-upload-container { .file-upload-container {
@@ -56,13 +62,17 @@
} }
.file-upload-container.file-selected { .file-upload-container.file-selected {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 100px; gap: 100px;
} }
.file-upload-container .clear { .file-upload-container .clear {
margin: 0; margin: 0;
} }
.file-upload-container .upload-header { .file-upload-container .upload-header {

View File

@@ -5,7 +5,7 @@
placeholder="'IMPORT.FIELD_PLACEHOLDER_FILTER' | translate" placeholder="'IMPORT.FIELD_PLACEHOLDER_FILTER' | translate"
properties="filteredErrorProperties"></guac-filter> properties="filteredErrorProperties"></guac-filter>
<!-- List of current u --> <!-- List of connection import errors -->
<table class="sorted"> <table class="sorted">
<thead> <thead>
<tr> <tr>

View File

@@ -1,34 +1,34 @@
<div class="settings-view import"> <div class="settings-view view import">
<div class="header"> <div class="header">
<h2>{{'IMPORT.HEADER' | translate}}</h2> <h2>{{'IMPORT.SECTION_HEADER_CONNECTION_IMPORT' | translate}}</h2>
<guac-user-menu></guac-user-menu> <guac-user-menu></guac-user-menu>
</div> </div>
<div ng-show="fileName" class="file-upload-container file-selected"> <div ng-show="fileName" class="file-upload-container file-selected">
<div class="file-name"> {{fileName}} </div> <div class="file-name"> {{fileName}} </div>
<button class="danger clear" ng-click="cancel()"> <button class="danger clear" ng-click="cancel()">
{{'IMPORT.BUTTON_CLEAR' | translate}} {{'IMPORT.ACTION_CLEAR' | translate}}
</button> </button>
</div> </div>
<div ng-show="!fileName" class="file-upload-container"> <div ng-show="!fileName" class="file-upload-container">
<div class="upload-header"> <div class="upload-header">
<span class="file-options">{{'IMPORT.UPLOAD_FILE_TYPES' | translate}}</span> <span class="file-options">{{'IMPORT.HELP_UPLOAD_FILE_TYPES' | translate}}</span>
<a <a
href="#/import/connection/file-format-help" target="_blank" href="#/import/connection/file-format-help" target="_blank"
class="file-help-link">{{'IMPORT.UPLOAD_HELP_LINK' | translate}} class="file-help-link">{{'IMPORT.ACTION_VIEW_FORMAT_HELP' | translate}}
</a> </a>
</div> </div>
<div class="drop-target" ng-class="{ 'drop-pending': dropPending, 'file-present': fileName}"> <div class="drop-target" ng-class="{ 'drop-pending': dropPending, 'file-present': fileName}">
<div class="title">{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}</div> <div class="title">{{'IMPORT.HELP_UPLOAD_DROP_TITLE' | translate}}</div>
<input type="file" class="file-upload-input"/> <input type="file" class="file-upload-input"/>
<a ng-click="openFileBrowser()" class="browse-link"> <a ng-click="openFileBrowser()" class="browse-link">
{{'IMPORT.UPLOAD_BROWSE_LINK' | translate}} {{'IMPORT.ACTION_BROWSE' | translate}}
</a> </a>
<div class="file-name"> {{fileName}} </div> <div class="file-name"> {{fileName}} </div>
@@ -39,12 +39,12 @@
<div class="import-buttons"> <div class="import-buttons">
<button <button
ng-click="import()" ng-disabled="importDisabled()" class="import"> ng-click="import()" ng-disabled="importDisabled()" class="save import">
{{'IMPORT.BUTTON_IMPORT' | translate}} {{'IMPORT.ACTION_IMPORT' | translate}}
</button> </button>
<button <button
ng-click="cancel()" ng-disabled="cancelDisabled()" class="cancel"> ng-click="cancel()" ng-disabled="cancelDisabled()" class="cancel">
{{'IMPORT.BUTTON_CANCEL' | translate}} {{'IMPORT.ACTION_CANCEL' | translate}}
</button> </button>
</div> </div>

View File

@@ -1,90 +1,26 @@
<div class="import help"> <div class="import view help">
<div class="header"> <div class="header">
<h2>{{'IMPORT.HELP_HEADER' | translate}}</h2> <h2>{{'IMPORT.SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE' | translate}}</h2>
<guac-user-menu></guac-user-menu> <guac-user-menu></guac-user-menu>
</div> </div>
<h2>{{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}</h2> <h2>{{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}</h2>
<p>{{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}</p> <p>{{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}</p>
<h2>{{'IMPORT.HELP_CSV_HEADER' | translate}}</h2> <h2>{{'IMPORT.SECTION_HEADER_CSV' | translate}}</h2>
<p>{{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}</p> <p>{{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}</p>
<p>{{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}</p> <p>{{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}</p>
<pre>name,protocol,hostname,group,users,groups,guacd-encryption (attribute) <pre>{{'IMPORT.HELP_CSV_EXAMPLE' | translate }}</pre>
conn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none
conn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl
conn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,
conn4,kubernetes,,,,,</pre>
<h2>{{'IMPORT.HELP_JSON_HEADER' | translate}}</h2> <h2>{{'IMPORT.SECTION_HEADER_JSON' | translate}}</h2>
<p>{{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}</p> <p>{{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}</p>
<p>{{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}</p> <p>{{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}</p>
<pre>[ <pre>{{'IMPORT.HELP_JSON_EXAMPLE' | translate }}</pre>
{
"name": "conn1",
"protocol": "vnc",
"parameters": { "hostname": "conn1.web.com" },
"parentIdentifier": "ROOT",
"users": [ "guac user 1", "guac user 2" ],
"groups": [ "Connection 1 Users" ],
"attributes": { "guacd-encryption": "none" }
},
{
"name": "conn2",
"protocol": "rdp",
"parameters": { "hostname": "conn2.web.com" },
"group": "ROOT/Parent Group",
"users": [ "guac user 1" ],
"attributes": { "guacd-encryption": "none" }
},
{
"name": "conn3",
"protocol": "ssh",
"parameters": { "hostname": "conn3.web.com" },
"group": "ROOT/Parent Group/Child Group",
"users": [ "guac user 2", "guac user 3" ]
},
{
"name": "conn4",
"protocol": "kubernetes"
}
]</pre>
<h2>{{'IMPORT.HELP_YAML_HEADER' | translate}}</h2> <h2>{{'IMPORT.SECTION_HEADER_YAML' | translate}}</h2>
<p>{{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}</p> <p>{{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}</p>
<pre>--- <pre>{{'IMPORT.HELP_YAML_EXAMPLE' | translate}}</pre>
- name: conn1
protocol: vnc
parameters:
hostname: conn1.web.com
group: ROOT
users:
- guac user 1
- guac user 2
groups:
- Connection 1 Users
attributes:
guacd-encryption: none
- name: conn2
protocol: rdp
parameters:
hostname: conn2.web.com
group: ROOT/Parent Group
users:
- guac user 1
attributes:
guacd-encryption: none
- name: conn3
protocol: ssh
parameters:
hostname: conn3.web.com
group: ROOT/Parent Group/Child Group
users:
- guac user 2
- guac user 3
- name: conn4
protocol: kubernetes</pre>
<ol class="footnotes"> <ol class="footnotes">
<li>{{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}</li> <li>{{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}</li>

View File

@@ -33,13 +33,21 @@ angular.module('import').factory('DisplayErrorList', [
*/ */
const DisplayErrorList = function DisplayErrorList(messages) { const DisplayErrorList = function DisplayErrorList(messages) {
// Use empty message list by default /**
* The error messages that should be prepared for display.
*
* @type {String[]}
*/
this.messages = messages || []; this.messages = messages || [];
// The single String message composed of all messages concatenated /**
// together. This will be used for filtering / sorting, and should only * The single String message composed of all messages concatenated
// be calculated once. * together. This will be used for filtering / sorting, and should only
this.cachedMessage = null; * be calculated once, when toString() is called.
*
* @type {String}
*/
this.concatenatedMessage = null;
}; };

View File

@@ -71,9 +71,7 @@ angular.module('import').factory('ImportConnection', [
/** /**
* Connection configuration parameters, as dictated by the protocol in * Connection configuration parameters, as dictated by the protocol in
* use, arranged as name/value pairs. This information may not be * use, arranged as name/value pairs.
* available until directly queried. If this information is
* unavailable, this property will be null or undefined.
* *
* @type Object.<String, String> * @type Object.<String, String>
*/ */

View File

@@ -27,13 +27,15 @@ angular.module('import').factory('ImportConnectionError', ['$injector',
const DisplayErrorList = $injector.get('DisplayErrorList'); const DisplayErrorList = $injector.get('DisplayErrorList');
/** /**
* A representation of a connection to be imported, as parsed from an * A representation of the errors associated with a connection to be
* imported, along with some basic information connection information to
* identify the connection having the error, as returned from a parsed
* user-supplied import file. * user-supplied import file.
* *
* @constructor * @constructor
* @param {ImportConnection|Object} [template={}] * @param {ImportConnectionError|Object} [template={}]
* The object whose properties should be copied within the new * The object whose properties should be copied within the new
* Connection. * ImportConnectionError.
*/ */
const ImportConnectionError = function ImportConnectionError(template) { const ImportConnectionError = function ImportConnectionError(template) {
@@ -46,22 +48,6 @@ angular.module('import').factory('ImportConnectionError', ['$injector',
*/ */
this.rowNumber = template.rowNumber; this.rowNumber = template.rowNumber;
/**
* 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 * The human-readable name of this connection, which is not necessarily
* unique. * unique.
@@ -78,22 +64,6 @@ angular.module('import').factory('ImportConnectionError', ['$injector',
*/ */
this.protocol = template.protocol; this.protocol = template.protocol;
/**
* 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 || [];
/** /**
* The error messages associated with this particular connection, if any. * The error messages associated with this particular connection, if any.
* *

View File

@@ -26,7 +26,7 @@ angular.module('import').factory('ParseResult', [function defineParseResult() {
* The result of parsing a connection import file - containing a list of * 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 * 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 * 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, and any errors that may have occurred while parsing
* each connection. * each connection.
* *
* @constructor * @constructor
@@ -49,7 +49,7 @@ angular.module('import').factory('ParseResult', [function defineParseResult() {
/** /**
* An object whose keys are the user identifiers of users specified * An object whose keys are the user identifiers of users specified
* in the batch import. and the keys are an array of indices of * in the batch import, and whose values are an array of indices of
* connections to which those users should be granted access. * connections to which those users should be granted access.
* *
* @type {Object.<String, Integer[]>} * @type {Object.<String, Integer[]>}
@@ -68,7 +68,7 @@ angular.module('import').factory('ParseResult', [function defineParseResult() {
/** /**
* An array of errors encountered while parsing the corresponding * An array of errors encountered while parsing the corresponding
* connection (at the same array index). Each connection should have a * connection (at the same array index). Each connection should have a
* an array of errors. If empty, no errors occured for this connection. * an array of errors. If empty, no errors occurred for this connection.
* *
* @type {ParseError[][]} * @type {ParseError[][]}
*/ */

View File

@@ -135,7 +135,7 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
resolve : { updateCurrentToken: updateCurrentToken } resolve : { updateCurrentToken: updateCurrentToken }
}) })
// Connection import page // Connection import file format help page
.when('/import/connection/file-format-help', { .when('/import/connection/file-format-help', {
title : 'APP.NAME', title : 'APP.NAME',
bodyClassName : 'settings', bodyClassName : 'settings',

View File

@@ -27,9 +27,6 @@ angular.module('rest').factory('connectionService', ['$injector',
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var cacheService = $injector.get('cacheService'); var cacheService = $injector.get('cacheService');
// Required types
const Error = $injector.get('Error');
var service = {}; var service = {};
/** /**
@@ -166,6 +163,10 @@ angular.module('rest').factory('connectionService', ['$injector',
* connection patching process, the entire request will fail, and no * connection patching process, the entire request will fail, and no
* changes will be persisted. * changes will be persisted.
* *
* @param {String} dataSource
* The identifier of the data source associated with the connections to
* be patched.
*
* @param {DirectoryPatch.<Connection>[]} patches * @param {DirectoryPatch.<Connection>[]} patches
* An array of patches to apply. * An array of patches to apply.
* *
@@ -194,7 +195,7 @@ angular.module('rest').factory('connectionService', ['$injector',
}); });
} };
/** /**
* Makes a request to the REST API to delete a connection, * Makes a request to the REST API to delete a connection,

View File

@@ -199,6 +199,10 @@ angular.module('rest').factory('userGroupService', ['$injector',
* connection patching process, the entire request will fail, and no * connection patching process, the entire request will fail, and no
* changes will be persisted. * changes will be persisted.
* *
* @param {String} dataSource
* The identifier of the data source associated with the user groups to
* be patched.
*
* @param {DirectoryPatch.<UserGroup>[]} patches * @param {DirectoryPatch.<UserGroup>[]} patches
* An array of patches to apply. * An array of patches to apply.
* *
@@ -221,7 +225,7 @@ angular.module('rest').factory('userGroupService', ['$injector',
return patchResponse; return patchResponse;
}); });
} };
return service; return service;

View File

@@ -245,6 +245,10 @@ angular.module('rest').factory('userService', ['$injector',
* connection patching process, the entire request will fail, and no * connection patching process, the entire request will fail, and no
* changes will be persisted. * changes will be persisted.
* *
* @param {String} dataSource
* The identifier of the data source associated with the users to be
* patched.
*
* @param {DirectoryPatch.<User>[]} patches * @param {DirectoryPatch.<User>[]} patches
* An array of patches to apply. * An array of patches to apply.
* *
@@ -267,7 +271,7 @@ angular.module('rest').factory('userService', ['$injector',
return patchResponse; return patchResponse;
}); });
} };
return service; return service;

View File

@@ -75,12 +75,12 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch(
/** /**
* Adds the specified object to the relation. * Adds the specified object to the relation.
*/ */
ADD : "add", ADD : 'add',
/** /**
* Removes the specified object from the relation. * Removes the specified object from the relation.
*/ */
REMOVE : "remove" REMOVE : 'remove'
}; };

View File

@@ -11,7 +11,7 @@
<a class="import-connections button" <a class="import-connections button"
ng-show="canCreateConnections()" ng-show="canCreateConnections()"
href="#/import/{{dataSource | escape}}/connection/">{{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}}</a> href="#/import/{{dataSource | escape}}/connection/">{{'SETTINGS_CONNECTIONS.ACTION_IMPORT' | translate}}</a>
<a class="add-connection button" <a class="add-connection button"
ng-show="canCreateConnections()" ng-show="canCreateConnections()"

View File

@@ -5,15 +5,17 @@
"APP" : { "APP" : {
"NAME" : "Apache Guacamole", "NAME" : "Apache Guacamole",
"VERSION" : "1.5.0", "VERSION" : "${project.version}",
"ACTION_ACKNOWLEDGE" : "OK", "ACTION_ACKNOWLEDGE" : "OK",
"ACTION_CANCEL" : "Cancel", "ACTION_CANCEL" : "Cancel",
"ACTION_CLEAR" : "Clear",
"ACTION_CLONE" : "Clone", "ACTION_CLONE" : "Clone",
"ACTION_CONTINUE" : "Continue", "ACTION_CONTINUE" : "Continue",
"ACTION_DELETE" : "Delete", "ACTION_DELETE" : "Delete",
"ACTION_DELETE_SESSIONS" : "Kill Sessions", "ACTION_DELETE_SESSIONS" : "Kill Sessions",
"ACTION_DOWNLOAD" : "Download", "ACTION_DOWNLOAD" : "Download",
"ACTION_IMPORT" : "Import",
"ACTION_LOGIN" : "Login", "ACTION_LOGIN" : "Login",
"ACTION_LOGIN_AGAIN" : "Re-login", "ACTION_LOGIN_AGAIN" : "Re-login",
"ACTION_LOGOUT" : "Logout", "ACTION_LOGOUT" : "Logout",
@@ -60,8 +62,8 @@
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
"ACTION_CLEAR_CLIENT_MESSAGES" : "Clear", "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR",
"ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR",
"ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE",
"ACTION_DISCONNECT" : "Disconnect", "ACTION_DISCONNECT" : "Disconnect",
"ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT",
@@ -187,73 +189,58 @@
"IMPORT": { "IMPORT": {
"ACTION_ACKNOWLEDGE": "@:APP.ACTION_ACKNOWLEDGE", "ACTION_ACKNOWLEDGE": "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_BROWSE": "Browse for File",
"BUTTON_CANCEL": "Cancel", "ACTION_CANCEL": "@:APP.ACTION_CANCEL",
"BUTTON_IMPORT": "Import Connections", "ACTION_CLEAR": "@:APP.ACTION_CLEAR",
"ACTION_VIEW_FORMAT_HELP": "View Format Tips",
"CONNECTIONS_IMPORTED_SUCCESS": "{NUMBER} connections imported successfully.", "ACTION_IMPORT": "@:APP.ACTION_IMPORT",
"ACTION_IMPORT_CONNECTIONS": "Import Connections",
"DIALOG_HEADER_ERROR": "@:APP.DIALOG_HEADER_ERROR", "DIALOG_HEADER_ERROR": "@:APP.DIALOG_HEADER_ERROR",
"DIALOG_HEADER_SUCCESS": "Success", "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_FILE_SINGLE_ONLY": "Please upload only a single file at a time",
"ERROR_INVALID_CSV_HEADER": "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter",
"ERROR_INVALID_FILE_TYPE": "Unsupported file type: \"{TYPE}\"",
"ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found",
"ERROR_INVALID_USER_GROUP_IDENTIFIERS": "User Groups not found: {IDENTIFIER_LIST}",
"ERROR_INVALID_USER_IDENTIFIERS": "Users not found: {IDENTIFIER_LIST}",
"ERROR_NO_FILE_SUPPLIED": "Please select a file to import",
"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",
"HEADER": "Connection Import",
"HELP_HEADER": "Connection Import File Format",
"HELP_FILE_TYPE_HEADER": "File Types",
"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_CSV_HEADER": "CSV Format",
"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_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-seperated.¹",
"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_JSON_HEADER": "JSON Format", "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_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_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_YAML_HEADER": "YAML Format",
"HELP_YAML_DESCRIPTION": "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.",
"HELP_SEMICOLON_FOOTNOTE": "If present, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", "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.",
"ERROR_AMBIGUOUS_CSV_HEADER": "SECTION_HEADER_CONNECTION_IMPORT": "Connection Import",
"Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE": "Connection Import File Format",
"ERROR_ARRAY_REQUIRED": "SECTION_HEADER_CSV": "CSV Format",
"The provided file must contain a list of connections", "SECTION_HEADER_JSON": "JSON Format",
"ERROR_DUPLICATE_CSV_HEADER": "SECTION_HEADER_YAML": "YAML Format",
"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_GROUP": "No group matching \"{GROUP}\" found",
"ERROR_INVALID_FILE_TYPE":
"Unsupported 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",
"ERROR_REQUIRED_PROTOCOL":
"No connection protocol found in the provided file",
"ERROR_REQUIRED_NAME":
"No connection name found in the provided file",
"ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time",
"TABLE_HEADER_ERRORS": "Errors",
"TABLE_HEADER_NAME": "Name", "TABLE_HEADER_NAME": "Name",
"TABLE_HEADER_PROTOCOL": "Protocol", "TABLE_HEADER_PROTOCOL": "Protocol",
"TABLE_HEADER_ERRORS" : "Errors", "TABLE_HEADER_ROW_NUMBER": "Row #"
"TABLE_HEADER_ROW_NUMBER": "Row #",
"UPLOAD_FILE_TYPES": "CSV, JSON, or YAML",
"UPLOAD_HELP_LINK": "View Format Tips",
"UPLOAD_DROP_TITLE": "Drop a File Here",
"UPLOAD_BROWSE_LINK": "Browse for File"
}, },
"DATA_SOURCE_DEFAULT" : { "DATA_SOURCE_DEFAULT" : {
@@ -978,7 +965,7 @@
"SETTINGS_CONNECTIONS" : { "SETTINGS_CONNECTIONS" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
"ACTION_IMPORT_CONNECTIONS" : "Import", "ACTION_IMPORT" : "@:APP.ACTION_IMPORT",
"ACTION_NEW_CONNECTION" : "New Connection", "ACTION_NEW_CONNECTION" : "New Connection",
"ACTION_NEW_CONNECTION_GROUP" : "New Group", "ACTION_NEW_CONNECTION_GROUP" : "New Group",
"ACTION_NEW_SHARING_PROFILE" : "New Sharing Profile", "ACTION_NEW_SHARING_PROFILE" : "New Sharing Profile",

View File

@@ -22,7 +22,6 @@ package org.apache.guacamole.rest;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleResourceNotFoundException;

View File

@@ -441,7 +441,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
// An outcome for each patch included in the request. This list // An outcome for each patch included in the request. This list
// may include both success and failure responses, though the // may include both success and failure responses, though the
// presense of any failure would indicated that the entire // presence of any failure would indicated that the entire
// request has failed and no changes have been made. // request has failed and no changes have been made.
List<APIPatchOutcome> patchOutcomes = new ArrayList<>(); List<APIPatchOutcome> patchOutcomes = new ArrayList<>();
@@ -457,8 +457,9 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
// endpoint requires that operations be performed atomically. // endpoint requires that operations be performed atomically.
if (!atomic) if (!atomic)
throw new GuacamoleUnsupportedException( throw new GuacamoleUnsupportedException(
"Atomic operations are not supported. " + "The extension providing this directory does not " +
"The patch cannot be executed."); "support Atomic Operations. The patch cannot be " +
"executed.");
// Keep a list of all objects that have been successfully // Keep a list of all objects that have been successfully
// added or removed // added or removed
@@ -482,7 +483,9 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
if (!path.startsWith("/")) if (!path.startsWith("/"))
throw new GuacamoleClientException("Patch paths must start with \"/\"."); throw new GuacamoleClientException("Patch paths must start with \"/\".");
if(patch.getOp() == APIPatch.Operation.add) { APIPatch.Operation op = patch.getOp();
if (op == APIPatch.Operation.add) {
// Filter/sanitize object contents // Filter/sanitize object contents
InternalType internal = filterAndTranslate(patch.getValue()); InternalType internal = filterAndTranslate(patch.getValue());
@@ -497,7 +500,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
// Add a success outcome describing the object creation // Add a success outcome describing the object creation
APIPatchOutcome response = new APIPatchOutcome( APIPatchOutcome response = new APIPatchOutcome(
patch.getOp(), internal.getIdentifier(), path); op, internal.getIdentifier(), path);
patchOutcomes.add(response); patchOutcomes.add(response);
creationSuccesses.add(response); creationSuccesses.add(response);
@@ -515,7 +518,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
*/ */
if (e instanceof GuacamoleException) if (e instanceof GuacamoleException)
patchOutcomes.add(new APIPatchError( patchOutcomes.add(new APIPatchError(
patch.getOp(), null, path, op, null, path,
((GuacamoleException) e).getMessage())); ((GuacamoleException) e).getMessage()));
// If an unexpected failure occurs, fall through to the // If an unexpected failure occurs, fall through to the
@@ -527,7 +530,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
} }
// Append each identifier to the list, to be removed atomically // Append each identifier to the list, to be removed atomically
else if (patch.getOp() == APIPatch.Operation.remove) { else if (op == APIPatch.Operation.remove) {
String identifier = path.substring(1); String identifier = path.substring(1);
@@ -541,7 +544,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
// Add a success outcome describing the object removal // Add a success outcome describing the object removal
APIPatchOutcome response = new APIPatchOutcome( APIPatchOutcome response = new APIPatchOutcome(
patch.getOp(), identifier, path); op, identifier, path);
patchOutcomes.add(response); patchOutcomes.add(response);
creationSuccesses.add(response); creationSuccesses.add(response);
} }
@@ -557,7 +560,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
*/ */
if (e instanceof GuacamoleException) if (e instanceof GuacamoleException)
patchOutcomes.add(new APIPatchError( patchOutcomes.add(new APIPatchError(
patch.getOp(), identifier, path, op, identifier, path,
((GuacamoleException) e).getMessage())); ((GuacamoleException) e).getMessage()));
// If an unexpected failure occurs, fall through to the // If an unexpected failure occurs, fall through to the
@@ -569,8 +572,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
else { else {
throw new GuacamoleUnsupportedException( throw new GuacamoleUnsupportedException(
"Unsupported patch operation \"" "Unsupported patch operation \"" + op + "\". "
+ patch.getOp() + "\". "
+ "Only add and remove are supported."); + "Only add and remove are supported.");
} }

View File

@@ -31,7 +31,6 @@ import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation;
* the user who submitted the Patch request. Rather than including the full * the user who submitted the Patch request. Rather than including the full
* contents of the value, only the identifier is included, allowing the user to * contents of the value, only the identifier is included, allowing the user to
* determine the identifier of any newly-created objects as part of the request. * determine the identifier of any newly-created objects as part of the request.
*
*/ */
public class APIPatchOutcome { public class APIPatchOutcome {
@@ -56,8 +55,13 @@ public class APIPatchOutcome {
* patch API request. * patch API request.
* *
* @param op * @param op
* The requested operation for the patch corresponding to this outcome.
*
* @param identifier * @param identifier
* The identifier for the value in patch corresponding to this outcome.
*
* @param path * @param path
* The path for the patch corresponding to this outcome.
*/ */
public APIPatchOutcome(Operation op, String identifier, String path) { public APIPatchOutcome(Operation op, String identifier, String path) {
this.op = op; this.op = op;