diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java new file mode 100644 index 000000000..1d5e196eb --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java @@ -0,0 +1,57 @@ +/* + * 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.net.auth; + +import org.apache.guacamole.GuacamoleException; + +/** + * An operation that should be attempted atomically when passed to + * {@link Directory#tryAtomically()}, if atomic operations are supported by + * the Directory. + */ +public interface AtomicDirectoryOperation { + + /** + * Attempt the operation atomically. If the Directory does not support + * atomic operations, the atomic flag will be set to false. If the atomic + * flag is set to true, the provided directory is guaranteed to perform + * the operations within this function atomically. Atomicity of the + * provided directory outside this function, or of the directory invoking + * this function are not guaranteed. + * + * NOTE: If atomicity is required for this operation, a GuacamoleException + * may be thrown by this function before any changes are made, ensuring the + * operation will only ever be performed atomically. + * + * @param atomic + * True if the provided directory is guaranteed to peform the operation + * atomically within the context of this function. + * + * @param directory + * A directory that will perform the operation atomically if the atomic + * flag is set to true. If the flag is false, the directory may still + * be used, though atomicity is not guaranteed. + * + * @throws GuacamoleException + * If an issue occurs during the operation. + */ + void executeOperation(boolean atomic, Directory directory) + throws GuacamoleException; +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java index ba1e1522d..ac76fae9d 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java @@ -20,6 +20,7 @@ package org.apache.guacamole.net.auth; import java.util.Collection; +import java.util.Iterator; import java.util.Set; import org.apache.guacamole.GuacamoleException; @@ -198,6 +199,29 @@ public interface Directory { void add(ObjectType object) throws GuacamoleException; + /** + * Adds the given objects to the overall set. If new identifiers are + * created for any of the the added objects, the identifiers will be + * automatically assigned via setIdentifier(). + * + * @param objects + * The objects to add. + * + * @throws GuacamoleException + * If an error occurs while adding any of the objects, or if adding + * the objects is not allowed. + */ + default void add(Collection objects) + throws GuacamoleException { + + // Add each object individually by default + Iterator iterator = objects.iterator(); + while (iterator.hasNext()) { + add(iterator.next()); + } + + } + /** * Updates the stored object with the data contained in the given object. * @@ -209,14 +233,73 @@ public interface Directory { void update(ObjectType object) throws GuacamoleException; + /** + * Updates the stored objects with the data contained in the given objects. + * + * @param objects The objects which will supply the data for the update. + * + * @throws GuacamoleException If an error occurs while updating the objects, + * or if updating an object is not allowed. + */ + default void update(Collection objects) + throws GuacamoleException { + + // Update each object individually by default + Iterator iterator = objects.iterator(); + while (iterator.hasNext()) { + update(iterator.next()); + } + + } + /** * Removes the object with the given identifier from the overall set. * * @param identifier The identifier of the object to remove. * * @throws GuacamoleException If an error occurs while removing the object, - * or if removing object is not allowed. + * or if removing the object is not allowed. */ void remove(String identifier) throws GuacamoleException; + /** + * Removes all object with any of the given identifier from the overall set. + * + * @param identifiers The identifiers of the objects to remove. + * + * @throws GuacamoleException If an error occurs while removing an object, + * or if removing an object is not allowed. + */ + default void remove(Collection identifiers) + throws GuacamoleException { + + // Remove each object individually by default + Iterator iterator = identifiers.iterator(); + while (iterator.hasNext()) { + remove(iterator.next()); + } + + } + + /** + * Attempt to perform the provided operation atomically if possible. If the + * operation can be performed atomically, the atomic flag will be set to + * true, and the directory passed to the provided operation callback will + * peform directory operations atomically within the operation callback. + * + * @param operation + * The directory operation that should be performed atomically. + * + * @throws GuacamoleException + * If an error occurs during execution of the provided operation. + */ + default void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + + // By default, perform the operation non-atomically. If atomic operation + // is supported by an implementation, it must be implemented there. + operation.executeOperation(false, this); + + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java new file mode 100644 index 000000000..3a88d4270 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java @@ -0,0 +1,12 @@ +package org.apache.guacamole.rest.directory; + +import org.apache.guacamole.GuacamoleException; + +public class DirectoryOperationException extends GuacamoleException { + + public DirectoryOperationException(String message) { + super(message); + } + + +} 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 50d428528..8aa464bd0 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 @@ -19,8 +19,10 @@ package org.apache.guacamole.rest.directory; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import javax.inject.Inject; @@ -37,6 +39,7 @@ import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleUnsupportedException; +import org.apache.guacamole.net.auth.AtomicDirectoryOperation; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Directory; @@ -341,6 +344,17 @@ public abstract class DirectoryResource> patches) + public void patchObjects(List> patches) throws GuacamoleException { - // Apply each operation specified within the patch - for (APIPatch patch : patches) { + // Objects will be add, updated, and removed atomically + Collection objectsToAdd = new ArrayList<>(); + Collection objectsToUpdate = new ArrayList<>(); + Collection identifiersToRemove = new ArrayList<>(); - // Only remove is supported - if (patch.getOp() != APIPatch.Operation.remove) - throw new GuacamoleUnsupportedException("Only the \"remove\" " - + "operation is supported."); + // Apply each operation specified within the patch + for (APIPatch patch : patches) { // Retrieve and validate path String path = patch.getPath(); if (!path.startsWith("/")) throw new GuacamoleClientException("Patch paths must start with \"/\"."); - // Remove specified object - String identifier = path.substring(1); - try { - directory.remove(identifier); - fireDirectorySuccessEvent(DirectoryEvent.Operation.REMOVE, identifier, null); + // Append each provided object to the list, to be added atomically + if(patch.getOp() == APIPatch.Operation.add) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + // Add to the list of objects to create + objectsToAdd.add(internal); } - catch (GuacamoleException | RuntimeException | Error e) { - fireDirectoryFailureEvent(DirectoryEvent.Operation.REMOVE, identifier, null, e); - throw e; + + // Append each provided object to the list, to be updated atomically + else if (patch.getOp() == APIPatch.Operation.replace) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + // Add to the list of objects to update + objectsToUpdate.add(internal); } + // Append each identifier to the list, to be removed atomically + else if (patch.getOp() == APIPatch.Operation.remove) { + + String identifier = path.substring(1); + identifiersToRemove.add(identifier); + + } + + } + + // Perform all requested operations atomically + directory.tryAtomically(new AtomicDirectoryOperation() { + + @Override + public void executeOperation(boolean atomic, Directory directory) + throws GuacamoleException { + + // If the underlying directory implentation does not support + // atomic operations, abort the patch operation. This REST + // endpoint requires that operations be performed atomically. + if (!atomic) + throw new GuacamoleUnsupportedException( + "Atomic operations are not supported. " + + "The patch cannot be executed."); + + // First, create every object from the patch + directory.add(objectsToAdd); + + // Next, update every object from the patch + directory.update(objectsToUpdate); + + // Finally, remove every object from the patch + directory.remove(identifiersToRemove); + + } + + }); + + // Fire directory success events for each created object + Iterator addedIterator = objectsToAdd.iterator(); + while (addedIterator.hasNext()) { + + InternalType internal = addedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each updated object + Iterator updatedIterator = objectsToUpdate.iterator(); + while (updatedIterator.hasNext()) { + + InternalType internal = updatedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each removed object + Iterator removedIterator = identifiersToRemove.iterator(); + while (removedIterator.hasNext()) { + + String identifier = removedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, identifier, null); + } } @@ -453,8 +544,7 @@ public abstract class DirectoryResource