GUACAMOLE-926: Improve response plumbing through to user.

This commit is contained in:
James Muehlner
2023-01-23 23:40:48 +00:00
parent 7d1d5cdf13
commit 9cdbe0fb36
12 changed files with 497 additions and 30 deletions

View File

@@ -68,8 +68,19 @@ public class ConnectionDirectory extends JDBCDirectory<Connection> {
@Override @Override
@Transactional @Transactional
public void update(Connection object) throws GuacamoleException { public void update(Connection object) throws GuacamoleException {
ModeledConnection connection = (ModeledConnection) object;
connectionService.updateObject(getCurrentUser(), connection); // If the provided connection is already an internal type, update
// using the internal method
if (object instanceof ModeledConnection) {
ModeledConnection connection = (ModeledConnection) object;
connectionService.updateObject(getCurrentUser(), connection);
}
// If the type is not already the expected internal type, use the
// external update method
else {
connectionService.updateExternalObject(getCurrentUser(), object);
}
} }
@Override @Override

View File

@@ -90,4 +90,10 @@ public class DelegatingDirectory<ObjectType extends Identifiable>
directory.remove(identifier); directory.remove(identifier);
} }
@Override
public void tryAtomically(AtomicDirectoryOperation<ObjectType> operation)
throws GuacamoleException {
directory.tryAtomically(operation);
}
} }

View File

