Skip to content

Commit c1a168d

Browse files
committed
GUACAMOLE-2250: Add an option to create groups on connection import.
1 parent 4245fbb commit c1a168d

7 files changed

Lines changed: 414 additions & 186 deletions

File tree

guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js

Lines changed: 275 additions & 52 deletions
Large diffs are not rendered by default.

guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ angular.module('import').directive('connectionImportErrors', [
5252
*/
5353
patchFailure : '=',
5454

55+
/**
56+
* The DirectoryPatch list sent to patchConnections. It is used to
57+
* map API errors back to import file rows.
58+
*
59+
* @type {DirectoryPatch[]}
60+
*/
61+
patches : '='
5562
}
5663
};
5764

@@ -114,7 +121,7 @@ angular.module('import').directive('connectionImportErrors', [
114121

115122
/**
116123
* Generate a ImportConnectionError representing any errors associated
117-
* with the row at the given index within the given parse result.
124+
* with the row at the given index within the given patch result.
118125
*
119126
* @param {ParseResult} parseResult
120127
* The result of parsing the connection import file.
@@ -131,37 +138,36 @@ angular.module('import').directive('connectionImportErrors', [
131138
* The connection error object associated with the given row in the
132139
* given parse result.
133140
*/
134-
const generateConnectionError = (parseResult, index, row) => {
141+
const generatePatchConnectionError = (parseResult, patches, index, row) => {
135142

136143
// Get the patch associated with the current row
137-
const patch = parseResult.patches[index];
144+
const patch = patches[index];
138145

139146
// The value of a patch is just the Connection object
140-
const connection = patch.value;
147+
const connectionObject = patch.value;
141148

142149
return new ImportConnectionError({
143150

144151
// Add 1 to the provided row to get the position in the file
145152
rowNumber: row + 1,
146153

147154
// Basic connection information - name, group, and protocol.
148-
name: connection.name,
155+
name: connectionObject.name,
149156
group: parseResult.groupPaths[index],
150-
protocol: connection.protocol,
157+
protocol: connectionObject.protocol,
151158

152159
// The human-readable error messages
153-
errors: new DisplayErrorList(
154-
[ ...(parseResult.errors[index] || []) ])
160+
errors: new DisplayErrorList([])
155161
});
156162
};
157163

158164
// If a new connection patch failure is seen, update the display list
159165
$scope.$watch('patchFailure', function patchFailureChanged(patchFailure) {
160166

161-
const { parseResult } = $scope;
167+
const { parseResult, patches } = $scope;
162168

163169
// Do not attempt to process anything before the data has loaded
164-
if (!patchFailure || !parseResult)
170+
if (!patchFailure || !parseResult || !patches)
165171
return;
166172

167173
// All promises from all translation requests. The scope will not be
@@ -183,7 +189,7 @@ angular.module('import').directive('connectionImportErrors', [
183189

184190
// Set up the list of connection errors based on the existing parse
185191
// result, with error messages fetched from the patch failure
186-
const connectionErrors = parseResult.patches.reduce(
192+
const connectionErrors = patches.reduce(
187193
(errors, patch, index) => {
188194

189195
// Do not process display REMOVE patches - they are always
@@ -204,8 +210,8 @@ angular.module('import').directive('connectionImportErrors', [
204210
}
205211

206212
// Generate a connection error for display
207-
const connectionError = generateConnectionError(
208-
parseResult, index, row++);
213+
const connectionError = generatePatchConnectionError(
214+
parseResult, patches, index, row++);
209215

210216
// Add the error associated with the previous REMOVE patch, if
211217
// any, to the error associated with the current patch, if any
@@ -238,6 +244,45 @@ angular.module('import').directive('connectionImportErrors', [
238244

239245
});
240246

247+
/**
248+
* Generate a ImportConnectionError representing any errors associated
249+
* with the row at the given index within the given parse result.
250+
*
251+
* @param {ParseResult} parseResult
252+
* The result of parsing the connection import file.
253+
*
254+
* @param {Integer} index
255+
* The current row within the patches array, 0-indexed.
256+
*
257+
* @param {Integer} row
258+
* The current row within the original connection, 0-indexed.
259+
* If any REMOVE patches are present, this may be greater than
260+
* the index.
261+
*
262+
* @returns {ImportConnectionError}
263+
* The connection error object associated with the given row in the
264+
* given parse result.
265+
*/
266+
const generateParseConnectionError = (parseResult, index, row) => {
267+
268+
// Get the connection details
269+
const connectionObject = parseResult.connectionObjects[index];
270+
271+
return new ImportConnectionError({
272+
273+
// Physical position in the file
274+
rowNumber: row + 1,
275+
276+
// Basic connection information - name, group, and protocol.
277+
name: connectionObject.name,
278+
group: connectionObject.group,
279+
protocol: connectionObject.protocol,
280+
281+
// Pull parse-time errors from the ImportConnection object
282+
errors: new DisplayErrorList([ ...(connectionObject.errors || []) ])
283+
});
284+
};
285+
241286
// If a new parse result with errors is seen, update the display list
242287
$scope.$watch('parseResult', function parseResultChanged(parseResult) {
243288

@@ -254,62 +299,47 @@ angular.module('import').directive('connectionImportErrors', [
254299
// entirely - if set, they will be from the previous file and no
255300
// longer relevant.
256301

257-
// The row number for display. Unlike the index, this number will
258-
// skip any REMOVE patches. In other words, this is the index of
259-
// connections within the original import file.
260-
let row = 0;
261-
262302
// Set up the list of connection errors based on the updated parse
263-
// result
264-
const connectionErrors = parseResult.patches.reduce(
265-
(errors, patch, index) => {
303+
// result using connectionObjects.
304+
const connectionErrors = parseResult.connectionObjects.map(
305+
(connectionObject, index) => {
266306

267-
// Do not process display REMOVE patches - they are always
268-
// followed by ADD patches containing the actual content
269-
// (and errors, if any)
270-
if (patch.op === DirectoryPatch.Operation.REMOVE)
271-
return errors;
272-
273-
// Generate a connection error for display
274-
const connectionError = generateConnectionError(
275-
parseResult, index, row++);
307+
// Generate a connection error for display.
308+
// In this new architecture, index and row are identical.
309+
const connectionError = generateParseConnectionError(
310+
parseResult, index, index);
276311

277-
// Go through the errors and check if any are translateable
278-
connectionError.errors.getArray().forEach(
279-
(error, errorIndex) => {
312+
// Go through the errors and check if any are translateable.
313+
// We access the underlying array from the DisplayErrorList.
314+
connectionError.errors.getArray().forEach((error, errorIndex) => {
280315

281316
// If this error is a ParseError, it can be translated.
282-
// NOTE: Generally one would translate error messages in the
283-
// template, but in this case, the connection errors need to
284-
// be raw strings in order to enable sorting and filtering.
285-
if (error instanceof ParseError)
317+
if (error instanceof ParseError) {
286318

287319
// Fetch the translation and update it when it's ready
288320
translationPromises.push($translate(
289-
error.key, error.variables)
290-
.then(translatedError => {
291-
connectionError.errors.getArray()[errorIndex] = translatedError;
292-
}));
321+
error.key, error.variables).then(translatedError => {
322+
connectionError.errors.getArray()[errorIndex] = translatedError;
323+
}));
324+
}
293325

294326
// If the error is not a known translatable type, add the
295327
// message directly to the error array
296-
else
297-
connectionError.errors.getArray()[errorIndex] = (
298-
error.message ? error.message : error);
328+
else {
329+
connectionError.errors.getArray()[errorIndex] = (
330+
error.message ? error.message : error);
331+
}
299332

300333
});
301334

302-
errors.push(connectionError);
303-
return errors;
304-
305-
}, []);
335+
return connectionError;
336+
});
306337

307338
// Once all the translations have been completed, update the
308339
// connectionErrors all in one go, to ensure no excessive reloading
309340
$q.all(translationPromises).then(() => {
310341
$scope.connectionErrors = connectionErrors;
311342
});
312-
313343
});
314344

315345
}];

guacamole/src/main/frontend/src/app/import/services/connectionParseService.js

Lines changed: 18 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ angular.module('import').factory('connectionParseService',
4141
['$injector', function connectionParseService($injector) {
4242

4343
// Required types
44-
const Connection = $injector.get('Connection');
4544
const ConnectionImportConfig = $injector.get('ConnectionImportConfig');
46-
const DirectoryPatch = $injector.get('DirectoryPatch');
4745
const ImportConnection = $injector.get('ImportConnection');
4846
const ParseError = $injector.get('ParseError');
4947
const ParseResult = $injector.get('ParseResult');
@@ -105,19 +103,18 @@ angular.module('import').factory('connectionParseService',
105103
const TreeLookups = template => ({
106104

107105
/**
108-
* A map of all known group paths to the corresponding identifier for
109-
* that group. The is that a user-provided import file might directly
110-
* specify a named group path like "ROOT", "ROOT/parent", or
111-
* "ROOT/parent/child". This field field will map all of the above to
112-
* the identifier of the appropriate group, if defined.
106+
* A map of group identifier to the path of that group, keyed by
107+
* identifier. Paths are of the form "ROOT", "ROOT/parent", or
108+
* "ROOT/parent/child".
113109
*
114110
* @type Object.<String, String>
115111
*/
116112
groupPathsByIdentifier: template.groupPathsByIdentifier || {},
117113

118114
/**
119-
* A map of all known group identifiers to the path of the corresponding
120-
* group. These paths are all of the form "ROOT/parent/child".
115+
* A map of group path to the identifier of that group, keyed by path.
116+
* Used when the import file specifies a group path rather than a
117+
* parentIdentifier.
121118
*
122119
* @type Object.<String, String>
123120
*/
@@ -166,11 +163,9 @@ angular.module('import').factory('connectionParseService',
166163
// To get the path for the current group, add the name
167164
const currentPath = prefix + group.name;
168165

169-
// Add the current path to the identifier map
170-
lookups.groupPathsByIdentifier[currentPath] = group.identifier;
171-
172-
// Add the current identifier to the path map
173-
lookups.groupIdentifiersByPath[group.identifier] = currentPath;
166+
// Map identifier to path and path to identifier
167+
lookups.groupPathsByIdentifier[group.identifier] = currentPath;
168+
lookups.groupIdentifiersByPath[currentPath] = group.identifier;
174169

175170
// Add each connection to the connection map
176171
_.forEach(group.childConnections,
@@ -239,9 +234,6 @@ angular.module('import').factory('connectionParseService',
239234

240235
// The identifier for the parent group of this connection
241236
let parentIdentifier;
242-
243-
// The operator to apply for this connection
244-
let op = DirectoryPatch.Operation.ADD;
245237

246238
// If both are specified, the parent group is ambigious
247239
if (providedIdentifier && connection.group) {
@@ -300,10 +292,10 @@ angular.module('import').factory('connectionParseService',
300292
group = group.slice(0, -1);
301293

302294
// Look up the parent identifier for the specified group path
303-
parentIdentifier = groupPathsByIdentifier[group];
295+
parentIdentifier = groupIdentifiersByPath[group];
304296

305297
// If the group doesn't match anything in the tree
306-
if (!parentIdentifier) {
298+
if (!parentIdentifier && !importConfig.createMissingGroups) {
307299
connection.errors.push(new ParseError({
308300
message: 'No group found named: ' + connection.group,
309301
key: 'IMPORT.ERROR_INVALID_GROUP',
@@ -578,7 +570,7 @@ angular.module('import').factory('connectionParseService',
578570
.then(({fieldTransformer, treeTransformer}) =>
579571
connectionData.reduce((parseResult, data) => {
580572

581-
const { patches, users, groups, groupPaths } = parseResult;
573+
const { connectionObjects, users, groups, groupPaths } = parseResult;
582574

583575
// Run the array data through each provided transform
584576
let connectionObject = data;
@@ -596,52 +588,14 @@ angular.module('import').factory('connectionParseService',
596588
if (connectionObject.errors.length)
597589
parseResult.hasErrors = true;
598590

599-
// The value for the patch is a full-fledged Connection
600-
const value = new Connection(connectionObject);
601-
602-
// If a new connection is being created
603-
if (connectionObject.importMode
604-
=== ImportConnection.ImportMode.CREATE)
605-
606-
// Add a patch for creating the connection
607-
patches.push(new DirectoryPatch({
608-
op: DirectoryPatch.Operation.ADD,
609-
path: '/',
610-
value
611-
}));
612-
613-
// The connection is being replaced, and permissions are only being
614-
// added, not replaced
615-
else if (importConfig.existingPermissionMode ===
616-
ConnectionImportConfig.ExistingPermissionMode.PRESERVE)
617-
618-
// Add a patch for replacing the connection
619-
patches.push(new DirectoryPatch({
620-
op: DirectoryPatch.Operation.REPLACE,
621-
path: '/' + connectionObject.identifier,
622-
value
623-
}));
591+
// Save the connection details
592+
connectionObjects.push(connectionObject);
624593

625-
// The connection is being replaced, and permissions are also being
626-
// replaced
627-
else {
628-
629-
// Add a patch for removing the existing connection
630-
patches.push(new DirectoryPatch({
631-
op: DirectoryPatch.Operation.REMOVE,
632-
path: '/' + connectionObject.identifier
633-
}));
634-
635-
// Increment the index for the additional remove patch
594+
// Logic to determine if this connection will later result in 2 patches
595+
if (connectionObject.importMode === ImportConnection.ImportMode.REPLACE &&
596+
importConfig.existingPermissionMode !== ConnectionImportConfig.ExistingPermissionMode.PRESERVE) {
597+
// Increment the index for the additional remove patch (which will be created during import)
636598
index += 1;
637-
638-
// Add a second patch for creating the replacement connection
639-
patches.push(new DirectoryPatch({
640-
op: DirectoryPatch.Operation.ADD,
641-
path: '/',
642-
value
643-
}));
644-
645599
}
646600

647601
// Save the connection group path into the parse result

guacamole/src/main/frontend/src/app/import/templates/connectionImport.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ <h2>{{'IMPORT.SECTION_HEADER_CONNECTION_IMPORT' | translate}}</h2>
5858
</label>
5959
<span ng-attr-title="{{'IMPORT.HELP_EXISTING_PERMISSION_MODE' | translate}}" class="help"></span>
6060
</li>
61+
<li>
62+
<input type="checkbox"
63+
id="create-missing-groups" ng-model="importConfig.createMissingGroups" />
64+
<label for="create-missing-groups">
65+
{{'IMPORT.FIELD_HEADER_CREATE_MISSING_GROUPS' | translate}}
66+
</label>
67+
<span ng-attr-title="{{'IMPORT.HELP_CREATE_MISSING_GROUPS' | translate}}" class="help"></span>
68+
</li>
6169
</ul>
6270

6371
</div>
@@ -76,7 +84,7 @@ <h2>{{'IMPORT.SECTION_HEADER_CONNECTION_IMPORT' | translate}}</h2>
7684
<div ng-show="isLoading()" class="loading"></div>
7785

7886
<!-- Connection specific errors, if there are any -->
79-
<connection-import-errors parse-result="parseResult" patch-failure="patchFailure" />
87+
<connection-import-errors parse-result="parseResult" patch-failure="patchFailure" patches="patches" />
8088

8189

8290
</div>

guacamole/src/main/frontend/src/app/import/types/ConnectionImportConfig.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ angular.module('import').factory('ConnectionImportConfig', [
5555
this.existingPermissionMode = template.existingPermissionMode
5656
|| ConnectionImportConfig.ExistingPermissionMode.PRESERVE;
5757

58+
/**
59+
* Whether to create connection groups referenced by the import file when
60+
* they do not already exist. When false, unknown group paths are treated
61+
* as parse errors (unless a valid parentIdentifier is set).
62+
*
63+
* @type Boolean
64+
*/
65+
this.createMissingGroups = template.createMissingGroups === true;
5866
};
5967

6068
/**

0 commit comments

Comments
 (0)