diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 3e364f509..775628889 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -68,8 +68,19 @@ public class ConnectionDirectory extends JDBCDirectory { @Override @Transactional 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 diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java index 0a9046c01..cc19847c1 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java @@ -90,4 +90,10 @@ public class DelegatingDirectory directory.remove(identifier); } + @Override + public void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + directory.tryAtomically(operation); + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java index 0d925d04f..63b55289b 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java @@ -21,6 +21,8 @@ package org.apache.guacamole.rest; import java.util.Collection; import java.util.Collections; +import java.util.List; + import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; 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.GuacamoleInsufficientCredentialsException; 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; /** @@ -71,6 +75,12 @@ public class APIError { */ private final Collection expected; + /** + * The outcome of each patch in the associated request, if this was a + * JSON Patch request. Otherwise null. + */ + private List patches = null; + /** * The type of error that occurred. */ @@ -202,13 +212,14 @@ public class APIError { 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 else this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY, 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 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 getPatches() { + return patches; + } + /** * Returns a human-readable error message describing the error that * occurred. diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 9ca71dba0..0b4e476bd 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -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.DirectoryFailureEvent; 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.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 @@ -344,7 +348,20 @@ public abstract class DirectoryResource> patches) + public APIPatchResponse patchObjects(List> patches) 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 patchOutcomes = new ArrayList<>(); + // Perform all requested operations atomically directory.tryAtomically(new AtomicDirectoryOperation() { @@ -432,13 +460,16 @@ public abstract class DirectoryResource addedObjects = new ArrayList<>(); Collection updatedObjects = new ArrayList<>(); Collection removedIdentifiers = new ArrayList<>(); + // A list of all responses associated with the successful + // creation of new objects + List creationSuccesses = new ArrayList<>(); + // True if any operation in the patch failed. Any failure will // fail the request, though won't result in immediate stoppage // since more errors may yet be uncovered. @@ -465,14 +496,33 @@ public abstract class DirectoryResource 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 @@ -536,7 +634,8 @@ public abstract class DirectoryResource * The type of object being patched. diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java new file mode 100644 index 000000000..85d28a714 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java @@ -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; + } +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java new file mode 100644 index 000000000..fa40bffc6 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java @@ -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 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 patches) { + + super(message ); + this.patches = patches; + } + + /** + * Return the outcome for each patch in the request corresponding to this + * response. + */ + public List getPatches() { + return patches; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java new file mode 100644 index 000000000..245664cbc --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java @@ -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; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java new file mode 100644 index 000000000..21096a2bc --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java @@ -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 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 patches) { + this.patches = patches; + } + + /** + * Return the outcome for each patch in the request corresponding to this + * response. + */ + public List getPatches() { + return patches; + } +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java new file mode 100644 index 000000000..b5f824f63 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java @@ -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; diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java index 38b337e0e..62f4d0ed9 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java @@ -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.Permission; 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