@@ -21,6 +21,8 @@ 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 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;
@@ -31,6 +33,8 @@ import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException;
import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome;
import org.apache.guacamole.tunnel.GuacamoleStreamException; import org.apache.guacamole.tunnel.GuacamoleStreamException;
/** /**
@@ -71,6 +75,12 @@ public class APIError {
*/ */
private final Collection<Field> expected; private final Collection<Field> expected;
/**
* The outcome of each patch in the associated request, if this was a
* JSON Patch request. Otherwise null.
*/
private List<APIPatchOutcome> patches = null;
/** /**
* The type of error that occurred. * The type of error that occurred.
*/ */
@@ -202,13 +212,14 @@ public class APIError {
this.translatableMessage = translatable.getTranslatableMessage(); this.translatableMessage = translatable.getTranslatableMessage();
} }
// TODO: Handle patch exceptions, need a bunch of JSON saying which things failed
// Use generic translation string if message is not translated // Use generic translation string if message is not translated
else else
this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY, this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY,
Collections.singletonMap(UNTRANSLATED_MESSAGE_VARIABLE_NAME, this.message)); Collections.singletonMap(UNTRANSLATED_MESSAGE_VARIABLE_NAME, this.message));
if (exception instanceof APIPatchFailureException)
this.patches = ((APIPatchFailureException) exception).getPatches();
} }
/** /**
@@ -245,6 +256,18 @@ public class APIError {
return expected; return expected;
} }
/**
* Return the outcome for every patch in the request, if the request was
* a JSON patch request. Otherwise, null.
*
* @return
* The outcome for every patch if responding to a JSON Patch request,
* otherwise null.
*/
public List<APIPatchOutcome> getPatches() {
return patches;
}
/** /**
* Returns a human-readable error message describing the error that * Returns a human-readable error message describing the error that
* occurred. * occurred.

View File

@@ -53,8 +53,12 @@ import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryEvent;
import org.apache.guacamole.net.event.DirectoryFailureEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent;
import org.apache.guacamole.net.event.DirectorySuccessEvent; import org.apache.guacamole.net.event.DirectorySuccessEvent;
import org.apache.guacamole.rest.APIPatch;
import org.apache.guacamole.rest.event.ListenerService; import org.apache.guacamole.rest.event.ListenerService;
import org.apache.guacamole.rest.jsonpatch.APIPatch;
import org.apache.guacamole.rest.jsonpatch.APIPatchError;
import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException;
import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome;
import org.apache.guacamole.rest.jsonpatch.APIPatchResponse;
/** /**
* A REST resource which abstracts the operations available on all Guacamole * A REST resource which abstracts the operations available on all Guacamole
@@ -344,7 +348,20 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
return resourceFactory; return resourceFactory;
} }
/**
* Filter and sanitize the provided external object, translate to the
* internal type, and return the translated internal object.
*
* @param object
* The external object to filter and translate.
*
* @return
* The filtered and translated internal object.
*
* @throws GuacamoleException
* If an error occurs while filtering or translating the external
* object.
*/
private InternalType filterAndTranslate(ExternalType object) private InternalType filterAndTranslate(ExternalType object)
throws GuacamoleException { throws GuacamoleException {
@@ -412,11 +429,22 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
* *
* @throws GuacamoleException * @throws GuacamoleException
* If an error occurs while adding, updating, or removing objects. * If an error occurs while adding, updating, or removing objects.
*
* @return
* A response describing the outcome of each patch. Only the identifier
* of each patched object will be included in the response, not the
* full object.
*/ */
@PATCH @PATCH
public void patchObjects(List<APIPatch<ExternalType>> patches) public APIPatchResponse patchObjects(List<APIPatch<ExternalType>> patches)
throws GuacamoleException { throws GuacamoleException {
// An outcome for each patch included in the request. This list
// may include both success and failure responses, though the
// presense of any failure would indicated that the entire
// request has failed and no changes have been made.
List<APIPatchOutcome> patchOutcomes = new ArrayList<>();
// Perform all requested operations atomically // Perform all requested operations atomically
directory.tryAtomically(new AtomicDirectoryOperation<InternalType>() { directory.tryAtomically(new AtomicDirectoryOperation<InternalType>() {
@@ -432,13 +460,16 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
"Atomic operations are not supported. " + "Atomic operations are not supported. " +
"The patch cannot be executed."); "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, updated, or removed // added, updated, or removed
Collection<InternalType> addedObjects = new ArrayList<>(); Collection<InternalType> addedObjects = new ArrayList<>();
Collection<InternalType> updatedObjects = new ArrayList<>(); Collection<InternalType> updatedObjects = new ArrayList<>();
Collection<String> removedIdentifiers = new ArrayList<>(); Collection<String> removedIdentifiers = new ArrayList<>();
// A list of all responses associated with the successful
// creation of new objects
List<APIPatchOutcome> creationSuccesses = new ArrayList<>();
// True if any operation in the patch failed. Any failure will // True if any operation in the patch failed. Any failure will
// fail the request, though won't result in immediate stoppage // fail the request, though won't result in immediate stoppage
// since more errors may yet be uncovered. // since more errors may yet be uncovered.
@@ -465,14 +496,33 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
// Add the object to the list if addition was successful // Add the object to the list if addition was successful
addedObjects.add(internal); addedObjects.add(internal);
// Add a success outcome describing the object creation
APIPatchOutcome response = new APIPatchOutcome(
patch.getOp(), internal.getIdentifier(), path);
patchOutcomes.add(response);
creationSuccesses.add(response);
} }
catch (GuacamoleException | RuntimeException | Error e) { catch (GuacamoleException | RuntimeException | Error e) {
failed = true;
fireDirectoryFailureEvent( fireDirectoryFailureEvent(
DirectoryEvent.Operation.ADD, DirectoryEvent.Operation.ADD,
internal.getIdentifier(), internal, e); internal.getIdentifier(), internal, e);
// TODO: Save the error for later inclusion in a big JSON error response /*
failed = true; * If the failure represents an understood issue,
* create a failure outcome for this failed patch.
*/
if (e instanceof GuacamoleException)
patchOutcomes.add(new APIPatchError(
patch.getOp(), null, path,
((GuacamoleException) e).getMessage()));
// If an unexpected failure occurs, fall through to the
// standard API error handling
else
throw e;
} }
} }
@@ -489,14 +539,33 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
// Add the object to the list if the update was successful // Add the object to the list if the update was successful
updatedObjects.add(internal); updatedObjects.add(internal);
// Add a success outcome describing the object update
APIPatchOutcome response = new APIPatchOutcome(
patch.getOp(), internal.getIdentifier(), path);
patchOutcomes.add(response);
creationSuccesses.add(response);
} }
catch (GuacamoleException | RuntimeException | Error e) { catch (GuacamoleException | RuntimeException | Error e) {
failed = true;
fireDirectoryFailureEvent( fireDirectoryFailureEvent(
DirectoryEvent.Operation.UPDATE, DirectoryEvent.Operation.UPDATE,
internal.getIdentifier(), internal, e); internal.getIdentifier(), internal, e);
// TODO: Save the error for later inclusion in a big JSON error response /*
failed = true; * If the failure represents an understood issue,
* create a failure outcome for this failed patch.
*/
if (e instanceof GuacamoleException)
patchOutcomes.add(new APIPatchError(
patch.getOp(), internal.getIdentifier(), path,
((GuacamoleException) e).getMessage()));
// If an unexpected failure occurs, fall through to the
// standard API error handling
else
throw e;
} }
} }
@@ -512,22 +581,51 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
// Add the object to the list if the removal was successful // Add the object to the list if the removal was successful
removedIdentifiers.add(identifier); removedIdentifiers.add(identifier);
// Add a success outcome describing the object removal
APIPatchOutcome response = new APIPatchOutcome(
patch.getOp(), identifier, path);
patchOutcomes.add(response);
creationSuccesses.add(response);
} }
catch (GuacamoleException | RuntimeException | Error e) { catch (GuacamoleException | RuntimeException | Error e) {
fireDirectoryFailureEvent(
DirectoryEvent.Operation.UPDATE, identifier, null, e);
// TODO: Save the error for later inclusion in a big JSON error response
failed = true; failed = true;
fireDirectoryFailureEvent(
DirectoryEvent.Operation.REMOVE,
identifier, null, e);
/*
* If the failure represents an understood issue,
* create a failure outcome for this failed patch.
*/
if (e instanceof GuacamoleException)
patchOutcomes.add(new APIPatchError(
patch.getOp(), identifier, path,
((GuacamoleException) e).getMessage()));
// If an unexpected failure occurs, fall through to the
// standard API error handling
else
throw e;
} }
} }
} }
// If any operation failed, fail now // If any operation failed
if (failed) { if (failed) {
throw new GuacamoleClientException(
"oh noes the patch batch failed"); // Any identifiers for objects created during this request
// will no longer be valid, since the creation of those
// objects will be rolled back.
creationSuccesses.forEach(
response -> response.clearIdentifier());
// Return an error response, including any failures that
// caused the failure of any patch in the request
throw new APIPatchFailureException(
"The provided patches failed to apply.", patchOutcomes);
} }
// Fire directory success events for each created object // Fire directory success events for each created object
@@ -536,7 +634,8 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
InternalType internal = addedIterator.next(); InternalType internal = addedIterator.next();
fireDirectorySuccessEvent( fireDirectorySuccessEvent(
DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); DirectoryEvent.Operation.ADD,
internal.getIdentifier(), internal);
} }
@@ -546,7 +645,8 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
InternalType internal = updatedIterator.next(); InternalType internal = updatedIterator.next();
fireDirectorySuccessEvent( fireDirectorySuccessEvent(
DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); DirectoryEvent.Operation.UPDATE,
internal.getIdentifier(), internal);
} }
@@ -556,7 +656,8 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
String identifier = removedIterator.next(); String identifier = removedIterator.next();
fireDirectorySuccessEvent( fireDirectorySuccessEvent(
DirectoryEvent.Operation.UPDATE, identifier, null); DirectoryEvent.Operation.UPDATE,
identifier, null);
} }
@@ -564,9 +665,8 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
}); });
// TODO: JSON response indicating which things were changed // Return a list of outcomes, one for each patch in the request
// NOTE: IDs are assigned at creation time so a user of the API return new APIPatchResponse(patchOutcomes);
// needs this response if they want to be able to actually use them
} }

View File

@@ -29,7 +29,7 @@ import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.RelatedObjectSet; import org.apache.guacamole.net.auth.RelatedObjectSet;
import org.apache.guacamole.rest.APIPatch; import org.apache.guacamole.rest.jsonpatch.APIPatch;
/** /**
* A REST resource which abstracts the operations available on arbitrary sets * A REST resource which abstracts the operations available on arbitrary sets

View File

@@ -17,11 +17,11 @@
* under the License. * under the License.
*/ */
package org.apache.guacamole.rest; package org.apache.guacamole.rest.jsonpatch;
/** /**
* An object for representing the body of a HTTP PATCH method. * An object for representing an entry within the body of a
* See https://tools.ietf.org/html/rfc6902 * JSON PATCH request. See https://tools.ietf.org/html/rfc6902
* *
* @param <T> * @param <T>
* The type of object being patched. * The type of object being patched.

View File

@@ -0,0 +1,71 @@
/*
* 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.
*/
package org.apache.guacamole.rest.jsonpatch;
import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation;
/**
* A failure outcome associated with a particular patch within a JSON Patch
* request. This status indicates that a particular patch failed to apply,
* and includes the error describing the failure, along with the operation and
* path from the original patch, and the identifier of the object
* referenced by the original patch.
*/
public class APIPatchError extends APIPatchOutcome {
/**
* The error associated with the submitted patch.
*/
private final String error;
/**
* Create a failure status associated with a submitted patch from a JSON
* patch API request.
*
* @param op
* The operation requested by the failed patch.
*
* @param identifier
* The identifier of the object associated with the failed patch. If
* the patch failed to create a new object, this will be null.
*
* @param path
* The patch from the failed patch.
*
* @param error
* The error message associated with the failure that prevented the
* patch from applying.
*/
public APIPatchError(
Operation op, String identifier, String path, String error) {
super(op, identifier, path);
this.error = error;
}
/**
* Return the error associated with the patch failure.
*
* @return
* The error associated with the patch failure.
*/
public String getError() {
return error;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.
*/
package org.apache.guacamole.rest.jsonpatch;
import java.util.List;
import org.apache.guacamole.GuacamoleClientException;
/**
* An exception describing a failure to apply the patches from a JSON Patch
* request. A list of outcomes is included, one for each patch in the request.
*/
public class APIPatchFailureException extends GuacamoleClientException {
/**
* A list of outcomes, each one corresponding to a patch in the request
* corresponding to this response. This may include a mix of successes and
* failures. Any failure will result in a failure of the entire request
* since JSON Patch requests are handled atomically.
*/
public final List<APIPatchOutcome> patches;
/**
* Create a new patch request failure with the provided list of outcomes
* for individual patches.
*
* @param message
* A human-readable message describing the overall request failure.
*
* @param patches
* A list of patch outcomes, one for each patch in the request
* associated with this response.
*/
public APIPatchFailureException(
String message, List<APIPatchOutcome> patches) {
super(message );
this.patches = patches;
}
/**
* Return the outcome for each patch in the request corresponding to this
* response.
*/
public List<APIPatchOutcome> getPatches() {
return patches;
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.
*/
package org.apache.guacamole.rest.jsonpatch;
import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation;
/**
* A successful outcome associated with a particular patch within a JSON Patch
* request. The outcome contains the operation requested by the original patch,
* the path from the original patch, and the identifier of the object corresponding
* to the value from the original patch.
*
* The purpose of this class is to present a relatively lightweight outcome for
* 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
* determine the identifier of any newly-created objects as part of the request.
*
*/
public class APIPatchOutcome {
/**
* The requested operation for the patch corresponding to this outcome.
*/
private final Operation op;
/**
* The identifier for the value in patch corresponding to this outcome.
* If the value in the patch was null, this identifier should also be null.
*/
private String identifier;
/**
* The path for the patch corresponding to this outcome.
*/
private final String path;
/**
* Create an outcome associated with a submitted patch, as part of a JSON
* patch API request.
*
* @param op
* @param identifier
* @param path
*/
public APIPatchOutcome(Operation op, String identifier, String path) {
this.op = op;
this.identifier = identifier;
this.path = path;
}
/**
* Clear the identifier associated with this patch outcome. This must
* be done when an identifier in a outcome refers to a temporary object
* that was rolled back during processing of a request.
*/
public void clearIdentifier() {
this.identifier = null;
}
/**
* Returns the requested operation for the patch corresponding to this
* outcome.
*
* @return
* The requested operation for the patch corresponding to this outcome.
*/
public Operation getOp() {
return op;
}
/**
* Returns the path for the patch corresponding to this outcome.
*
* @return
* The path for the patch corresponding to this outcome.
*/
public String getPath() {
return path;
}
/**
* Returns the identifier for the value in patch corresponding to this
* outcome, or null if the value in the patch was null.
*
* @return
* The identifier for the value in patch corresponding to this
* outcome, or null if the value was null.
*/
public String getIdentifier() {
return identifier;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.
*/
package org.apache.guacamole.rest.jsonpatch;
import java.util.List;
/**
* A REST response describing the successful application of a JSON PATCH
* request to a directory. This consists of a list of outcomes, one for each
* patch within the request, in the same order.
*/
public class APIPatchResponse {
/**
* A list of outcomes, each one corresponding to a patch in the request
* corresponding to this response.
*/
public final List<APIPatchOutcome> patches;
/**
* Create a new patch response with the provided list of outcomes for
* individual patches.
*
* @param patches
* A list of patch outcomes, one for each patch in the request
* associated with this response.
*/
public APIPatchResponse(List<APIPatchOutcome> patches) {
this.patches = patches;
}
/**
* Return the outcome for each patch in the request corresponding to this
* response.
*/
public List<APIPatchOutcome> getPatches() {
return patches;
}
}

View File

@@ -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.
*/
/**
* Classes related to JSON Patch HTTP requests or responses.
* See https://www.rfc-editor.org/rfc/rfc6902.
*/
package org.apache.guacamole.rest.jsonpatch;

View File

@@ -31,7 +31,7 @@ import org.apache.guacamole.net.auth.Permissions;
import org.apache.guacamole.net.auth.permission.ObjectPermission; import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.Permission; import org.apache.guacamole.net.auth.permission.Permission;
import org.apache.guacamole.net.auth.permission.SystemPermission; import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.apache.guacamole.rest.APIPatch; import org.apache.guacamole.rest.jsonpatch.APIPatch;
/** /**
* A REST resource which abstracts the operations available on the permissions * A REST resource which abstracts the operations available on the permissions