NIFIREG-321 Integrate revision concept into UI, REST API, and service layers

NIFIREG-321 Making response message more user friendly when encountering InvalidRevisionException

NIFIREG-321 Updating frontend to be consistent with revision error handling

NIFIREG-321 Verifying revisable entity exists on update/delete before checking revisions to produce appropriate response codes (404 vs 400)

NIFIREG-321 Fixing SecureFileIT to correctly delete entities so test-all-dbs profile passes

This closes #230.

Signed-off-by: Kevin Doran <kdoran@apache.org>
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
index 80f72bb..87a0592 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
@@ -18,6 +18,7 @@
 
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.field.Fields;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import java.io.IOException;
 import java.util.List;
@@ -55,9 +56,10 @@
      * Deletes the bucket with the given id.
      *
      * @param bucketId the id of the bucket to delete
+     * @param revision the revision info for the bucket being deleted
      * @return the deleted bucket
      */
-    Bucket delete(String bucketId) throws NiFiRegistryException, IOException;
+    Bucket delete(String bucketId, RevisionInfo revision) throws NiFiRegistryException, IOException;
 
     /**
      * Gets the fields that can be used to sort/search buckets.
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
index 6dd72e9..73f4e91 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
@@ -19,6 +19,7 @@
 import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import java.io.IOException;
 import java.util.List;
@@ -79,11 +80,12 @@
      *
      * @param bucketId a bucket id
      * @param flowId the id of the flow to delete
+     * @param revision the revision information for the entity being deleted
      * @return the deleted flow
      * @throws NiFiRegistryException if an error is encountered other than IOException
      * @throws IOException if an I/O error is encountered
      */
-    VersionedFlow delete(String bucketId, String flowId) throws NiFiRegistryException, IOException;
+    VersionedFlow delete(String bucketId, String flowId, RevisionInfo revision) throws NiFiRegistryException, IOException;
 
     /**
      * Gets the field info for flows.
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
index 479699e..dd9792d 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
@@ -16,7 +16,9 @@
  */
 package org.apache.nifi.registry.client.impl;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.client.Invocation;
@@ -45,6 +47,32 @@
     }
 
     /**
+     * Adds query parameters for the given RevisionInfo if populated.
+     *
+     * @param target the WebTarget
+     * @param revision the RevisionInfo
+     * @return the target with query params added
+     */
+    protected WebTarget addRevisionQueryParams(WebTarget target, RevisionInfo revision) {
+        if (revision == null) {
+            return target;
+        }
+
+        WebTarget localTarget = target;
+
+        final Long version = revision.getVersion();
+        if (version != null) {
+            localTarget = localTarget.queryParam("version", version.longValue());
+        }
+
+        final String clientId = revision.getClientId();
+        if (!StringUtils.isBlank(clientId)) {
+            localTarget = localTarget.queryParam("clientId", clientId);
+        }
+        return localTarget;
+    }
+
+    /**
      * Creates a new Invocation.Builder for the given WebTarget with the headers added to the builder.
      *
      * @param webTarget the target for the request
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
index f84f8c6..2be7b20 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
@@ -21,6 +21,7 @@
 import org.apache.nifi.registry.client.BucketClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.field.Fields;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.client.WebTarget;
@@ -105,16 +106,22 @@
     }
 
     @Override
-    public Bucket delete(final String bucketId) throws NiFiRegistryException, IOException {
+    public Bucket delete(final String bucketId, final RevisionInfo revision) throws NiFiRegistryException, IOException {
         if (StringUtils.isBlank(bucketId)) {
             throw new IllegalArgumentException("Bucket ID cannot be blank");
         }
 
+        if (revision == null) {
+            throw new IllegalArgumentException("RevisionInfo cannot be null");
+        }
+
         return executeAction("Error deleting bucket", () -> {
-            final WebTarget target = bucketsTarget
+            WebTarget target = bucketsTarget
                     .path("/{bucketId}")
                     .resolveTemplate("bucketId", bucketId);
 
+            target = addRevisionQueryParams(target, revision);
+
             return getRequestBuilder(target).delete(Bucket.class);
         });
     }
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
index 486a20a..2e6d4e3 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
@@ -22,6 +22,7 @@
 import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.client.WebTarget;
@@ -138,7 +139,8 @@
     }
 
     @Override
-    public VersionedFlow delete(final String bucketId, final String flowId) throws NiFiRegistryException, IOException {
+    public VersionedFlow delete(final String bucketId, final String flowId, final RevisionInfo revision)
+            throws NiFiRegistryException, IOException {
         if (StringUtils.isBlank(bucketId)) {
             throw new IllegalArgumentException("Bucket Identifier cannot be blank");
         }
@@ -147,12 +149,18 @@
             throw new IllegalArgumentException("Flow Identifier cannot be blank");
         }
 
+        if (revision == null) {
+            throw new IllegalArgumentException("RevisionInfo cannot be null");
+        }
+
         return executeAction("Error deleting flow", () -> {
-            final WebTarget target = bucketFlowsTarget
+            WebTarget target = bucketFlowsTarget
                     .path("/{flowId}")
                     .resolveTemplate("bucketId", bucketId)
                     .resolveTemplate("flowId", flowId);
 
+            target = addRevisionQueryParams(target, revision);
+
             return getRequestBuilder(target).delete(VersionedFlow.class);
         });
     }
diff --git a/nifi-registry-core/nifi-registry-data-model/pom.xml b/nifi-registry-core/nifi-registry-data-model/pom.xml
index 05352f7..c404598 100644
--- a/nifi-registry-core/nifi-registry-data-model/pom.xml
+++ b/nifi-registry-core/nifi-registry-data-model/pom.xml
@@ -33,6 +33,11 @@
             <groupId>javax.ws.rs</groupId>
             <artifactId>javax.ws.rs-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-revision-entity-model</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
     </dependencies>
 
     <profiles>
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java
index 909299e..e432dc4 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java
@@ -19,23 +19,28 @@
 
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.revision.entity.RevisableEntity;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 /**
  * Access policy summary of which actions ("read', "write", "delete") are allowable for a specified web resource.
  */
 @ApiModel
-public class AccessPolicySummary {
+public class AccessPolicySummary implements RevisableEntity {
 
     private String identifier;
     private String resource;
     private String action;
     private Boolean configurable;
+    private RevisionInfo revision;
 
     @ApiModelProperty(value = "The id of the policy. Set by server at creation time.", readOnly = true)
+    @Override
     public String getIdentifier() {
         return identifier;
     }
 
+    @Override
     public void setIdentifier(String identifier) {
         this.identifier = identifier;
     }
@@ -71,4 +76,19 @@
     public void setConfigurable(Boolean configurable) {
         this.configurable = configurable;
     }
+
+    @ApiModelProperty(
+            value = "The revision of this entity used for optimistic-locking during updates.",
+            readOnly = true
+    )
+    @Override
+    public RevisionInfo getRevision() {
+        return revision;
+    }
+
+    @Override
+    public void setRevision(RevisionInfo revision) {
+        this.revision = revision;
+    }
+
 }
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java
index 59d8c17..7d416ae 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java
@@ -18,6 +18,8 @@
 
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.revision.entity.RevisableEntity;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import java.util.Collection;
 import java.util.HashSet;
@@ -27,13 +29,14 @@
  * A tenant of this NiFi Registry
  */
 @ApiModel
-public class Tenant {
+public class Tenant implements RevisableEntity {
 
     private String identifier;
     private String identity;
     private Boolean configurable;
     private ResourcePermissions resourcePermissions;
     private Set<AccessPolicySummary> accessPolicies;
+    private RevisionInfo revision;
 
     public Tenant() {}
 
@@ -48,10 +51,12 @@
     @ApiModelProperty(
             value = "The computer-generated identifier of the tenant.",
             readOnly = true)
+    @Override
     public String getIdentifier() {
         return identifier;
     }
 
+    @Override
     public void setIdentifier(String identifier) {
         this.identifier = identifier;
     }
@@ -114,4 +119,17 @@
         }
     }
 
+    @ApiModelProperty(
+            value = "The revision of this entity used for optimistic-locking during updates.",
+            readOnly = true
+    )
+    @Override
+    public RevisionInfo getRevision() {
+        return revision;
+    }
+
+    @Override
+    public void setRevision(RevisionInfo revision) {
+        this.revision = revision;
+    }
 }
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java
index 9cd82e0..e94c38e 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java
@@ -20,6 +20,8 @@
 import io.swagger.annotations.ApiModelProperty;
 import org.apache.nifi.registry.authorization.Permissions;
 import org.apache.nifi.registry.link.LinkableEntity;
+import org.apache.nifi.registry.revision.entity.RevisableEntity;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotBlank;
@@ -28,7 +30,7 @@
 
 @XmlRootElement
 @ApiModel
-public class Bucket extends LinkableEntity {
+public class Bucket extends LinkableEntity implements RevisableEntity {
 
     @NotBlank
     private String identifier;
@@ -47,6 +49,8 @@
 
     private Permissions permissions;
 
+    private RevisionInfo revision;
+
     @ApiModelProperty(value = "An ID to uniquely identify this object.", readOnly = true)
     public String getIdentifier() {
         return identifier;
@@ -110,6 +114,20 @@
         this.permissions = permissions;
     }
 
+    @ApiModelProperty(
+            value = "The revision of this entity used for optimistic-locking during updates.",
+            readOnly = true
+    )
+    @Override
+    public RevisionInfo getRevision() {
+        return revision;
+    }
+
+    @Override
+    public void setRevision(RevisionInfo revision) {
+        this.revision = revision;
+    }
+
     @Override
     public int hashCode() {
         return Objects.hashCode(this.getIdentifier());
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java
index 98a873d..09fe35b 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java
@@ -54,6 +54,7 @@
 
     private Permissions permissions;
 
+
     public BucketItem(final BucketItemType type) {
         this.type = type;
     }
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java
index 5c5a43a..818dddd 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java
@@ -20,9 +20,12 @@
 import io.swagger.annotations.ApiModelProperty;
 import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.bucket.BucketItemType;
+import org.apache.nifi.registry.revision.entity.RevisableEntity;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 
 import javax.validation.constraints.Min;
 import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Objects;
 
 /**
  * <p>
@@ -35,11 +38,13 @@
  */
 @XmlRootElement
 @ApiModel
-public class VersionedFlow extends BucketItem {
+public class VersionedFlow extends BucketItem implements RevisableEntity {
 
     @Min(0)
     private long versionCount;
 
+    private RevisionInfo revision;
+
     public VersionedFlow() {
         super(BucketItemType.Flow);
     }
@@ -53,4 +58,23 @@
         this.versionCount = versionCount;
     }
 
+    @ApiModelProperty(
+            value = "The revision of this entity used for optimistic-locking during updates.",
+            readOnly = true
+    )
+    @Override
+    public RevisionInfo getRevision() {
+        return revision;
+    }
+
+    @Override
+    public void setRevision(RevisionInfo revision) {
+        this.revision = revision;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.getIdentifier());
+    }
+
 }
diff --git a/nifi-registry-core/nifi-registry-framework/pom.xml b/nifi-registry-core/nifi-registry-framework/pom.xml
index 28ccc1b..67f77c4 100644
--- a/nifi-registry-core/nifi-registry-framework/pom.xml
+++ b/nifi-registry-core/nifi-registry-framework/pom.xml
@@ -193,6 +193,11 @@
             <version>1.0.0-SNAPSHOT</version>
         </dependency>
         <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-flow-diff</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
             <version>3.1.0</version>
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
index 5190b64..12f39b6 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.service;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.authorization.AccessPolicy;
 import org.apache.nifi.registry.authorization.AccessPolicySummary;
 import org.apache.nifi.registry.authorization.CurrentUser;
@@ -26,6 +27,7 @@
 import org.apache.nifi.registry.authorization.User;
 import org.apache.nifi.registry.authorization.UserGroup;
 import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
 import org.apache.nifi.registry.security.authorization.AccessPolicyProvider;
 import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext;
 import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
@@ -49,6 +51,8 @@
 import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
 import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
 import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -57,14 +61,16 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.stream.Collectors;
 
+/**
+ * Service for performing operations on users, groups, and policies.
+ */
 @Service
 public class AuthorizationService {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationService.class);
+
     public static final String MSG_NON_MANAGED_AUTHORIZER = "This NiFi Registry is not configured to internally manage users, groups, or policies. Please contact your system administrator.";
     public static final String MSG_NON_CONFIGURABLE_POLICIES = "This NiFi Registry is not configured to allow configurable policies. Please contact your system administrator.";
     public static final String MSG_NON_CONFIGURABLE_USERS = "This NiFi Registry is not configured to allow configurable users and groups. Please contact your system administrator.";
@@ -75,10 +81,6 @@
     private UserGroupProvider userGroupProvider;
     private AccessPolicyProvider accessPolicyProvider;
 
-    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-    private final Lock readLock = lock.readLock();
-    private final Lock writeLock = lock.writeLock();
-
     @Autowired
     public AuthorizationService(
             final AuthorizableLookup authorizableLookup,
@@ -103,14 +105,41 @@
         return authorizableLookup;
     }
 
-    public Authorizer getAuthorizer() {
-        return authorizer;
-    }
-
     public void authorize(Authorizable authorizable, RequestAction action) throws AccessDeniedException {
         authorizable.authorize(authorizer, action, NiFiUserUtils.getNiFiUser());
     }
 
+    public boolean isManagedAuthorizer() {
+        return AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer);
+    }
+
+    public boolean isConfigurableUserGroupProvider() {
+        return AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer);
+    }
+
+    public boolean isConfigurableAccessPolicyProvider() {
+        return AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer);
+    }
+
+    public void verifyAuthorizerIsManaged() {
+        if (!isManagedAuthorizer()) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER);
+        }
+    }
+
+    public void verifyAuthorizerSupportsConfigurablePolicies() {
+        if (!isConfigurableAccessPolicyProvider()) {
+            verifyAuthorizerIsManaged();
+            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_POLICIES);
+        }
+    }
+
+    public void verifyAuthorizerSupportsConfigurableUserGroups() {
+        if (!isConfigurableUserGroupProvider()) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_USERS);
+        }
+    }
+
     // ---------------------- Permissions methods ---------------------------------------
 
     public CurrentUser getCurrentUser() {
@@ -176,255 +205,259 @@
 
     // ---------------------- User methods ----------------------------------------------
 
-    public User createUser(User user) {
+    public User createUser(final User user) {
         verifyUserGroupProviderIsConfigurable();
-        writeLock.lock();
-        try {
-            final org.apache.nifi.registry.security.authorization.User createdUser =
-                ((ConfigurableUserGroupProvider) userGroupProvider).addUser(userFromDTO(user));
-            return userToDTO(createdUser);
-        } finally {
-            writeLock.unlock();
+
+        if (StringUtils.isBlank(user.getIdentity())) {
+            throw new IllegalArgumentException("User identity must be specified when creating a new user.");
         }
+
+        final org.apache.nifi.registry.security.authorization.User createdUser =
+                configurableUserGroupProvider().addUser(userFromDTO(user));
+        return userToDTO(createdUser);
     }
 
     public List<User> getUsers() {
-        this.readLock.lock();
-        try {
-            return userGroupProvider.getUsers().stream().map(this::userToDTO).collect(Collectors.toList());
-        } finally {
-            this.readLock.unlock();
+        return userGroupProvider.getUsers().stream().map(this::userToDTO).collect(Collectors.toList());
+    }
+
+    public User getUser(final String identifier) {
+        final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier);
+        if (user == null) {
+            LOGGER.warn("The specified user id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+
+        return userToDTO(user);
+    }
+
+    public User getUserByIdentity(final String identity) {
+        final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUserByIdentity(identity);
+        if (user == null) {
+            LOGGER.warn("The specified user identity [{}] does not exist.", identity);
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+
+        return userToDTO(user);
+    }
+
+    public void verifyUserExists(final String identifier) {
+        final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier);
+        if (user == null) {
+            LOGGER.warn("The specified user id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
         }
     }
 
-    public User getUser(String identifier) {
-        this.readLock.lock();
-        try {
-            return userToDTO(userGroupProvider.getUser(identifier));
-        } finally {
-            this.readLock.unlock();
-        }
-    }
-
-    public User getUserByIdentity(String identity) {
-        this.readLock.lock();
-        try {
-            return userToDTO(userGroupProvider.getUserByIdentity(identity));
-        } finally {
-            this.readLock.unlock();
-        }
-    }
-
-    public User updateUser(User user) {
+    public User updateUser(final User user) {
         verifyUserGroupProviderIsConfigurable();
-        this.writeLock.lock();
-        try {
-            final org.apache.nifi.registry.security.authorization.User updatedUser =
-                    ((ConfigurableUserGroupProvider) userGroupProvider).updateUser(userFromDTO(user));
-            if (updatedUser == null) {
-                return null;
-            }
-            return userToDTO(updatedUser);
-        } finally {
-            this.writeLock.unlock();
+
+        final org.apache.nifi.registry.security.authorization.User updatedUser =
+                configurableUserGroupProvider().updateUser(userFromDTO(user));
+
+        if (updatedUser == null) {
+            LOGGER.warn("The specified user id [{}] does not exist.", user.getIdentifier());
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
         }
+
+        return userToDTO(updatedUser);
     }
 
-    public User deleteUser(String identifier) {
+    public User deleteUser(final String identifier) {
         verifyUserGroupProviderIsConfigurable();
-        this.writeLock.lock();
-        try {
-            User deletedUserDTO = getUser(identifier);
-            if (deletedUserDTO != null) {
-                ((ConfigurableUserGroupProvider) userGroupProvider).deleteUser(identifier);
-            }
-            return deletedUserDTO;
-        } finally {
-            this.writeLock.unlock();
-        }
-    }
 
+        final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier);
+        if (user == null) {
+            LOGGER.warn("The specified user id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+
+        configurableUserGroupProvider().deleteUser(user);
+        return userToDTO(user);
+    }
 
     // ---------------------- User Group methods --------------------------------------
 
-    public UserGroup createUserGroup(UserGroup userGroup) {
+    public UserGroup createUserGroup(final UserGroup userGroup) {
         verifyUserGroupProviderIsConfigurable();
-        writeLock.lock();
-        try {
-            final org.apache.nifi.registry.security.authorization.Group createdGroup =
-                    ((ConfigurableUserGroupProvider) userGroupProvider).addGroup(userGroupFromDTO(userGroup));
-            return userGroupToDTO(createdGroup);
-        } finally {
-            writeLock.unlock();
+
+        if (StringUtils.isBlank(userGroup.getIdentity())) {
+            throw new IllegalArgumentException("User group identity must be specified when creating a new group.");
         }
+
+        final org.apache.nifi.registry.security.authorization.Group createdGroup =
+                configurableUserGroupProvider().addGroup(userGroupFromDTO(userGroup));
+        return userGroupToDTO(createdGroup);
     }
 
     public List<UserGroup> getUserGroups() {
-        this.readLock.lock();
-        try {
-            return userGroupProvider.getGroups().stream().map(this::userGroupToDTO).collect(Collectors.toList());
-        } finally {
-            this.readLock.unlock();
+        return userGroupProvider.getGroups().stream().map(this::userGroupToDTO).collect(Collectors.toList());
+    }
+
+    public UserGroup getUserGroup(final String identifier) {
+        final org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier);
+
+        if (group == null) {
+            LOGGER.warn("The specified user group id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
+        }
+
+        return userGroupToDTO(group);
+    }
+
+    public void verifyUserGroupExists(final String identifier) {
+        final org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier);
+        if (group == null) {
+            LOGGER.warn("The specified user group id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
         }
     }
 
-    public UserGroup getUserGroup(String identifier) {
-        this.readLock.lock();
-        try {
-            return userGroupToDTO(userGroupProvider.getGroup(identifier));
-        } finally {
-            this.readLock.unlock();
-        }
-    }
-
-    public UserGroup updateUserGroup(UserGroup userGroup) {
+    public UserGroup updateUserGroup(final UserGroup userGroup) {
         verifyUserGroupProviderIsConfigurable();
-        writeLock.lock();
-        try {
-            final org.apache.nifi.registry.security.authorization.Group updatedGroup =
-                    ((ConfigurableUserGroupProvider) userGroupProvider).updateGroup(userGroupFromDTO(userGroup));
-            if (updatedGroup == null) {
-                return null;
-            }
-            return userGroupToDTO(updatedGroup);
-        } finally {
-            writeLock.unlock();
+
+        final org.apache.nifi.registry.security.authorization.Group updatedGroup =
+                configurableUserGroupProvider().updateGroup(userGroupFromDTO(userGroup));
+
+        if (updatedGroup == null) {
+            LOGGER.warn("The specified user group id [{}] does not exist.", userGroup.getIdentifier());
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
         }
+
+        return userGroupToDTO(updatedGroup);
     }
 
-    public UserGroup deleteUserGroup(String identifier) {
+    public UserGroup deleteUserGroup(final String identifier) {
         verifyUserGroupProviderIsConfigurable();
-        writeLock.lock();
-        try {
-            final UserGroup userGroupDTO = getUserGroup(identifier);
-            if (userGroupDTO != null) {
-                ((ConfigurableUserGroupProvider) userGroupProvider).deleteGroup(identifier);
-            }
-            return userGroupDTO;
-        } finally {
-            writeLock.unlock();
+
+        final Group group = userGroupProvider.getGroup(identifier);
+        if (group == null) {
+            LOGGER.warn("The specified user group id [{}] does not exist.", group.getIdentifier());
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
         }
+
+        configurableUserGroupProvider().deleteGroup(group);
+        return userGroupToDTO(group);
     }
 
 
     // ---------------------- Access Policy methods ----------------------------------------
 
-    public AccessPolicy createAccessPolicy(AccessPolicy accessPolicy) {
+    public AccessPolicy createAccessPolicy(final AccessPolicy accessPolicy) {
         verifyAccessPolicyProviderIsConfigurable();
-        writeLock.lock();
-        try {
-            org.apache.nifi.registry.security.authorization.AccessPolicy createdAccessPolicy =
-                    ((ConfigurableAccessPolicyProvider) accessPolicyProvider).addAccessPolicy(accessPolicyFromDTO(accessPolicy));
-            return accessPolicyToDTO(createdAccessPolicy);
-        } finally {
-            writeLock.unlock();
+
+        if (accessPolicy.getResource() == null) {
+            throw new IllegalArgumentException("Resource must be specified when creating a new access policy.");
         }
+
+        RequestAction.valueOfValue(accessPolicy.getAction());
+
+        final org.apache.nifi.registry.security.authorization.AccessPolicy createdAccessPolicy =
+                configurableAccessPolicyProvider().addAccessPolicy(accessPolicyFromDTO(accessPolicy));
+        return accessPolicyToDTO(createdAccessPolicy);
     }
 
-    public AccessPolicy getAccessPolicy(String identifier) {
-        readLock.lock();
-        try {
-            return accessPolicyToDTO(accessPolicyProvider.getAccessPolicy(identifier));
-        } finally {
-            readLock.unlock();
+    public AccessPolicy getAccessPolicy(final String identifier) {
+        final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy =
+                accessPolicyProvider.getAccessPolicy(identifier);
+
+        if (accessPolicy == null) {
+            LOGGER.warn("The specified access policy id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
         }
+
+        return accessPolicyToDTO(accessPolicy);
     }
 
-    public AccessPolicy getAccessPolicy(String resource, RequestAction action) {
-        readLock.lock();
-        try {
-            return accessPolicyToDTO(accessPolicyProvider.getAccessPolicy(resource, action));
-        } finally {
-            readLock.unlock();
+    public AccessPolicy getAccessPolicy(final String resource, final RequestAction action) {
+        final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy =
+                accessPolicyProvider.getAccessPolicy(resource, action);
+
+        if (accessPolicy == null) {
+            throw new ResourceNotFoundException("No policy found for action='" + action + "', resource='" + resource + "'");
         }
+
+        return accessPolicyToDTO(accessPolicy);
     }
 
     public List<AccessPolicy> getAccessPolicies() {
-        readLock.lock();
-        try {
-            return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToDTO).collect(Collectors.toList());
-        } finally {
-            readLock.unlock();
-        }
+        return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToDTO).collect(Collectors.toList());
     }
 
     public List<AccessPolicySummary> getAccessPolicySummaries() {
-        readLock.lock();
-        try {
-            return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToSummaryDTO).collect(Collectors.toList());
-        } finally {
-            readLock.unlock();
-        }
+        return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToSummaryDTO).collect(Collectors.toList());
     }
 
     private List<AccessPolicySummary> getAccessPolicySummariesForUser(String userIdentifier) {
-        readLock.lock();
-        try {
-            return accessPolicyProvider.getAccessPolicies().stream()
-                    .filter(accessPolicy -> {
-                        if (accessPolicy.getUsers().contains(userIdentifier)) {
-                            return true;
-                        }
-                        return accessPolicy.getGroups().stream().anyMatch(g -> {
-                            final Group group = userGroupProvider.getGroup(g);
-                            return group != null && group.getUsers().contains(userIdentifier);
-                        });
-                    })
-                    .map(this::accessPolicyToSummaryDTO)
-                    .collect(Collectors.toList());
-        } finally {
-            readLock.unlock();
-        }
+        return accessPolicyProvider.getAccessPolicies().stream()
+                .filter(accessPolicy -> {
+                    if (accessPolicy.getUsers().contains(userIdentifier)) {
+                        return true;
+                    }
+                    return accessPolicy.getGroups().stream().anyMatch(g -> {
+                        final Group group = userGroupProvider.getGroup(g);
+                        return group != null && group.getUsers().contains(userIdentifier);
+                    });
+                })
+                .map(this::accessPolicyToSummaryDTO)
+                .collect(Collectors.toList());
     }
 
     private List<AccessPolicySummary> getAccessPolicySummariesForUserGroup(String userGroupIdentifier) {
-        readLock.lock();
-        try {
-            return accessPolicyProvider.getAccessPolicies().stream()
-                    .filter(accessPolicy -> accessPolicy.getGroups().contains(userGroupIdentifier))
-                    .map(this::accessPolicyToSummaryDTO)
-                    .collect(Collectors.toList());
-        } finally {
-            readLock.unlock();
+        return accessPolicyProvider.getAccessPolicies().stream()
+                .filter(accessPolicy -> accessPolicy.getGroups().contains(userGroupIdentifier))
+                .map(this::accessPolicyToSummaryDTO)
+                .collect(Collectors.toList());
+    }
+
+    public void verifyAccessPolicyExists(final String identifier) {
+        final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy =
+                accessPolicyProvider.getAccessPolicy(identifier);
+        if (accessPolicy == null) {
+            LOGGER.warn("The specified access policy id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
         }
     }
 
-    public AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) {
+    public AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) {
         verifyAccessPolicyProviderIsConfigurable();
-        writeLock.lock();
-        try {
-            // Don't allow changing action or resource of existing policy (should only be adding/removing users/groups)
-            org.apache.nifi.registry.security.authorization.AccessPolicy currentAccessPolicy =
-                    accessPolicyProvider.getAccessPolicy(accessPolicy.getIdentifier());
-            if (currentAccessPolicy == null) {
-                return null;
-            }
-            accessPolicy.setResource(currentAccessPolicy.getResource());
-            accessPolicy.setAction(currentAccessPolicy.getAction().toString());
 
-            org.apache.nifi.registry.security.authorization.AccessPolicy updatedAccessPolicy =
-                    ((ConfigurableAccessPolicyProvider) accessPolicyProvider).updateAccessPolicy(accessPolicyFromDTO(accessPolicy));
-            if (updatedAccessPolicy == null) {
-                return null;
-            }
-            return accessPolicyToDTO(updatedAccessPolicy);
-        } finally {
-            writeLock.unlock();
+        // Don't allow changing action or resource of existing policy (should only be adding/removing users/groups)
+        final org.apache.nifi.registry.security.authorization.AccessPolicy currentAccessPolicy =
+                accessPolicyProvider.getAccessPolicy(accessPolicy.getIdentifier());
+
+        if (currentAccessPolicy == null) {
+            LOGGER.warn("The specified access policy id [{}] does not exist.", accessPolicy.getIdentifier());
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
         }
+
+        accessPolicy.setResource(currentAccessPolicy.getResource());
+        accessPolicy.setAction(currentAccessPolicy.getAction().toString());
+
+        final org.apache.nifi.registry.security.authorization.AccessPolicy updatedAccessPolicy =
+                configurableAccessPolicyProvider().updateAccessPolicy(accessPolicyFromDTO(accessPolicy));
+
+        if (updatedAccessPolicy == null) {
+            LOGGER.warn("The specified access policy id [{}] does not exist.", accessPolicy.getIdentifier());
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
+        }
+
+        return accessPolicyToDTO(updatedAccessPolicy);
     }
 
-    public AccessPolicy deleteAccessPolicy(String identifier) {
+    public AccessPolicy deleteAccessPolicy(final String identifier) {
         verifyAccessPolicyProviderIsConfigurable();
-        writeLock.lock();
-        try {
-            AccessPolicy deletedAccessPolicyDTO = getAccessPolicy(identifier);
-            if (deletedAccessPolicyDTO != null) {
-                ((ConfigurableAccessPolicyProvider) accessPolicyProvider).deleteAccessPolicy(identifier);
-            }
-            return deletedAccessPolicyDTO;
-        } finally {
-            writeLock.unlock();
+
+        final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy =
+                accessPolicyProvider.getAccessPolicy(identifier);
+
+        if (accessPolicy == null) {
+            LOGGER.warn("The specified access policy id [{}] does not exist.", identifier);
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
         }
+
+        configurableAccessPolicyProvider().deleteAccessPolicy(identifier);
+        return accessPolicyToDTO(accessPolicy);
     }
 
 
@@ -466,6 +499,14 @@
 
     // ---------------------- Private Helper methods --------------------------------------
 
+    private ConfigurableUserGroupProvider configurableUserGroupProvider() {
+        return ((ConfigurableUserGroupProvider) userGroupProvider);
+    }
+
+    private ConfigurableAccessPolicyProvider configurableAccessPolicyProvider() {
+        return ((ConfigurableAccessPolicyProvider) accessPolicyProvider);
+    }
+
     private void verifyUserGroupProviderIsConfigurable() {
         if (!(userGroupProvider instanceof ConfigurableUserGroupProvider)) {
             throw new IllegalStateException(MSG_NON_CONFIGURABLE_USERS);
@@ -608,17 +649,12 @@
     }
 
     private Tenant tenantIdToDTO(String identifier) {
-        this.readLock.lock();
-        try {
-            org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier);
-            if (user != null) {
-                return tenantToDTO(user);
-            } else {
-                org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier);
-                return tenantToDTO(group);
-            }
-        } finally {
-            this.readLock.unlock();
+        final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier);
+        if (user != null) {
+            return tenantToDTO(user);
+        } else {
+            org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier);
+            return tenantToDTO(group);
         }
     }
 
@@ -672,7 +708,7 @@
             return null;
         }
         return new org.apache.nifi.registry.security.authorization.User.Builder()
-                .identifier(userDTO.getIdentifier() != null ? userDTO.getIdentifier() : UUID.randomUUID().toString())
+                .identifier(userDTO.getIdentifier())
                 .identity(userDTO.getIdentity())
                 .build();
     }
@@ -683,7 +719,7 @@
             return null;
         }
         org.apache.nifi.registry.security.authorization.Group.Builder groupBuilder = new org.apache.nifi.registry.security.authorization.Group.Builder()
-                .identifier(userGroupDTO.getIdentifier() != null ? userGroupDTO.getIdentifier() : UUID.randomUUID().toString())
+                .identifier(userGroupDTO.getIdentifier())
                 .name(userGroupDTO.getIdentity());
         Set<Tenant> users = userGroupDTO.getUsers();
         if (users != null) {
@@ -696,7 +732,7 @@
             final AccessPolicy accessPolicyDTO) {
         org.apache.nifi.registry.security.authorization.AccessPolicy.Builder accessPolicyBuilder =
                 new org.apache.nifi.registry.security.authorization.AccessPolicy.Builder()
-                        .identifier(accessPolicyDTO.getIdentifier() != null ? accessPolicyDTO.getIdentifier() : UUID.randomUUID().toString())
+                        .identifier(accessPolicyDTO.getIdentifier())
                         .resource(accessPolicyDTO.getResource())
                         .action(RequestAction.valueOfValue(accessPolicyDTO.getAction()));
 
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
index 8df44e8..b50196d 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
@@ -31,21 +31,6 @@
 import org.apache.nifi.registry.exception.ResourceNotFoundException;
 import org.apache.nifi.registry.extension.BundleCoordinate;
 import org.apache.nifi.registry.extension.BundlePersistenceProvider;
-import org.apache.nifi.registry.extension.bundle.Bundle;
-import org.apache.nifi.registry.extension.bundle.BundleFilterParams;
-import org.apache.nifi.registry.extension.bundle.BundleType;
-import org.apache.nifi.registry.extension.bundle.BundleVersion;
-import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams;
-import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata;
-import org.apache.nifi.registry.extension.component.ExtensionFilterParams;
-import org.apache.nifi.registry.extension.component.ExtensionMetadata;
-import org.apache.nifi.registry.extension.component.TagCount;
-import org.apache.nifi.registry.extension.component.manifest.Extension;
-import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI;
-import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
-import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
-import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
-import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
 import org.apache.nifi.registry.flow.FlowPersistenceProvider;
 import org.apache.nifi.registry.flow.FlowSnapshotContext;
 import org.apache.nifi.registry.flow.VersionedComponent;
@@ -65,7 +50,6 @@
 import org.apache.nifi.registry.serialization.FlowContent;
 import org.apache.nifi.registry.serialization.FlowContentSerializer;
 import org.apache.nifi.registry.service.alias.RegistryUrlAliasService;
-import org.apache.nifi.registry.service.extension.ExtensionService;
 import org.apache.nifi.registry.service.mapper.BucketMappings;
 import org.apache.nifi.registry.service.mapper.ExtensionMappings;
 import org.apache.nifi.registry.service.mapper.FlowMappings;
@@ -73,16 +57,13 @@
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
 import javax.validation.ConstraintViolation;
 import javax.validation.ConstraintViolationException;
 import javax.validation.Validator;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -91,20 +72,12 @@
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
-import java.util.UUID;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.stream.Collectors;
 
 /**
- * Main service for all back-end operations, REST resources should only interact with this service.
- *
- * This service is marked as @Transactional so that Spring will automatically start a transaction upon entering
- * any method, and will rollback the transaction if any Exception is thrown out of a method.
- *
+ * Main service for all back-end operations on buckets and flows.
  */
 @Service
-@Transactional(rollbackFor = Exception.class)
 public class RegistryService {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(RegistryService.class);
@@ -113,27 +86,20 @@
     private final FlowPersistenceProvider flowPersistenceProvider;
     private final BundlePersistenceProvider bundlePersistenceProvider;
     private final FlowContentSerializer flowContentSerializer;
-    private final ExtensionService extensionService;
     private final Validator validator;
     private final RegistryUrlAliasService registryUrlAliasService;
 
-    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-    private final Lock readLock = lock.readLock();
-    private final Lock writeLock = lock.writeLock();
-
     @Autowired
     public RegistryService(final MetadataService metadataService,
                            final FlowPersistenceProvider flowPersistenceProvider,
                            final BundlePersistenceProvider bundlePersistenceProvider,
                            final FlowContentSerializer flowContentSerializer,
-                           final ExtensionService extensionService,
                            final Validator validator,
                            final RegistryUrlAliasService registryUrlAliasService) {
         this.metadataService = Validate.notNull(metadataService);
         this.flowPersistenceProvider = Validate.notNull(flowPersistenceProvider);
         this.bundlePersistenceProvider = Validate.notNull(bundlePersistenceProvider);
         this.flowContentSerializer = Validate.notNull(flowContentSerializer);
-        this.extensionService = Validate.notNull(extensionService);
         this.validator = Validate.notNull(validator);
         this.registryUrlAliasService = Validate.notNull(registryUrlAliasService);
     }
@@ -152,8 +118,7 @@
             throw new IllegalArgumentException("Bucket cannot be null");
         }
 
-        // set an id, the created time, and clear out the flows since its read-only
-        bucket.setIdentifier(UUID.randomUUID().toString());
+        // set the created time, and clear out the flows since its read-only
         bucket.setCreatedTimestamp(System.currentTimeMillis());
 
         if (bucket.isAllowBundleRedeploy() == null) {
@@ -166,18 +131,13 @@
 
         validate(bucket, "Cannot create Bucket");
 
-        writeLock.lock();
-        try {
-            final List<BucketEntity> bucketsWithSameName = metadataService.getBucketsByName(bucket.getName());
-            if (bucketsWithSameName.size() > 0) {
-                throw new IllegalStateException("A bucket with the same name already exists");
-            }
-
-            final BucketEntity createdBucket = metadataService.createBucket(BucketMappings.map(bucket));
-            return BucketMappings.map(createdBucket);
-        } finally {
-            writeLock.unlock();
+        final List<BucketEntity> bucketsWithSameName = metadataService.getBucketsByName(bucket.getName());
+        if (bucketsWithSameName.size() > 0) {
+            throw new IllegalStateException("A bucket with the same name already exists");
         }
+
+        final BucketEntity createdBucket = metadataService.createBucket(BucketMappings.map(bucket));
+        return BucketMappings.map(createdBucket);
     }
 
     public Bucket getBucket(final String bucketIdentifier) {
@@ -185,17 +145,24 @@
             throw new IllegalArgumentException("Bucket identifier cannot be null");
         }
 
-        readLock.lock();
-        try {
-            final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier);
-            if (bucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
+        final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier);
+        if (bucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+        }
 
-            return BucketMappings.map(bucket);
-        } finally {
-            readLock.unlock();
+        return BucketMappings.map(bucket);
+    }
+
+    public void verifyBucketExists(final String bucketIdentifier) {
+        if (bucketIdentifier == null) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null");
+        }
+
+        final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier);
+        if (bucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
     }
 
@@ -204,38 +171,23 @@
             throw new IllegalArgumentException("Bucket name cannot be null");
         }
 
-        readLock.lock();
-        try {
-            final List<BucketEntity> buckets = metadataService.getBucketsByName(bucketName);
-            if (buckets.isEmpty()) {
-                LOGGER.warn("The specified bucket name [{}] does not exist.", bucketName);
-                throw new ResourceNotFoundException("The specified bucket name does not exist in this registry.");
-            }
-
-            return BucketMappings.map(buckets.get(0));
-        } finally {
-            readLock.unlock();
+        final List<BucketEntity> buckets = metadataService.getBucketsByName(bucketName);
+        if (buckets.isEmpty()) {
+            LOGGER.warn("The specified bucket name [{}] does not exist.", bucketName);
+            throw new ResourceNotFoundException("The specified bucket name does not exist in this registry.");
         }
+
+        return BucketMappings.map(buckets.get(0));
     }
 
     public List<Bucket> getBuckets() {
-        readLock.lock();
-        try {
-            final List<BucketEntity> buckets = metadataService.getAllBuckets();
-            return buckets.stream().map(b -> BucketMappings.map(b)).collect(Collectors.toList());
-        } finally {
-            readLock.unlock();
-        }
+        final List<BucketEntity> buckets = metadataService.getAllBuckets();
+        return buckets.stream().map(b -> BucketMappings.map(b)).collect(Collectors.toList());
     }
 
     public List<Bucket> getBuckets(final Set<String> bucketIds) {
-        readLock.lock();
-        try {
-            final List<BucketEntity> buckets = metadataService.getBuckets(bucketIds);
-            return buckets.stream().map(b -> BucketMappings.map(b)).collect(Collectors.toList());
-        } finally {
-            readLock.unlock();
-        }
+        final List<BucketEntity> buckets = metadataService.getBuckets(bucketIds);
+        return buckets.stream().map(b -> BucketMappings.map(b)).collect(Collectors.toList());
     }
 
     public Bucket updateBucket(final Bucket bucket) {
@@ -251,51 +203,46 @@
             throw new IllegalArgumentException("Bucket name cannot be blank");
         }
 
-        writeLock.lock();
-        try {
-            // ensure a bucket with the given id exists
-            final BucketEntity existingBucketById = metadataService.getBucketById(bucket.getIdentifier());
-            if (existingBucketById == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucket.getIdentifier());
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
+        // ensure a bucket with the given id exists
+        final BucketEntity existingBucketById = metadataService.getBucketById(bucket.getIdentifier());
+        if (existingBucketById == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucket.getIdentifier());
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+        }
 
-            // ensure a different bucket with the same name does not exist
-            // since we're allowing partial updates here, only check this if a non-null name is provided
-            if (StringUtils.isNotBlank(bucket.getName())) {
-                final List<BucketEntity> bucketsWithSameName = metadataService.getBucketsByName(bucket.getName());
-                if (bucketsWithSameName != null) {
-                    for (final BucketEntity bucketWithSameName : bucketsWithSameName) {
-                        if (!bucketWithSameName.getId().equals(existingBucketById.getId())){
-                            throw new IllegalStateException("A bucket with the same name already exists - " + bucket.getName());
-                        }
+        // ensure a different bucket with the same name does not exist
+        // since we're allowing partial updates here, only check this if a non-null name is provided
+        if (StringUtils.isNotBlank(bucket.getName())) {
+            final List<BucketEntity> bucketsWithSameName = metadataService.getBucketsByName(bucket.getName());
+            if (bucketsWithSameName != null) {
+                for (final BucketEntity bucketWithSameName : bucketsWithSameName) {
+                    if (!bucketWithSameName.getId().equals(existingBucketById.getId())){
+                        throw new IllegalStateException("A bucket with the same name already exists - " + bucket.getName());
                     }
                 }
             }
-
-            // transfer over the new values to the existing bucket
-            if (StringUtils.isNotBlank(bucket.getName())) {
-                existingBucketById.setName(bucket.getName());
-            }
-
-            if (bucket.getDescription() != null) {
-                existingBucketById.setDescription(bucket.getDescription());
-            }
-
-            if (bucket.isAllowBundleRedeploy() != null) {
-                existingBucketById.setAllowExtensionBundleRedeploy(bucket.isAllowBundleRedeploy());
-            }
-
-            if (bucket.isAllowPublicRead() != null) {
-                existingBucketById.setAllowPublicRead(bucket.isAllowPublicRead());
-            }
-
-            // perform the actual update
-            final BucketEntity updatedBucket = metadataService.updateBucket(existingBucketById);
-            return BucketMappings.map(updatedBucket);
-        } finally {
-            writeLock.unlock();
         }
+
+        // transfer over the new values to the existing bucket
+        if (StringUtils.isNotBlank(bucket.getName())) {
+            existingBucketById.setName(bucket.getName());
+        }
+
+        if (bucket.getDescription() != null) {
+            existingBucketById.setDescription(bucket.getDescription());
+        }
+
+        if (bucket.isAllowBundleRedeploy() != null) {
+            existingBucketById.setAllowExtensionBundleRedeploy(bucket.isAllowBundleRedeploy());
+        }
+
+        if (bucket.isAllowPublicRead() != null) {
+            existingBucketById.setAllowPublicRead(bucket.isAllowPublicRead());
+        }
+
+        // perform the actual update
+        final BucketEntity updatedBucket = metadataService.updateBucket(existingBucketById);
+        return BucketMappings.map(updatedBucket);
     }
 
     public Bucket deleteBucket(final String bucketIdentifier) {
@@ -303,37 +250,32 @@
             throw new IllegalArgumentException("Bucket identifier cannot be null");
         }
 
-        writeLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // for each flow in the bucket, delete all snapshots from the flow persistence provider
-            for (final FlowEntity flowEntity : metadataService.getFlowsByBucket(existingBucket.getId())) {
-                flowPersistenceProvider.deleteAllFlowContent(bucketIdentifier, flowEntity.getId());
-            }
-
-            // for each bundle in the bucket, delete all versions from the bundle persistence provider
-            for (final BundleEntity bundleEntity : metadataService.getBundlesByBucket(existingBucket.getId())) {
-                final BundleCoordinate bundleCoordinate = new StandardBundleCoordinate.Builder()
-                        .bucketId(bundleEntity.getBucketId())
-                        .groupId(bundleEntity.getGroupId())
-                        .artifactId(bundleEntity.getArtifactId())
-                        .build();
-                bundlePersistenceProvider.deleteAllBundleVersions(bundleCoordinate);
-            }
-
-            // now delete the bucket from the metadata provider, which deletes all flows referencing it
-            metadataService.deleteBucket(existingBucket);
-
-            return BucketMappings.map(existingBucket);
-        } finally {
-            writeLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // for each flow in the bucket, delete all snapshots from the flow persistence provider
+        for (final FlowEntity flowEntity : metadataService.getFlowsByBucket(existingBucket.getId())) {
+            flowPersistenceProvider.deleteAllFlowContent(bucketIdentifier, flowEntity.getId());
+        }
+
+        // for each bundle in the bucket, delete all versions from the bundle persistence provider
+        for (final BundleEntity bundleEntity : metadataService.getBundlesByBucket(existingBucket.getId())) {
+            final BundleCoordinate bundleCoordinate = new StandardBundleCoordinate.Builder()
+                    .bucketId(bundleEntity.getBucketId())
+                    .groupId(bundleEntity.getGroupId())
+                    .artifactId(bundleEntity.getArtifactId())
+                    .build();
+            bundlePersistenceProvider.deleteAllBundleVersions(bundleCoordinate);
+        }
+
+        // now delete the bucket from the metadata provider, which deletes all flows referencing it
+        metadataService.deleteBucket(existingBucket);
+
+        return BucketMappings.map(existingBucket);
     }
 
     // ---------------------- BucketItem methods ---------------------------------------------
@@ -343,20 +285,15 @@
             throw new IllegalArgumentException("Bucket identifier cannot be null");
         }
 
-        readLock.lock();
-        try {
-            final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier);
-            if (bucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            final List<BucketItem> bucketItems = new ArrayList<>();
-            metadataService.getBucketItems(bucket.getId()).stream().forEach(b -> addBucketItem(bucketItems, b));
-            return bucketItems;
-        } finally {
-            readLock.unlock();
+        final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier);
+        if (bucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        final List<BucketItem> bucketItems = new ArrayList<>();
+        metadataService.getBucketItems(bucket.getId()).stream().forEach(b -> addBucketItem(bucketItems, b));
+        return bucketItems;
     }
 
     public List<BucketItem> getBucketItems(final Set<String> bucketIdentifiers) {
@@ -364,14 +301,9 @@
             throw new IllegalArgumentException("Bucket identifiers cannot be null or empty");
         }
 
-        readLock.lock();
-        try {
-            final List<BucketItem> bucketItems = new ArrayList<>();
-            metadataService.getBucketItems(bucketIdentifiers).stream().forEach(b -> addBucketItem(bucketItems, b));
-            return bucketItems;
-        } finally {
-            readLock.unlock();
-        }
+        final List<BucketItem> bucketItems = new ArrayList<>();
+        metadataService.getBucketItems(bucketIdentifiers).stream().forEach(b -> addBucketItem(bucketItems, b));
+        return bucketItems;
     }
 
     private void addBucketItem(final List<BucketItem> bucketItems, final BucketItemEntity itemEntity) {
@@ -406,39 +338,32 @@
             versionedFlow.setBucketIdentifier(bucketIdentifier);
         }
 
-        versionedFlow.setIdentifier(UUID.randomUUID().toString());
-
         final long timestamp = System.currentTimeMillis();
         versionedFlow.setCreatedTimestamp(timestamp);
         versionedFlow.setModifiedTimestamp(timestamp);
 
         validate(versionedFlow, "Cannot create versioned flow");
 
-        writeLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // ensure another flow with the same name doesn't exist
-            final List<FlowEntity> flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName());
-            if (flowsWithSameName != null && flowsWithSameName.size() > 0) {
-                throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket");
-            }
-
-            // convert from dto to entity and set the bucket relationship
-            final FlowEntity flowEntity = FlowMappings.map(versionedFlow);
-            flowEntity.setBucketId(existingBucket.getId());
-
-            // persist the flow and return the created entity
-            final FlowEntity createdFlow = metadataService.createFlow(flowEntity);
-            return FlowMappings.map(existingBucket, createdFlow);
-        } finally {
-            writeLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // ensure another flow with the same name doesn't exist
+        final List<FlowEntity> flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName());
+        if (flowsWithSameName != null && flowsWithSameName.size() > 0) {
+            throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket");
+        }
+
+        // convert from dto to entity and set the bucket relationship
+        final FlowEntity flowEntity = FlowMappings.map(versionedFlow);
+        flowEntity.setBucketId(existingBucket.getId());
+
+        // persist the flow and return the created entity
+        final FlowEntity createdFlow = metadataService.createFlow(flowEntity);
+        return FlowMappings.map(existingBucket, createdFlow);
     }
 
     public VersionedFlow getFlow(final String bucketIdentifier, final String flowIdentifier) {
@@ -450,29 +375,24 @@
             throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank");
         }
 
-        readLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
-
-            return FlowMappings.map(existingBucket, existingFlow);
-        } finally {
-            readLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
+
+        if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
+
+        return FlowMappings.map(existingBucket, existingFlow);
     }
 
     public VersionedFlow getFlow(final String flowIdentifier) {
@@ -480,18 +400,25 @@
             throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank");
         }
 
-        readLock.lock();
-        try {
-            final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist.");
-            }
+        final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist.");
+        }
 
-            final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId());
-            return FlowMappings.map(existingBucket, existingFlow);
-        } finally {
-            readLock.unlock();
+        final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId());
+        return FlowMappings.map(existingBucket, existingFlow);
+    }
+
+    public void verifyFlowExists(final String flowIdentifier) {
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank");
+        }
+
+        final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist.");
         }
     }
 
@@ -500,20 +427,15 @@
             throw new IllegalArgumentException("Bucket identifier cannot be null");
         }
 
-        readLock.lock();
-        try {
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketId);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketId);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // return non-verbose set of flows for the given bucket
-            final List<FlowEntity> flows = metadataService.getFlowsByBucket(existingBucket.getId());
-            return flows.stream().map(f -> FlowMappings.map(existingBucket, f)).collect(Collectors.toList());
-        } finally {
-            readLock.unlock();
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketId);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketId);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // return non-verbose set of flows for the given bucket
+        final List<FlowEntity> flows = metadataService.getFlowsByBucket(existingBucket.getId());
+        return flows.stream().map(f -> FlowMappings.map(existingBucket, f)).collect(Collectors.toList());
     }
 
     public VersionedFlow updateFlow(final VersionedFlow versionedFlow) {
@@ -533,53 +455,48 @@
             throw new IllegalArgumentException("Versioned flow name cannot be blank");
         }
 
-        writeLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(versionedFlow.getBucketIdentifier());
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", versionedFlow.getBucketIdentifier());
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(versionedFlow.getBucketIdentifier());
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", versionedFlow.getBucketIdentifier());
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+        }
 
-            final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(versionedFlow.getIdentifier());
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", versionedFlow.getIdentifier());
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
+        final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(versionedFlow.getIdentifier());
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", versionedFlow.getIdentifier());
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
 
-            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
+        if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
 
-            // ensure a different flow with the same name does not exist
-            // since we're allowing partial updates here, only check this if a non-null name is provided
-            if (StringUtils.isNotBlank(versionedFlow.getName())) {
-                final List<FlowEntity> flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName());
-                if (flowsWithSameName != null) {
-                    for (final FlowEntity flowWithSameName : flowsWithSameName) {
-                         if(!flowWithSameName.getId().equals(existingFlow.getId())) {
-                            throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket");
-                        }
+        // ensure a different flow with the same name does not exist
+        // since we're allowing partial updates here, only check this if a non-null name is provided
+        if (StringUtils.isNotBlank(versionedFlow.getName())) {
+            final List<FlowEntity> flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName());
+            if (flowsWithSameName != null) {
+                for (final FlowEntity flowWithSameName : flowsWithSameName) {
+                     if(!flowWithSameName.getId().equals(existingFlow.getId())) {
+                        throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket");
                     }
                 }
             }
-
-            // transfer over the new values to the existing flow
-            if (StringUtils.isNotBlank(versionedFlow.getName())) {
-                existingFlow.setName(versionedFlow.getName());
-            }
-
-            if (versionedFlow.getDescription() != null) {
-                existingFlow.setDescription(versionedFlow.getDescription());
-            }
-
-            // perform the actual update
-            final FlowEntity updatedFlow = metadataService.updateFlow(existingFlow);
-            return FlowMappings.map(existingBucket, updatedFlow);
-        } finally {
-            writeLock.unlock();
         }
+
+        // transfer over the new values to the existing flow
+        if (StringUtils.isNotBlank(versionedFlow.getName())) {
+            existingFlow.setName(versionedFlow.getName());
+        }
+
+        if (versionedFlow.getDescription() != null) {
+            existingFlow.setDescription(versionedFlow.getDescription());
+        }
+
+        // perform the actual update
+        final FlowEntity updatedFlow = metadataService.updateFlow(existingFlow);
+        return FlowMappings.map(existingBucket, updatedFlow);
     }
 
     public VersionedFlow deleteFlow(final String bucketIdentifier, final String flowIdentifier) {
@@ -590,36 +507,31 @@
             throw new IllegalArgumentException("Flow identifier cannot be null or blank");
         }
 
-        writeLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // ensure the flow exists
-            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
-
-            // delete all snapshots from the flow persistence provider
-            flowPersistenceProvider.deleteAllFlowContent(existingFlow.getBucketId(), existingFlow.getId());
-
-            // now delete the flow from the metadata provider
-            metadataService.deleteFlow(existingFlow);
-
-            return FlowMappings.map(existingBucket, existingFlow);
-        } finally {
-            writeLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // ensure the flow exists
+        final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
+
+        if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
+
+        // delete all snapshots from the flow persistence provider
+        flowPersistenceProvider.deleteAllFlowContent(existingFlow.getBucketId(), existingFlow.getId());
+
+        // now delete the flow from the metadata provider
+        metadataService.deleteFlow(existingFlow);
+
+        return FlowMappings.map(existingBucket, existingFlow);
     }
 
     // ---------------------- VersionedFlowSnapshot methods ---------------------------------------------
@@ -640,98 +552,93 @@
 
         validate(flowSnapshot, "Cannot create versioned flow snapshot");
 
-        writeLock.lock();
-        try {
-            final VersionedFlowSnapshotMetadata snapshotMetadata = flowSnapshot.getSnapshotMetadata();
+        final VersionedFlowSnapshotMetadata snapshotMetadata = flowSnapshot.getSnapshotMetadata();
 
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(snapshotMetadata.getBucketIdentifier());
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", snapshotMetadata.getBucketIdentifier());
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // ensure the flow exists
-            final FlowEntity existingFlow = metadataService.getFlowById(snapshotMetadata.getFlowIdentifier());
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", snapshotMetadata.getFlowIdentifier());
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
-
-            if (snapshotMetadata.getVersion() == 0) {
-                throw new IllegalArgumentException("Version must be greater than zero, or use -1 to indicate latest version");
-            }
-
-            // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata
-            final SortedSet<VersionedFlowSnapshotMetadata> sortedSnapshots = new TreeSet<>();
-            final List<FlowSnapshotEntity> existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId());
-            if (existingFlowSnapshots != null) {
-                existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(FlowMappings.map(existingBucket, s)));
-            }
-
-            // if we already have snapshots we need to verify the new one has the correct version
-            if (sortedSnapshots.size() > 0) {
-                final VersionedFlowSnapshotMetadata lastSnapshot = sortedSnapshots.last();
-
-                // if we have existing versions and a client sends -1, then make this the latest version
-                if (snapshotMetadata.getVersion() == -1) {
-                    snapshotMetadata.setVersion(lastSnapshot.getVersion() + 1);
-                } else if (snapshotMetadata.getVersion() <= lastSnapshot.getVersion()) {
-                    throw new IllegalStateException("A Versioned flow snapshot with the same version already exists: " + snapshotMetadata.getVersion());
-                } else if (snapshotMetadata.getVersion() > (lastSnapshot.getVersion() + 1)) {
-                    throw new IllegalStateException("Version must be a one-up number, last version was " + lastSnapshot.getVersion()
-                            + " and version for this snapshot was " + snapshotMetadata.getVersion());
-                }
-
-            } else if (snapshotMetadata.getVersion() == -1) {
-                // if we have no existing versions and a client sends -1, then this is the first version
-                snapshotMetadata.setVersion(1);
-            } else if (snapshotMetadata.getVersion() != 1) {
-                throw new IllegalStateException("Version of first snapshot must be 1");
-            }
-
-            // serialize the snapshot
-            final ByteArrayOutputStream out = new ByteArrayOutputStream();
-            registryUrlAliasService.setInternal(flowSnapshot.getFlowContents());
-
-            final FlowContent flowContent = new FlowContent();
-            flowContent.setFlowSnapshot(flowSnapshot);
-
-            // temporarily remove the metadata so it isn't serialized, but then put it back for returning the response
-            flowSnapshot.setSnapshotMetadata(null);
-            flowContentSerializer.serializeFlowContent(flowContent, out);
-            flowSnapshot.setSnapshotMetadata(snapshotMetadata);
-
-            // save the serialized snapshot to the persistence provider
-            final Bucket bucket = BucketMappings.map(existingBucket);
-            final VersionedFlow versionedFlow = FlowMappings.map(existingBucket, existingFlow);
-            final FlowSnapshotContext context = new StandardFlowSnapshotContext.Builder(bucket, versionedFlow, snapshotMetadata).build();
-            flowPersistenceProvider.saveFlowContent(context, out.toByteArray());
-
-            // create snapshot in the metadata provider
-            metadataService.createFlowSnapshot(FlowMappings.map(snapshotMetadata));
-
-            // update the modified date on the flow
-            metadataService.updateFlow(existingFlow);
-
-            // get the updated flow, we need to use "with counts" here so we can return this is a part of the response
-            final FlowEntity updatedFlow = metadataService.getFlowByIdWithSnapshotCounts(snapshotMetadata.getFlowIdentifier());
-            if (updatedFlow == null) {
-                throw new ResourceNotFoundException("Versioned flow does not exist for identifier " + snapshotMetadata.getFlowIdentifier());
-            }
-            final VersionedFlow updatedVersionedFlow = FlowMappings.map(existingBucket, updatedFlow);
-
-            flowSnapshot.setBucket(bucket);
-            flowSnapshot.setFlow(updatedVersionedFlow);
-            registryUrlAliasService.setExternal(flowSnapshot.getFlowContents());
-            return flowSnapshot;
-        } finally {
-            writeLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(snapshotMetadata.getBucketIdentifier());
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", snapshotMetadata.getBucketIdentifier());
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // ensure the flow exists
+        final FlowEntity existingFlow = metadataService.getFlowById(snapshotMetadata.getFlowIdentifier());
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", snapshotMetadata.getFlowIdentifier());
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
+
+        if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
+
+        if (snapshotMetadata.getVersion() == 0) {
+            throw new IllegalArgumentException("Version must be greater than zero, or use -1 to indicate latest version");
+        }
+
+        // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata
+        final SortedSet<VersionedFlowSnapshotMetadata> sortedSnapshots = new TreeSet<>();
+        final List<FlowSnapshotEntity> existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId());
+        if (existingFlowSnapshots != null) {
+            existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(FlowMappings.map(existingBucket, s)));
+        }
+
+        // if we already have snapshots we need to verify the new one has the correct version
+        if (sortedSnapshots.size() > 0) {
+            final VersionedFlowSnapshotMetadata lastSnapshot = sortedSnapshots.last();
+
+            // if we have existing versions and a client sends -1, then make this the latest version
+            if (snapshotMetadata.getVersion() == -1) {
+                snapshotMetadata.setVersion(lastSnapshot.getVersion() + 1);
+            } else if (snapshotMetadata.getVersion() <= lastSnapshot.getVersion()) {
+                throw new IllegalStateException("A Versioned flow snapshot with the same version already exists: " + snapshotMetadata.getVersion());
+            } else if (snapshotMetadata.getVersion() > (lastSnapshot.getVersion() + 1)) {
+                throw new IllegalStateException("Version must be a one-up number, last version was " + lastSnapshot.getVersion()
+                        + " and version for this snapshot was " + snapshotMetadata.getVersion());
+            }
+
+        } else if (snapshotMetadata.getVersion() == -1) {
+            // if we have no existing versions and a client sends -1, then this is the first version
+            snapshotMetadata.setVersion(1);
+        } else if (snapshotMetadata.getVersion() != 1) {
+            throw new IllegalStateException("Version of first snapshot must be 1");
+        }
+
+        // serialize the snapshot
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        registryUrlAliasService.setInternal(flowSnapshot.getFlowContents());
+
+        final FlowContent flowContent = new FlowContent();
+        flowContent.setFlowSnapshot(flowSnapshot);
+
+        // temporarily remove the metadata so it isn't serialized, but then put it back for returning the response
+        flowSnapshot.setSnapshotMetadata(null);
+        flowContentSerializer.serializeFlowContent(flowContent, out);
+        flowSnapshot.setSnapshotMetadata(snapshotMetadata);
+
+        // save the serialized snapshot to the persistence provider
+        final Bucket bucket = BucketMappings.map(existingBucket);
+        final VersionedFlow versionedFlow = FlowMappings.map(existingBucket, existingFlow);
+        final FlowSnapshotContext context = new StandardFlowSnapshotContext.Builder(bucket, versionedFlow, snapshotMetadata).build();
+        flowPersistenceProvider.saveFlowContent(context, out.toByteArray());
+
+        // create snapshot in the metadata provider
+        metadataService.createFlowSnapshot(FlowMappings.map(snapshotMetadata));
+
+        // update the modified date on the flow
+        metadataService.updateFlow(existingFlow);
+
+        // get the updated flow, we need to use "with counts" here so we can return this is a part of the response
+        final FlowEntity updatedFlow = metadataService.getFlowByIdWithSnapshotCounts(snapshotMetadata.getFlowIdentifier());
+        if (updatedFlow == null) {
+            throw new ResourceNotFoundException("Versioned flow does not exist for identifier " + snapshotMetadata.getFlowIdentifier());
+        }
+        final VersionedFlow updatedVersionedFlow = FlowMappings.map(existingBucket, updatedFlow);
+
+        flowSnapshot.setBucket(bucket);
+        flowSnapshot.setFlow(updatedVersionedFlow);
+        registryUrlAliasService.setExternal(flowSnapshot.getFlowContents());
+        return flowSnapshot;
     }
 
     public VersionedFlowSnapshot getFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) {
@@ -747,29 +654,24 @@
             throw new IllegalArgumentException("Version cannot be null or blank");
         }
 
-        readLock.lock();
-        try {
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // we need to populate the version count here so we have to do this retrieval instead of snapshotEntity.getFlow()
-            final FlowEntity flowEntityWithCount = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
-            if (flowEntityWithCount == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            if (!existingBucket.getId().equals(flowEntityWithCount.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
-
-            return getVersionedFlowSnapshot(existingBucket, flowEntityWithCount, version);
-        } finally {
-            readLock.unlock();
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // we need to populate the version count here so we have to do this retrieval instead of snapshotEntity.getFlow()
+        final FlowEntity flowEntityWithCount = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
+        if (flowEntityWithCount == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
+
+        if (!existingBucket.getId().equals(flowEntityWithCount.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
+
+        return getVersionedFlowSnapshot(existingBucket, flowEntityWithCount, version);
     }
 
     private VersionedFlowSnapshot getVersionedFlowSnapshot(final BucketEntity bucketEntity, final FlowEntity flowEntity, final Integer version) {
@@ -837,38 +739,32 @@
             throw new IllegalArgumentException("Flow identifier cannot be null or blank");
         }
 
-        readLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // ensure the flow exists
-            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
-
-            // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata, ordered by version descending
-            final SortedSet<VersionedFlowSnapshotMetadata> sortedSnapshots = new TreeSet<>(Collections.reverseOrder());
-            final List<FlowSnapshotEntity> existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId());
-            if (existingFlowSnapshots != null) {
-                existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(FlowMappings.map(existingBucket, s)));
-            }
-
-            return sortedSnapshots;
-
-        } finally {
-            readLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // ensure the flow exists
+        final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
+
+        if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
+
+        // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata, ordered by version descending
+        final SortedSet<VersionedFlowSnapshotMetadata> sortedSnapshots = new TreeSet<>(Collections.reverseOrder());
+        final List<FlowSnapshotEntity> existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId());
+        if (existingFlowSnapshots != null) {
+            existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(FlowMappings.map(existingBucket, s)));
+        }
+
+        return sortedSnapshots;
     }
 
     public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String bucketIdentifier, final String flowIdentifier) {
@@ -880,36 +776,31 @@
             throw new IllegalArgumentException("Flow identifier cannot be null or blank");
         }
 
-        readLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // ensure the flow exists
-            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
-
-            // get latest snapshot for the flow
-            final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId());
-            if (latestSnapshot == null) {
-                throw new ResourceNotFoundException("The specified flow ID has no versions");
-            }
-
-            return FlowMappings.map(existingBucket, latestSnapshot);
-        } finally {
-            readLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // ensure the flow exists
+        final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
+
+        if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
+
+        // get latest snapshot for the flow
+        final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId());
+        if (latestSnapshot == null) {
+            throw new ResourceNotFoundException("The specified flow ID has no versions");
+        }
+
+        return FlowMappings.map(existingBucket, latestSnapshot);
     }
 
     public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String flowIdentifier) {
@@ -917,32 +808,27 @@
             throw new IllegalArgumentException("Flow identifier cannot be null or blank");
         }
 
-        readLock.lock();
-        try {
-            // ensure the flow exists
-            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId());
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", existingFlow.getBucketId());
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // get latest snapshot for the flow
-            final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId());
-            if (latestSnapshot == null) {
-                throw new ResourceNotFoundException("The specified flow ID has no versions");
-            }
-
-            return FlowMappings.map(existingBucket, latestSnapshot);
-        } finally {
-            readLock.unlock();
+        // ensure the flow exists
+        final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
         }
+
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId());
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", existingFlow.getBucketId());
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+        }
+
+        // get latest snapshot for the flow
+        final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId());
+        if (latestSnapshot == null) {
+            throw new ResourceNotFoundException("The specified flow ID has no versions");
+        }
+
+        return FlowMappings.map(existingBucket, latestSnapshot);
     }
 
     public VersionedFlowSnapshotMetadata deleteFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) {
@@ -958,42 +844,37 @@
             throw new IllegalArgumentException("Version cannot be null or blank");
         }
 
-        writeLock.lock();
-        try {
-            // ensure the bucket exists
-            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
-            if (existingBucket == null) {
-                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
-                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
-            }
-
-            // ensure the flow exists
-            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
-            if (existingFlow == null) {
-                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
-                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
-            }
-
-            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
-                throw new IllegalStateException("The requested flow is not located in the given bucket");
-            }
-
-            // ensure the snapshot exists
-            final FlowSnapshotEntity snapshotEntity = metadataService.getFlowSnapshot(flowIdentifier, version);
-            if (snapshotEntity == null) {
-                throw new ResourceNotFoundException("Versioned flow snapshot does not exist for flow "
-                        + flowIdentifier + " and version " + version);
-            }
-
-            // delete the content of the snapshot
-            flowPersistenceProvider.deleteFlowContent(bucketIdentifier, flowIdentifier, version);
-
-            // delete the snapshot itself
-            metadataService.deleteFlowSnapshot(snapshotEntity);
-            return FlowMappings.map(existingBucket, snapshotEntity);
-        } finally {
-            writeLock.unlock();
+        // ensure the bucket exists
+        final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+        if (existingBucket == null) {
+            LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+            throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
         }
+
+        // ensure the flow exists
+        final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+        if (existingFlow == null) {
+            LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+            throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+        }
+
+        if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+            throw new IllegalStateException("The requested flow is not located in the given bucket");
+        }
+
+        // ensure the snapshot exists
+        final FlowSnapshotEntity snapshotEntity = metadataService.getFlowSnapshot(flowIdentifier, version);
+        if (snapshotEntity == null) {
+            throw new ResourceNotFoundException("Versioned flow snapshot does not exist for flow "
+                    + flowIdentifier + " and version " + version);
+        }
+
+        // delete the content of the snapshot
+        flowPersistenceProvider.deleteFlowContent(bucketIdentifier, flowIdentifier, version);
+
+        // delete the snapshot itself
+        metadataService.deleteFlowSnapshot(snapshotEntity);
+        return FlowMappings.map(existingBucket, snapshotEntity);
     }
 
     /**
@@ -1022,51 +903,46 @@
         final Integer older = Math.min(versionA, versionB);
         final Integer newer = Math.max(versionA, versionB);
 
-        readLock.lock();
-        try {
-            // Get the content for both versions of the flow
-            final byte[] serializedSnapshotA = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, older);
-            if (serializedSnapshotA == null || serializedSnapshotA.length == 0) {
-                throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
-                        + flowIdentifier + " and version " + older);
-            }
-
-            final byte[] serializedSnapshotB = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, newer);
-            if (serializedSnapshotB == null || serializedSnapshotB.length == 0) {
-                throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
-                        + flowIdentifier + " and version " + newer);
-            }
-
-            // deserialize the contents
-            final InputStream inputA = new ByteArrayInputStream(serializedSnapshotA);
-            final VersionedFlowSnapshot snapshotA = deserializeFlowContent(inputA);
-            final VersionedProcessGroup flowContentsA = snapshotA.getFlowContents();
-
-            final InputStream inputB = new ByteArrayInputStream(serializedSnapshotB);
-            final VersionedFlowSnapshot snapshotB = deserializeFlowContent(inputB);
-            final VersionedProcessGroup flowContentsB = snapshotB.getFlowContents();
-
-            final ComparableDataFlow comparableFlowA = new StandardComparableDataFlow(String.format("Version %d", older), flowContentsA);
-            final ComparableDataFlow comparableFlowB = new StandardComparableDataFlow(String.format("Version %d", newer), flowContentsB);
-
-            // Compare the two versions of the flow
-            final FlowComparator flowComparator = new StandardFlowComparator(comparableFlowA, comparableFlowB,
-                    null, new ConciseEvolvingDifferenceDescriptor());
-            final FlowComparison flowComparison = flowComparator.compare();
-
-            final VersionedFlowDifference result = new VersionedFlowDifference();
-            result.setBucketId(bucketIdentifier);
-            result.setFlowId(flowIdentifier);
-            result.setVersionA(older);
-            result.setVersionB(newer);
-
-            final Set<ComponentDifferenceGroup> differenceGroups = getStringComponentDifferenceGroupMap(flowComparison.getDifferences());
-            result.setComponentDifferenceGroups(differenceGroups);
-
-            return result;
-        } finally {
-            readLock.unlock();
+        // Get the content for both versions of the flow
+        final byte[] serializedSnapshotA = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, older);
+        if (serializedSnapshotA == null || serializedSnapshotA.length == 0) {
+            throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
+                    + flowIdentifier + " and version " + older);
         }
+
+        final byte[] serializedSnapshotB = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, newer);
+        if (serializedSnapshotB == null || serializedSnapshotB.length == 0) {
+            throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
+                    + flowIdentifier + " and version " + newer);
+        }
+
+        // deserialize the contents
+        final InputStream inputA = new ByteArrayInputStream(serializedSnapshotA);
+        final VersionedFlowSnapshot snapshotA = deserializeFlowContent(inputA);
+        final VersionedProcessGroup flowContentsA = snapshotA.getFlowContents();
+
+        final InputStream inputB = new ByteArrayInputStream(serializedSnapshotB);
+        final VersionedFlowSnapshot snapshotB = deserializeFlowContent(inputB);
+        final VersionedProcessGroup flowContentsB = snapshotB.getFlowContents();
+
+        final ComparableDataFlow comparableFlowA = new StandardComparableDataFlow(String.format("Version %d", older), flowContentsA);
+        final ComparableDataFlow comparableFlowB = new StandardComparableDataFlow(String.format("Version %d", newer), flowContentsB);
+
+        // Compare the two versions of the flow
+        final FlowComparator flowComparator = new StandardFlowComparator(comparableFlowA, comparableFlowB,
+                null, new ConciseEvolvingDifferenceDescriptor());
+        final FlowComparison flowComparison = flowComparator.compare();
+
+        final VersionedFlowDifference result = new VersionedFlowDifference();
+        result.setBucketId(bucketIdentifier);
+        result.setFlowId(flowIdentifier);
+        result.setVersionA(older);
+        result.setVersionB(newer);
+
+        final Set<ComponentDifferenceGroup> differenceGroups = getStringComponentDifferenceGroupMap(flowComparison.getDifferences());
+        result.setComponentDifferenceGroups(differenceGroups);
+
+        return result;
     }
 
     /**
@@ -1091,214 +967,6 @@
         return differenceGroups.values().stream().collect(Collectors.toSet());
     }
 
-    // ---------------------- Bundle methods ---------------------------------------------
-
-    public BundleVersion createBundleVersion(final String bucketIdentifier, final BundleType bundleType,
-                                             final InputStream inputStream, final String clientSha256) throws IOException {
-        writeLock.lock();
-        try {
-            return extensionService.createBundleVersion(bucketIdentifier, bundleType, inputStream, clientSha256);
-        } finally {
-            writeLock.unlock();
-        }
-    }
-
-    public List<Bundle> getBundles(final Set<String> bucketIdentifiers, final BundleFilterParams filterParams) {
-        readLock.lock();
-        try {
-            return extensionService.getBundles(bucketIdentifiers, filterParams);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public List<Bundle> getBundlesByBucket(final String bucketIdentifier) {
-        readLock.lock();
-        try {
-            return extensionService.getBundlesByBucket(bucketIdentifier);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public Bundle getBundle(final String extensionBundleId) {
-        readLock.lock();
-        try {
-            return extensionService.getBundle(extensionBundleId);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public Bundle deleteBundle(final Bundle bundle) {
-        writeLock.lock();
-        try {
-            return extensionService.deleteBundle(bundle);
-        } finally {
-            writeLock.unlock();
-        }
-    }
-
-    public SortedSet<BundleVersionMetadata> getBundleVersions(final Set<String> bucketIdentifiers, final BundleVersionFilterParams filterParams) {
-        readLock.lock();
-        try {
-            return extensionService.getBundleVersions(bucketIdentifiers, filterParams);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-
-    public SortedSet<BundleVersionMetadata> getBundleVersions(final String extensionBundleIdentifier) {
-        readLock.lock();
-        try {
-            return extensionService.getBundleVersions(extensionBundleIdentifier);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public BundleVersion getBundleVersion(final String bucketId, final String bundleId, final String version) {
-        readLock.lock();
-        try {
-            return extensionService.getBundleVersion(bucketId, bundleId, version);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public BundleVersion getBundleVersion(final String bucketId, final String groupId, final String artifactId, final String version) {
-        readLock.lock();
-        try {
-            return extensionService.getBundleVersion(bucketId, groupId, artifactId, version);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public void writeBundleVersionContent(final BundleVersion bundleVersion, final OutputStream out) {
-        readLock.lock();
-        try {
-            extensionService.writeBundleVersionContent(bundleVersion, out);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public BundleVersion deleteBundleVersion(final BundleVersion bundleVersion) {
-        writeLock.lock();
-        try {
-            return extensionService.deleteBundleVersion(bundleVersion);
-        } finally {
-            writeLock.unlock();
-        }
-    }
-
-    // ---------------------- Extension methods ---------------------------------------------
-
-    public SortedSet<ExtensionMetadata> getExtensionMetadata(final Set<String> bucketIdentifiers, final ExtensionFilterParams filterParams) {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionMetadata(bucketIdentifiers, filterParams);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public SortedSet<ExtensionMetadata> getExtensionMetadata(final Set<String> bucketIdentifiers, final ProvidedServiceAPI serviceAPI) {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionMetadata(bucketIdentifiers, serviceAPI);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public SortedSet<ExtensionMetadata> getExtensionMetadata(final BundleVersion bundleVersion) {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionMetadata(bundleVersion);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public Extension getExtension(final BundleVersion bundleVersion, final String name) {
-        readLock.lock();
-        try {
-            return extensionService.getExtension(bundleVersion, name);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public void writeExtensionDocs(final BundleVersion bundleVersion, final String name, final OutputStream outputStream)
-            throws IOException {
-        readLock.lock();
-        try {
-            extensionService.writeExtensionDocs(bundleVersion, name, outputStream);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public void writeAdditionalDetailsDocs(final BundleVersion bundleVersion, final String name, final OutputStream outputStream)
-            throws IOException {
-        readLock.lock();
-        try {
-            extensionService.writeAdditionalDetailsDocs(bundleVersion, name, outputStream);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public SortedSet<TagCount> getExtensionTags() {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionTags();
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    // ---------------------- Extension Repository methods ---------------------------------------------
-
-    public SortedSet<ExtensionRepoBucket> getExtensionRepoBuckets(final Set<String> bucketIds) {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionRepoBuckets(bucketIds);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public SortedSet<ExtensionRepoGroup> getExtensionRepoGroups(final Bucket bucket) {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionRepoGroups(bucket);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public SortedSet<ExtensionRepoArtifact> getExtensionRepoArtifacts(final Bucket bucket, final String groupId) {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionRepoArtifacts(bucket, groupId);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
-    public SortedSet<ExtensionRepoVersionSummary> getExtensionRepoVersions(final Bucket bucket, final String groupId, final String artifactId) {
-        readLock.lock();
-        try {
-            return extensionService.getExtensionRepoVersions(bucket, groupId, artifactId);
-        } finally {
-            readLock.unlock();
-        }
-    }
-
     // ---------------------- Field methods ---------------------------------------------
 
     public Set<String> getBucketFields() {
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V7__AddRevision.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V7__AddRevision.sql
new file mode 100644
index 0000000..eb37fcd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V7__AddRevision.sql
@@ -0,0 +1,21 @@
+-- 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.
+
+CREATE TABLE REVISION (
+    ENTITY_ID VARCHAR(200) NOT NULL,
+    VERSION BIGINT NOT NULL DEFAULT (0),
+    CLIENT_ID VARCHAR(100),
+    CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)
+);
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V7__AddRevision.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V7__AddRevision.sql
new file mode 100644
index 0000000..e29a515
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V7__AddRevision.sql
@@ -0,0 +1,21 @@
+-- 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.
+
+CREATE TABLE REVISION (
+    ENTITY_ID VARCHAR(200) NOT NULL,
+    VERSION BIGINT NOT NULL DEFAULT 0,
+    CLIENT_ID VARCHAR(100),
+    CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)
+);
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V7__AddRevision.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V7__AddRevision.sql
new file mode 100644
index 0000000..eb37fcd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V7__AddRevision.sql
@@ -0,0 +1,21 @@
+-- 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.
+
+CREATE TABLE REVISION (
+    ENTITY_ID VARCHAR(200) NOT NULL,
+    VERSION BIGINT NOT NULL DEFAULT (0),
+    CLIENT_ID VARCHAR(100),
+    CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)
+);
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
index f47e7ce..33b9f40 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
+++ b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
@@ -20,6 +20,7 @@
 import org.apache.nifi.registry.authorization.User
 import org.apache.nifi.registry.authorization.UserGroup
 import org.apache.nifi.registry.bucket.Bucket
+import org.apache.nifi.registry.exception.ResourceNotFoundException
 import org.apache.nifi.registry.security.authorization.*
 import org.apache.nifi.registry.security.authorization.AccessPolicy as AuthAccessPolicy
 import org.apache.nifi.registry.security.authorization.User as AuthUser
@@ -56,12 +57,12 @@
         accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()  // needed for converting user to DTO
 
         when: "new user is created successfully"
-        def user = new User(null, "username")
+        def user = new User("id", "username")
         User createdUser = authorizationService.createUser(user)
 
         then: "created user has been assigned an identifier"
         with(createdUser) {
-            identifier != null
+            identifier == "id"
             identity == "username"
         }
 
@@ -134,7 +135,7 @@
         def user2 = authorizationService.getUser("does-not-exist")
 
         then: "no user is returned"
-        user2 == null
+        thrown(ResourceNotFoundException.class)
 
     }
 
@@ -162,8 +163,9 @@
     def "delete user"() {
 
         setup:
-        userGroupProvider.getUser("userId") >> new AuthUser.Builder().identifier("userId").identity("username").build()
-        userGroupProvider.deleteUser("userId") >> new AuthUser.Builder().identifier("userId").identity("username").build()
+        def user1 = new AuthUser.Builder().identifier("userId").identity("username").build()
+        userGroupProvider.getUser("userId") >> user1
+        userGroupProvider.deleteUser(user1) >> user1
         userGroupProvider.getGroups() >> new HashSet<Group>()
         accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
 
@@ -190,12 +192,12 @@
         accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()  // needed for converting to DTO
 
         when: "new group is created successfully"
-        def group = new UserGroup(null, "groupName")
+        def group = new UserGroup("id", "groupName")
         UserGroup createdGroup = authorizationService.createUserGroup(group)
 
         then: "created group has been assigned an identifier"
         with(createdGroup) {
-            identifier != null
+            identifier == "id"
             identity == "groupName"
         }
 
@@ -255,7 +257,7 @@
         def g2 = authorizationService.getUserGroup("nonExistentId")
 
         then: "no group is returned"
-        g2 == null
+        thrown(ResourceNotFoundException.class)
 
     }
 
@@ -282,8 +284,9 @@
     def "delete user group"() {
 
         setup:
-        userGroupProvider.getGroup("id") >> new Group.Builder().identifier("id").name("name").build()
-        userGroupProvider.deleteGroup("id") >> new Group.Builder().identifier("id").name("name").build()
+        def group1 = new Group.Builder().identifier("id").name("name").build();
+        userGroupProvider.getGroup("id") >> group1
+        userGroupProvider.deleteGroup(group1) >> group1
         accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
 
 
@@ -316,11 +319,14 @@
 
 
         when: "new access policy is created successfully"
-        def createdPolicy = authorizationService.createAccessPolicy(new AccessPolicy([resource: "/resource", action: "read"]))
+        def accessPolicy = new AccessPolicy([resource: "/resource", action: "read"])
+        accessPolicy.setIdentifier("id")
+
+        def createdPolicy = authorizationService.createAccessPolicy(accessPolicy)
 
         then: "created policy has been assigned an identifier"
         with(createdPolicy) {
-            identifier != null
+            identifier == "id"
             resource == "/resource"
             action == "read"
             configurable == true
@@ -378,7 +384,7 @@
         def p2 = authorizationService.getAccessPolicy("nonExistentId")
 
         then: "no policy is returned"
-        p2 == null
+        thrown(ResourceNotFoundException.class)
 
     }
 
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestDatabaseFlowPersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestDatabaseFlowPersistenceProvider.java
index 5314860..5851b21 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestDatabaseFlowPersistenceProvider.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestDatabaseFlowPersistenceProvider.java
@@ -16,20 +16,13 @@
  */
 package org.apache.nifi.registry.provider.flow;
 
-import org.apache.nifi.registry.db.DatabaseTestApplication;
+import org.apache.nifi.registry.db.DatabaseBaseTest;
 import org.apache.nifi.registry.flow.FlowPersistenceProvider;
 import org.apache.nifi.registry.flow.FlowSnapshotContext;
 import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.TestExecutionListeners;
-import org.springframework.test.context.junit4.SpringRunner;
-import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
-import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
-import org.springframework.transaction.annotation.Transactional;
 
 import javax.sql.DataSource;
 import java.nio.charset.StandardCharsets;
@@ -39,11 +32,7 @@
 import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.when;
 
-@Transactional
-@RunWith(SpringRunner.class)
-@SpringBootTest(classes = DatabaseTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
-@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class})
-public class TestDatabaseFlowPersistenceProvider {
+public class TestDatabaseFlowPersistenceProvider extends DatabaseBaseTest {
 
     @Autowired
     private DataSource dataSource;
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
index 4e43645..dd09170 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
@@ -34,8 +34,6 @@
 import org.apache.nifi.registry.serialization.FlowContent;
 import org.apache.nifi.registry.serialization.FlowContentSerializer;
 import org.apache.nifi.registry.service.alias.RegistryUrlAliasService;
-import org.apache.nifi.registry.service.extension.ExtensionService;
-import org.apache.nifi.registry.service.extension.StandardExtensionService;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -78,7 +76,6 @@
     private FlowPersistenceProvider flowPersistenceProvider;
     private BundlePersistenceProvider bundlePersistenceProvider;
     private FlowContentSerializer flowContentSerializer;
-    private ExtensionService extensionService;
     private Validator validator;
     private RegistryUrlAliasService registryUrlAliasService;
 
@@ -90,14 +87,13 @@
         flowPersistenceProvider = mock(FlowPersistenceProvider.class);
         bundlePersistenceProvider = mock(BundlePersistenceProvider.class);
         flowContentSerializer = mock(FlowContentSerializer.class);
-        extensionService = mock(StandardExtensionService.class);
         registryUrlAliasService = mock(RegistryUrlAliasService.class);
 
         final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
         validator = validatorFactory.getValidator();
 
         registryService = new RegistryService(metadataService, flowPersistenceProvider, bundlePersistenceProvider,
-                flowContentSerializer, extensionService, validator, registryUrlAliasService);
+                flowContentSerializer, validator, registryUrlAliasService);
     }
 
     // ---------------------- Test Bucket methods ---------------------------------------------
@@ -105,6 +101,7 @@
     @Test
     public void testCreateBucketValid() {
         final Bucket bucket = new Bucket();
+        bucket.setIdentifier("1");
         bucket.setName("My Bucket");
         bucket.setDescription("This is my bucket.");
 
@@ -117,6 +114,7 @@
         assertNotNull(createdBucket.getIdentifier());
         assertNotNull(createdBucket.getCreatedTimestamp());
 
+        assertEquals(bucket.getIdentifier(), createdBucket.getIdentifier());
         assertEquals(bucket.getName(), createdBucket.getName());
         assertEquals(bucket.getDescription(), createdBucket.getDescription());
     }
@@ -124,6 +122,7 @@
     @Test(expected = IllegalStateException.class)
     public void testCreateBucketWithSameName() {
         final Bucket bucket = new Bucket();
+        bucket.setIdentifier("b2");
         bucket.setName("My Bucket");
         bucket.setDescription("This is my bucket.");
 
@@ -313,6 +312,7 @@
         when(metadataService.getBucketById(any(String.class))).thenReturn(null);
 
         final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier("f1");
         versionedFlow.setName("My Flow");
         versionedFlow.setBucketIdentifier("b1");
 
@@ -341,6 +341,7 @@
         when(metadataService.getFlowsByName(existingBucket.getId(), flowWithSameName.getName())).thenReturn(Collections.singletonList(flowWithSameName));
 
         final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier("flow2");
         versionedFlow.setName(flowWithSameName.getName());
         versionedFlow.setBucketIdentifier("b1");
 
@@ -358,6 +359,7 @@
         when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
 
         final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier("f1");
         versionedFlow.setName("My Flow");
         versionedFlow.setBucketIdentifier("b1");
 
@@ -368,6 +370,7 @@
         assertNotNull(createdFlow.getIdentifier());
         assertTrue(createdFlow.getCreatedTimestamp() > 0);
         assertTrue(createdFlow.getModifiedTimestamp() > 0);
+        assertEquals(versionedFlow.getIdentifier(), createdFlow.getIdentifier());
         assertEquals(versionedFlow.getName(), createdFlow.getName());
         assertEquals(versionedFlow.getBucketIdentifier(), createdFlow.getBucketIdentifier());
         assertEquals(versionedFlow.getDescription(), createdFlow.getDescription());
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties b/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties
index 1e0d7c9..cbeee4f 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties
@@ -23,3 +23,6 @@
 #logging.level.org.springframework.core.io.support: DEBUG
 #logging.level.org.springframework.context.annotation: DEBUG
 #logging.level.org.springframework.web: DEBUG
+
+# Controls logging of SQL queries and parameters
+# logging.level.org.springframework.jdbc: TRACE
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java
index c5d66f5..fec764c 100644
--- a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.revision.entity;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.function.Supplier;
 
@@ -75,4 +76,11 @@
      */
     <T extends RevisableEntity> T delete(String entityIdentifier, RevisionInfo revisionInfo, Supplier<T> deleteEntity);
 
+    /**
+     * Populates RevisionInfo on any objects in the collection that implement RevisableEntity.
+     *
+     * @param entities the entities collection which may contain one or more RevisableEntity instances
+     */
+    void populateRevisions(Collection<?> entities);
+
 }
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java
index cd55ea3..541f31f 100644
--- a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java
@@ -26,6 +26,7 @@
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Supplier;
 
 /**
@@ -72,7 +73,7 @@
     @Override
     public <T extends RevisableEntity> List<T> getEntities(final Supplier<List<T>> getEntities) {
         final List<T> entities = getEntities.get();
-        populateRevisions(entities);
+        populateRevisableEntityRevisions(entities);
         return entities;
     }
 
@@ -108,6 +109,24 @@
         return revisionManager.deleteRevision(claim, () -> deleteEntity.get());
     }
 
+    @Override
+    public void populateRevisions(final Collection<?> entities) {
+        if (entities == null || entities.isEmpty()) {
+            return;
+        }
+
+        // Note: This might be inefficient to retrieve all the revisions when there are lots of revisions
+        // and only a few entities that we might need revisions for, we could consider allowing a set of
+        // entity ids to be passed in, but then we also might end up with a massive OR statement when selecting
+        final Map<String,Revision> revisionMap = revisionManager.getRevisionMap();
+
+        for (final Object obj : entities) {
+            if (obj instanceof RevisableEntity) {
+                populateRevision(revisionMap, (RevisableEntity) obj);
+            }
+        }
+    }
+
     private <T extends RevisableEntity> T createOrUpdate(final T requestEntity, final String userIdentity, final Supplier<T> updateOrCreateEntity) {
         final Revision revision = createRevision(requestEntity.getIdentifier(), requestEntity.getRevision());
         final RevisionClaim claim = new StandardRevisionClaim(revision);
@@ -127,21 +146,35 @@
         return revisionUpdate.getEntity();
     }
 
-    private <T extends RevisableEntity> void populateRevisions(final Collection<T> revisableEntities) {
+    private <T extends RevisableEntity> void populateRevisableEntityRevisions(final Collection<T> revisableEntities) {
         if (revisableEntities == null) {
             return;
         }
 
+        final Map<String,Revision> revisionMap = revisionManager.getRevisionMap();
         revisableEntities.forEach(e -> {
-            populateRevision(e);
+            populateRevision(revisionMap, e);
         });
     }
 
+    private void populateRevision(final Map<String, Revision> revisionMap, final RevisableEntity revisableEntity) {
+        final Revision revision = revisionMap.get(revisableEntity.getIdentifier());
+        if (revision != null) {
+            final RevisionInfo revisionInfo = createRevisionInfo(revision);
+            revisableEntity.setRevision(revisionInfo);
+        } else {
+            // need to make sure that if there isn't an entry in the map, we call getRevision which will cause a
+            // revision to be created in the RevisionManager
+            populateRevision(revisableEntity);
+        }
+    }
+
     private void populateRevision(final RevisableEntity e) {
         if (e == null) {
             return;
         }
 
+        // get or create the revision
         final Revision entityRevision = revisionManager.getRevision(e.getIdentifier());
         final RevisionInfo revisionInfo = createRevisionInfo(entityRevision);
         e.setRevision(revisionInfo);
diff --git a/nifi-registry-core/nifi-registry-revision/pom.xml b/nifi-registry-core/nifi-registry-revision/pom.xml
index 9c80f29..c3ce4d8 100644
--- a/nifi-registry-core/nifi-registry-revision/pom.xml
+++ b/nifi-registry-core/nifi-registry-revision/pom.xml
@@ -27,8 +27,8 @@
         <module>nifi-registry-revision-api</module>
         <module>nifi-registry-revision-common</module>
         <module>nifi-registry-revision-spring-jdbc</module>
-	<module>nifi-registry-revision-entity-model</module>
-	<module>nifi-registry-revision-entity-service</module>
+	    <module>nifi-registry-revision-entity-model</module>
+	    <module>nifi-registry-revision-entity-service</module>
     </modules>
 
 </project>
diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml
index 4dada50..a65a75a 100644
--- a/nifi-registry-core/nifi-registry-web-api/pom.xml
+++ b/nifi-registry-core/nifi-registry-web-api/pom.xml
@@ -370,6 +370,16 @@
         </dependency>
         <dependency>
             <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-revision-spring-jdbc</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-revision-entity-service</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
             <artifactId>nifi-registry-properties</artifactId>
             <version>1.0.0-SNAPSHOT</version>
             <scope>provided</scope> <!-- This will be in the lib directory -->
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
index bfbeabf..3250807 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
@@ -24,30 +24,30 @@
 import io.swagger.annotations.Authorization;
 import io.swagger.annotations.Extension;
 import io.swagger.annotations.ExtensionProperty;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.authorization.AccessPolicy;
 import org.apache.nifi.registry.authorization.AccessPolicySummary;
 import org.apache.nifi.registry.authorization.Resource;
 import org.apache.nifi.registry.event.EventService;
-import org.apache.nifi.registry.exception.ResourceNotFoundException;
-import org.apache.nifi.registry.security.authorization.Authorizer;
-import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.revision.web.ClientIdParameter;
+import org.apache.nifi.registry.revision.web.LongParameter;
 import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -65,19 +65,11 @@
         description = "Endpoint for managing access policies.",
         authorizations = { @Authorization("Authorization") }
 )
-public class AccessPolicyResource extends AuthorizableApplicationResource {
-
-    private static final Logger logger = LoggerFactory.getLogger(AccessPolicyResource.class);
-
-    private Authorizer authorizer;
+public class AccessPolicyResource extends ApplicationResource {
 
     @Autowired
-    public AccessPolicyResource(
-            Authorizer authorizer,
-            AuthorizationService authorizationService,
-            EventService eventService) {
-        super(authorizationService, eventService);
-        this.authorizer = authorizer;
+    public AccessPolicyResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     /**
@@ -109,22 +101,7 @@
             @ApiParam(value = "The access policy configuration details.", required = true)
             final AccessPolicy requestAccessPolicy) {
 
-        verifyAuthorizerSupportsConfigurablePolicies();
-        authorizeAccess(RequestAction.WRITE);
-
-        if (requestAccessPolicy == null) {
-            throw new IllegalArgumentException("Access policy details must be specified when creating a new policy.");
-        }
-        if (requestAccessPolicy.getIdentifier() != null) {
-            throw new IllegalArgumentException("Access policy ID cannot be specified when creating a new policy.");
-        }
-        if (requestAccessPolicy.getResource() == null) {
-            throw new IllegalArgumentException("Resource must be specified when creating a new access policy.");
-        }
-        RequestAction.valueOfValue(requestAccessPolicy.getAction());
-
-        AccessPolicy createdPolicy = authorizationService.createAccessPolicy(requestAccessPolicy);
-
+        AccessPolicy createdPolicy = serviceFacade.createAccessPolicy(requestAccessPolicy);
         String locationUri = generateAccessPolicyUri(createdPolicy);
         return generateCreatedResponse(URI.create(locationUri), createdPolicy).build();
     }
@@ -152,11 +129,7 @@
             @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response getAccessPolicies() {
-
-        verifyAuthorizerIsManaged();
-        authorizeAccess(RequestAction.READ);
-
-        List<AccessPolicy> accessPolicies = authorizationService.getAccessPolicies();
+        List<AccessPolicy> accessPolicies = serviceFacade.getAccessPolicies();
         if (accessPolicies == null) {
             accessPolicies = Collections.emptyList();
         }
@@ -191,17 +164,7 @@
     public Response getAccessPolicy(
             @ApiParam(value = "The access policy id.", required = true)
             @PathParam("id") final String identifier) {
-
-        verifyAuthorizerIsManaged();
-        authorizeAccess(RequestAction.READ);
-
-        final AccessPolicy accessPolicy = authorizationService.getAccessPolicy(identifier);
-        if (accessPolicy == null) {
-            logger.warn("The specified access policy id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
-        }
-
+        final AccessPolicy accessPolicy = serviceFacade.getAccessPolicy(identifier);
         return generateOkResponse(accessPolicy).build();
     }
 
@@ -241,17 +204,11 @@
             @PathParam("resource")
             final String rawResource) {
 
-        verifyAuthorizerIsManaged();
-        authorizeAccess(RequestAction.READ);
-
         // parse the action and resource type
         final RequestAction requestAction = RequestAction.valueOfValue(action);
         final String resource = "/" + rawResource;
 
-        AccessPolicy accessPolicy = authorizationService.getAccessPolicy(resource, requestAction);
-        if (accessPolicy == null) {
-            throw new ResourceNotFoundException("No policy found for action='" + action + "', resource='" + resource + "'");
-        }
+        final AccessPolicy accessPolicy = serviceFacade.getAccessPolicy(resource, requestAction);
         return generateOkResponse(accessPolicy).build();
     }
 
@@ -285,15 +242,12 @@
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
     public Response updateAccessPolicy(
             @Context
-            final HttpServletRequest httpServletRequest,
+                final HttpServletRequest httpServletRequest,
             @ApiParam(value = "The access policy id.", required = true)
             @PathParam("id")
-            final String identifier,
+                final String identifier,
             @ApiParam(value = "The access policy configuration details.", required = true)
-            final AccessPolicy requestAccessPolicy) {
-
-        verifyAuthorizerSupportsConfigurablePolicies();
-        authorizeAccess(RequestAction.WRITE);
+                final AccessPolicy requestAccessPolicy) {
 
         if (requestAccessPolicy == null) {
             throw new IllegalArgumentException("Access policy details must be specified when updating a policy.");
@@ -303,9 +257,7 @@
                     + "policy id of the requested resource (%s).", requestAccessPolicy.getIdentifier(), identifier));
         }
 
-        AccessPolicy createdPolicy = authorizationService.updateAccessPolicy(requestAccessPolicy);
-
-        String locationUri = generateAccessPolicyUri(createdPolicy);
+        final AccessPolicy createdPolicy = serviceFacade.updateAccessPolicy(requestAccessPolicy);
         return generateOkResponse(createdPolicy).build();
     }
 
@@ -336,19 +288,21 @@
             @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
     public Response removeAccessPolicy(
-            @Context final HttpServletRequest httpServletRequest,
+            @Context
+                final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true)
+            @QueryParam(VERSION)
+                final LongParameter version,
+            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.")
+            @QueryParam(CLIENT_ID)
+            @DefaultValue(StringUtils.EMPTY)
+                final ClientIdParameter clientId,
             @ApiParam(value = "The access policy id.", required = true)
             @PathParam("id")
-            final String identifier) {
+                final String identifier) {
 
-        verifyAuthorizerSupportsConfigurablePolicies();
-        authorizeAccess(RequestAction.DELETE);
-        AccessPolicy deletedPolicy = authorizationService.deleteAccessPolicy(identifier);
-        if (deletedPolicy == null) {
-            logger.warn("The specified access policy id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
-        }
+        final RevisionInfo revisionInfo = getRevisionInfo(version, clientId);
+        final AccessPolicy deletedPolicy = serviceFacade.deleteAccessPolicy(identifier, revisionInfo);
         return generateOkResponse(deletedPolicy).build();
     }
 
@@ -376,32 +330,10 @@
             @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
             @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) })
     public Response getResources() {
-        authorizeAccess(RequestAction.READ);
-
-        final List<Resource> resources = authorizationService.getResources();
-
+        final List<Resource> resources = serviceFacade.getResources();
         return generateOkResponse(resources).build();
     }
 
-
-    private void verifyAuthorizerIsManaged() {
-        if (!AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)) {
-            throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER);
-        }
-    }
-
-    private void verifyAuthorizerSupportsConfigurablePolicies() {
-        if (!AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer)) {
-            verifyAuthorizerIsManaged();
-            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_POLICIES);
-        }
-    }
-
-    private void authorizeAccess(RequestAction actionType) {
-        final Authorizable policiesAuthorizable = authorizableLookup.getPoliciesAuthorizable();
-        authorizationService.authorize(policiesAuthorizable, actionType);
-    }
-
     private String generateAccessPolicyUri(final AccessPolicySummary accessPolicy) {
         return generateResourceUri("policies", accessPolicy.getIdentifier());
     }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
index 30374f0..278f635 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
@@ -36,11 +36,11 @@
 import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
 import org.apache.nifi.registry.security.authorization.user.NiFiUser;
 import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
-import org.apache.nifi.registry.service.AuthorizationService;
 import org.apache.nifi.registry.web.exception.UnauthorizedException;
 import org.apache.nifi.registry.web.security.authentication.jwt.JwtService;
 import org.apache.nifi.registry.web.security.authentication.kerberos.KerberosSpnegoIdentityProvider;
 import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -74,7 +74,6 @@
     private static final Logger logger = LoggerFactory.getLogger(AccessResource.class);
 
     private NiFiRegistryProperties properties;
-    private AuthorizationService authorizationService;
     private JwtService jwtService;
     private X509IdentityProvider x509IdentityProvider;
     private KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider;
@@ -83,19 +82,18 @@
     @Autowired
     public AccessResource(
             NiFiRegistryProperties properties,
-            AuthorizationService authorizationService,
             JwtService jwtService,
             X509IdentityProvider x509IdentityProvider,
             @Nullable KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider,
             @Nullable IdentityProvider identityProvider,
+            ServiceFacade serviceFacade,
             EventService eventService) {
-        super(eventService);
+        super(serviceFacade, eventService);
         this.properties = properties;
         this.jwtService = jwtService;
         this.x509IdentityProvider = x509IdentityProvider;
         this.kerberosSpnegoIdentityProvider = kerberosSpnegoIdentityProvider;
         this.identityProvider = identityProvider;
-        this.authorizationService = authorizationService;
     }
 
     /**
@@ -123,7 +121,7 @@
             throw new WebApplicationException(new Throwable("Unable to access details for current user."));
         }
 
-        final CurrentUser currentUser = authorizationService.getCurrentUser();
+        final CurrentUser currentUser = serviceFacade.getCurrentUser();
         currentUser.setLoginSupported(httpServletRequest.isSecure() && identityProvider != null);
 
         return generateOkResponse(currentUser).build();
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
index adabbfc..d33fd8b 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
@@ -20,6 +20,10 @@
 import org.apache.commons.lang3.Validate;
 import org.apache.nifi.registry.event.EventService;
 import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.revision.web.ClientIdParameter;
+import org.apache.nifi.registry.revision.web.LongParameter;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -35,6 +39,9 @@
 
 public class ApplicationResource {
 
+    public static final String CLIENT_ID = "clientId";
+    public static final String VERSION = "version";
+
     public static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme";
     public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost";
     public static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort";
@@ -55,10 +62,14 @@
     @Context
     private UriInfo uriInfo;
 
+    protected final ServiceFacade serviceFacade;
     private final EventService eventService;
 
-    public ApplicationResource(final EventService eventService) {
+    public ApplicationResource(final ServiceFacade serviceFacade,
+                               final EventService eventService) {
+        this.serviceFacade = serviceFacade;
         this.eventService = eventService;
+        Validate.notNull(this.serviceFacade);
         Validate.notNull(this.eventService);
     }
 
@@ -196,4 +207,18 @@
         return null;
     }
 
+    /**
+     * Creates a RevisionInfo from the version and clientId parameters.
+     *
+     * @param version the version
+     * @param clientId the client id
+     * @return the RevisionInfo
+     */
+    protected RevisionInfo getRevisionInfo(final LongParameter version, final ClientIdParameter clientId) {
+        final RevisionInfo revisionInfo = new RevisionInfo();
+        revisionInfo.setVersion(version == null ? null : version.getLong());
+        revisionInfo.setClientId(clientId.getClientId());
+        return revisionInfo;
+    }
+
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
deleted file mode 100644
index 83240c7..0000000
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.nifi.registry.web.api;
-
-import org.apache.nifi.registry.authorization.Resource;
-import org.apache.nifi.registry.bucket.BucketItem;
-import org.apache.nifi.registry.event.EventService;
-import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.security.authorization.resource.ResourceType;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-public class AuthorizableApplicationResource extends ApplicationResource {
-
-    private static final Logger logger = LoggerFactory.getLogger(AuthorizableApplicationResource.class);
-
-    protected final AuthorizationService authorizationService;
-    protected final AuthorizableLookup authorizableLookup;
-
-    protected AuthorizableApplicationResource(
-            AuthorizationService authorizationService,
-            EventService eventService) {
-        super(eventService);
-        this.authorizationService = authorizationService;
-        this.authorizableLookup = authorizationService.getAuthorizableLookup();
-    }
-
-    protected void authorizeBucketAccess(RequestAction actionType, String bucketIdentifier) {
-        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketIdentifier);
-        authorizationService.authorize(bucketAuthorizable, actionType);
-    }
-
-    protected void authorizeBucketItemAccess(RequestAction actionType, BucketItem bucketItem) {
-        authorizeBucketAccess(actionType, bucketItem.getBucketIdentifier());
-    }
-
-    protected Set<String> getAuthorizedBucketIds(RequestAction actionType) {
-        return authorizationService
-                .getAuthorizedResources(actionType, ResourceType.Bucket)
-                .stream()
-                .map(AuthorizableApplicationResource::extractBucketIdFromResource)
-                .filter(Objects::nonNull)
-                .distinct()
-                .collect(Collectors.toSet());
-    }
-
-    private static String extractBucketIdFromResource(Resource resource) {
-
-        if (resource == null || resource.getIdentifier() == null || !resource.getIdentifier().startsWith("/buckets/")) {
-            return null;
-        }
-
-        String[] pathComponents = resource.getIdentifier().split("/");
-        if (pathComponents.length < 3) {
-            return null;
-        }
-        return pathComponents[2];
-    }
-
-}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketBundleResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketBundleResource.java
index abfeddd..60d8df5 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketBundleResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketBundleResource.java
@@ -30,11 +30,7 @@
 import org.apache.nifi.registry.extension.bundle.BundleType;
 import org.apache.nifi.registry.extension.bundle.BundleTypeValues;
 import org.apache.nifi.registry.extension.bundle.BundleVersion;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
-import org.apache.nifi.registry.web.security.PermissionsService;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
 import org.glassfish.jersey.media.multipart.FormDataParam;
 import org.slf4j.Logger;
@@ -61,25 +57,13 @@
         description = "Create extension bundles scoped to an existing bucket in the registry. ",
         authorizations = { @Authorization("Authorization") }
 )
-public class BucketBundleResource extends AuthorizableApplicationResource {
+public class BucketBundleResource extends ApplicationResource {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(BucketBundleResource.class);
 
-    private final RegistryService registryService;
-    private final LinkService linkService;
-    private final PermissionsService permissionsService;
-
     @Autowired
-    public BucketBundleResource(
-            final RegistryService registryService,
-            final LinkService linkService,
-            final PermissionsService permissionsService,
-            final AuthorizationService authorizationService,
-            final EventService eventService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
-        this.permissionsService =permissionsService;
+    public BucketBundleResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     @POST
@@ -124,22 +108,14 @@
             @FormDataParam("sha256")
                 final String clientSha256) throws IOException {
 
-        authorizeBucketAccess(RequestAction.WRITE, bucketId);
-
         LOGGER.debug("Creating extension bundle version for bundle type {}", new Object[]{bundleType});
 
-        final BundleVersion createdBundleVersion = registryService.createBundleVersion(
+        final BundleVersion createdBundleVersion = serviceFacade.createBundleVersion(
                 bucketId, bundleType, fileInputStream, clientSha256);
 
         publish(EventFactory.extensionBundleCreated(createdBundleVersion.getBundle()));
         publish(EventFactory.extensionBundleVersionCreated(createdBundleVersion));
 
-        linkService.populateLinks(createdBundleVersion.getVersionMetadata());
-        linkService.populateLinks(createdBundleVersion.getBundle());
-        linkService.populateLinks(createdBundleVersion.getBucket());
-
-        permissionsService.populateItemPermissions(createdBundleVersion.getBundle());
-
         return Response.status(Response.Status.OK).entity(createdBundleVersion).build();
     }
 
@@ -166,14 +142,9 @@
     public Response getExtensionBundles(
             @PathParam("bucketId")
             @ApiParam(value = "The bucket identifier", required = true)
-                final String bucketId
-    ) {
-        authorizeBucketAccess(RequestAction.READ, bucketId);
+                final String bucketId) {
 
-        final List<Bundle> bundles = registryService.getBundlesByBucket(bucketId);
-        permissionsService.populateItemPermissions(bundles);
-        linkService.populateLinks(bundles);
-
+        final List<Bundle> bundles = serviceFacade.getBundlesByBucket(bucketId);
         return Response.status(Response.Status.OK).entity(bundles).build();
     }
 
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
index ecf1b7a..8dd7290 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
@@ -32,14 +32,11 @@
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
-import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.revision.web.ClientIdParameter;
+import org.apache.nifi.registry.revision.web.LongParameter;
 import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
-import org.apache.nifi.registry.web.security.PermissionsService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -47,12 +44,14 @@
 import javax.ws.rs.BadRequestException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.util.List;
@@ -65,25 +64,11 @@
         description = "Create flows scoped to an existing bucket in the registry.",
         authorizations = { @Authorization("Authorization") }
 )
-public class BucketFlowResource extends AuthorizableApplicationResource {
-
-    private static final Logger logger = LoggerFactory.getLogger(BucketFlowResource.class);
-
-    private final RegistryService registryService;
-    private final LinkService linkService;
-    private final PermissionsService permissionsService;
+public class BucketFlowResource extends ApplicationResource {
 
     @Autowired
-    public BucketFlowResource(
-            final RegistryService registryService,
-            final LinkService linkService,
-            final PermissionsService permissionsService,
-            final AuthorizationService authorizationService,
-            final EventService eventService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
-        this.permissionsService =permissionsService;
+    public BucketFlowResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     @POST
@@ -108,18 +93,14 @@
     public Response createFlow(
             @PathParam("bucketId")
             @ApiParam("The bucket identifier")
-            final String bucketId,
+                final String bucketId,
             @ApiParam(value = "The details of the flow to create.", required = true)
-            final VersionedFlow flow) {
+                final VersionedFlow flow) {
 
-        authorizeBucketAccess(RequestAction.WRITE, bucketId);
         verifyPathParamsMatchBody(bucketId, flow);
 
-        final VersionedFlow createdFlow = registryService.createFlow(bucketId, flow);
+        final VersionedFlow createdFlow = serviceFacade.createFlow(bucketId, flow);
         publish(EventFactory.flowCreated(createdFlow));
-
-        permissionsService.populateItemPermissions(createdFlow);
-        linkService.populateLinks(createdFlow);
         return Response.status(Response.Status.OK).entity(createdFlow).build();
     }
 
@@ -148,12 +129,7 @@
             @ApiParam("The bucket identifier")
             final String bucketId) {
 
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final List<VersionedFlow> flows = registryService.getFlows(bucketId);
-        permissionsService.populateItemPermissions(flows);
-        linkService.populateLinks(flows);
-
+        final List<VersionedFlow> flows = serviceFacade.getFlows(bucketId);
         return Response.status(Response.Status.OK).entity(flows).build();
     }
 
@@ -185,12 +161,7 @@
             @ApiParam("The flow identifier")
                 final String flowId) {
 
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final VersionedFlow flow = registryService.getFlow(bucketId, flowId);
-        permissionsService.populateItemPermissions(flow);
-        linkService.populateLinks(flow);
-
+        final VersionedFlow flow = serviceFacade.getFlow(bucketId, flowId);
         return Response.status(Response.Status.OK).entity(flow).build();
     }
 
@@ -225,16 +196,12 @@
                 final VersionedFlow flow) {
 
         verifyPathParamsMatchBody(bucketId, flowId, flow);
-        authorizeBucketAccess(RequestAction.WRITE, bucketId);
 
         // bucketId and flowId fields are optional in the body parameter, but required before calling the service layer
         setBucketItemMetadataIfMissing(bucketId, flowId, flow);
 
-        final VersionedFlow updatedFlow = registryService.updateFlow(flow);
+        final VersionedFlow updatedFlow = serviceFacade.updateFlow(flow);
         publish(EventFactory.flowUpdated(updatedFlow));
-        permissionsService.populateItemPermissions(updatedFlow);
-        linkService.populateLinks(updatedFlow);
-
         return Response.status(Response.Status.OK).entity(updatedFlow).build();
     }
 
@@ -258,6 +225,13 @@
             @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response deleteFlow(
+            @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true)
+            @QueryParam(VERSION)
+                final LongParameter version,
+            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.")
+            @QueryParam(CLIENT_ID)
+            @DefaultValue(StringUtils.EMPTY)
+                final ClientIdParameter clientId,
             @PathParam("bucketId")
             @ApiParam("The bucket identifier")
                 final String bucketId,
@@ -265,9 +239,10 @@
             @ApiParam("The flow identifier")
                 final String flowId) {
 
-        authorizeBucketAccess(RequestAction.DELETE, bucketId);
-        final VersionedFlow deletedFlow = registryService.deleteFlow(bucketId, flowId);
+        final RevisionInfo revisionInfo = getRevisionInfo(version, clientId);
+        final VersionedFlow deletedFlow = serviceFacade.deleteFlow(bucketId, flowId, revisionInfo);
         publish(EventFactory.flowDeleted(deletedFlow));
+
         return Response.status(Response.Status.OK).entity(deletedFlow).build();
     }
 
@@ -303,7 +278,6 @@
                 final VersionedFlowSnapshot snapshot) {
 
         verifyPathParamsMatchBody(bucketId, flowId, snapshot);
-        authorizeBucketAccess(RequestAction.WRITE, bucketId);
 
         // bucketId and flowId fields are optional in the body parameter, but required before calling the service layer
         setSnaphotMetadataIfMissing(bucketId, flowId, snapshot);
@@ -311,16 +285,9 @@
         final String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
         snapshot.getSnapshotMetadata().setAuthor(userIdentity);
 
-        final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(snapshot);
+        final VersionedFlowSnapshot createdSnapshot = serviceFacade.createFlowSnapshot(snapshot);
         publish(EventFactory.flowVersionCreated(createdSnapshot));
 
-        if (createdSnapshot.getSnapshotMetadata() != null) {
-            linkService.populateLinks(createdSnapshot.getSnapshotMetadata());
-        }
-        if (createdSnapshot.getBucket() != null) {
-            permissionsService.populateBucketPermissions(createdSnapshot.getBucket());
-            linkService.populateLinks(createdSnapshot.getBucket());
-        }
         return Response.status(Response.Status.OK).entity(createdSnapshot).build();
     }
 
@@ -352,13 +319,7 @@
             @ApiParam("The flow identifier")
                 final String flowId) {
 
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketId, flowId);
-        if (snapshots != null ) {
-            linkService.populateLinks(snapshots);
-        }
-
+        final SortedSet<VersionedFlowSnapshotMetadata> snapshots = serviceFacade.getFlowSnapshots(bucketId, flowId);
         return Response.status(Response.Status.OK).entity(snapshots).build();
     }
 
@@ -389,12 +350,7 @@
             @ApiParam("The flow identifier")
                 final String flowId) {
 
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(bucketId, flowId);
-        final VersionedFlowSnapshot lastSnapshot = registryService.getFlowSnapshot(bucketId, flowId, latestMetadata.getVersion());
-        populateLinksAndPermissions(lastSnapshot);
-
+        final VersionedFlowSnapshot lastSnapshot = serviceFacade.getLatestFlowSnapshot(bucketId, flowId);
         return Response.status(Response.Status.OK).entity(lastSnapshot).build();
     }
 
@@ -425,11 +381,7 @@
             @ApiParam("The flow identifier")
             final String flowId) {
 
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final VersionedFlowSnapshotMetadata latest = registryService.getLatestFlowSnapshotMetadata(bucketId, flowId);
-        linkService.populateLinks(latest);
-
+        final VersionedFlowSnapshotMetadata latest = serviceFacade.getLatestFlowSnapshotMetadata(bucketId, flowId);
         return Response.status(Response.Status.OK).entity(latest).build();
     }
 
@@ -463,11 +415,8 @@
             @PathParam("versionNumber")
             @ApiParam("The version number")
                 final Integer versionNumber) {
-        authorizeBucketAccess(RequestAction.READ, bucketId);
 
-        final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketId, flowId, versionNumber);
-        populateLinksAndPermissions(snapshot);
-
+        final VersionedFlowSnapshot snapshot = serviceFacade.getFlowSnapshot(bucketId, flowId, versionNumber);
         return Response.status(Response.Status.OK).entity(snapshot).build();
     }
 
@@ -494,37 +443,21 @@
     public Response getFlowDiff(
             @PathParam("bucketId")
             @ApiParam("The bucket identifier")
-            final String bucketId,
+                final String bucketId,
             @PathParam("flowId")
             @ApiParam("The flow identifier")
-            final String flowId,
+                final String flowId,
             @PathParam("versionA")
             @ApiParam("The first version number")
-            final Integer versionNumberA,
+                final Integer versionNumberA,
             @PathParam("versionB")
             @ApiParam("The second version number")
-            final Integer versionNumberB) {
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-        VersionedFlowDifference result = registryService.getFlowDiff(bucketId, flowId, versionNumberA, versionNumberB);
+                final Integer versionNumberB) {
+
+        final VersionedFlowDifference result = serviceFacade.getFlowDiff(bucketId, flowId, versionNumberA, versionNumberB);
         return Response.status(Response.Status.OK).entity(result).build();
     }
 
-    private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
-        if (snapshot.getSnapshotMetadata() != null) {
-            linkService.populateLinks(snapshot.getSnapshotMetadata());
-        }
-
-        if (snapshot.getFlow() != null) {
-            linkService.populateLinks(snapshot.getFlow());
-        }
-
-        if (snapshot.getBucket() != null) {
-            permissionsService.populateBucketPermissions(snapshot.getBucket());
-            linkService.populateLinks(snapshot.getBucket());
-        }
-
-    }
-
     private static void verifyPathParamsMatchBody(String bucketIdParam, BucketItem bodyBucketItem) throws BadRequestException {
         if (StringUtils.isBlank(bucketIdParam)) {
             throw new BadRequestException("Bucket id path parameter cannot be blank");
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
index 34387e8..1692a29 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
@@ -26,36 +26,29 @@
 import io.swagger.annotations.ExtensionProperty;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.bucket.Bucket;
-import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.event.EventFactory;
 import org.apache.nifi.registry.event.EventService;
 import org.apache.nifi.registry.field.Fields;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
-import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
-import org.apache.nifi.registry.web.security.PermissionsService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.revision.web.ClientIdParameter;
+import org.apache.nifi.registry.revision.web.LongParameter;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import javax.ws.rs.BadRequestException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -67,30 +60,11 @@
                 "Search for and retrieve existing buckets.",
         authorizations = { @Authorization("Authorization") }
 )
-public class BucketResource extends AuthorizableApplicationResource {
-
-    private static final Logger logger = LoggerFactory.getLogger(BucketResource.class);
-
-    @Context
-    UriInfo uriInfo;
-
-    private final LinkService linkService;
-
-    private final RegistryService registryService;
-
-    private final PermissionsService permissionsService;
+public class BucketResource extends ApplicationResource {
 
     @Autowired
-    public BucketResource(
-            final RegistryService registryService,
-            final LinkService linkService,
-            final PermissionsService permissionsService,
-            final AuthorizationService authorizationService,
-            final EventService eventService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
-        this.permissionsService = permissionsService;
+    public BucketResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     @POST
@@ -112,13 +86,9 @@
     public Response createBucket(
             @ApiParam(value = "The bucket to create", required = true)
             final Bucket bucket) {
-        authorizeAccess(RequestAction.WRITE);
 
-        final Bucket createdBucket = registryService.createBucket(bucket);
+        final Bucket createdBucket = serviceFacade.createBucket(bucket);
         publish(EventFactory.bucketCreated(createdBucket));
-
-        permissionsService.populateBucketPermissions(createdBucket);
-        linkService.populateLinks(createdBucket);
         return Response.status(Response.Status.OK).entity(createdBucket).build();
     }
 
@@ -134,7 +104,7 @@
     )
     @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
     public Response getBuckets() {
-
+        // ServiceFacade will determine which buckets the user is authorized for
         // Note: We don't explicitly check for access to (READ, /buckets) because
         // a user might have access to individual buckets without top-level access.
         // For example, a user that has (READ, /buckets/bucket-id-1) but not access
@@ -142,18 +112,7 @@
         // This has the side effect that a user with no access to any buckets
         // gets an empty array returned from this endpoint instead of 403 as one
         // might expect.
-
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<BucketItem>()).build();
-        }
-
-        final List<Bucket> buckets = registryService.getBuckets(authorizedBucketIds);
-        permissionsService.populateBucketPermissions(buckets);
-        linkService.populateLinks(buckets);
-
+        final List<Bucket> buckets = serviceFacade.getBuckets();
         return Response.status(Response.Status.OK).entity(buckets).build();
     }
 
@@ -180,11 +139,7 @@
             @ApiParam("The bucket identifier")
             final String bucketId) {
 
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-        final Bucket bucket = registryService.getBucket(bucketId);
-        permissionsService.populateBucketPermissions(bucket);
-        linkService.populateLinks(bucket);
-
+        final Bucket bucket = serviceFacade.getBucket(bucketId);
         return Response.status(Response.Status.OK).entity(bucket).build();
     }
 
@@ -211,9 +166,9 @@
     public Response updateBucket(
             @PathParam("bucketId")
             @ApiParam("The bucket identifier")
-            final String bucketId,
+                final String bucketId,
             @ApiParam(value = "The updated bucket", required = true)
-            final Bucket bucket) {
+                final Bucket bucket) {
 
         if (StringUtils.isBlank(bucketId)) {
             throw new BadRequestException("Bucket id cannot be blank");
@@ -229,13 +184,8 @@
             bucket.setIdentifier(bucketId);
         }
 
-        authorizeBucketAccess(RequestAction.WRITE, bucketId);
-
-        final Bucket updatedBucket = registryService.updateBucket(bucket);
+        final Bucket updatedBucket = serviceFacade.updateBucket(bucket);
         publish(EventFactory.bucketUpdated(updatedBucket));
-
-        permissionsService.populateBucketPermissions(updatedBucket);
-        linkService.populateLinks(updatedBucket);
         return Response.status(Response.Status.OK).entity(updatedBucket).build();
     }
 
@@ -259,16 +209,23 @@
             @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
             @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) })
     public Response deleteBucket(
+            @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true)
+            @QueryParam(VERSION)
+                final LongParameter version,
+            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.")
+            @QueryParam(CLIENT_ID)
+            @DefaultValue(StringUtils.EMPTY)
+                final ClientIdParameter clientId,
             @PathParam("bucketId")
             @ApiParam("The bucket identifier")
-            final String bucketId) {
+                final String bucketId) {
 
         if (StringUtils.isBlank(bucketId)) {
             throw new BadRequestException("Bucket id cannot be blank");
         }
-        authorizeBucketAccess(RequestAction.DELETE, bucketId);
 
-        final Bucket deletedBucket = registryService.deleteBucket(bucketId);
+        final RevisionInfo revisionInfo = getRevisionInfo(version, clientId);
+        final Bucket deletedBucket = serviceFacade.deleteBucket(bucketId, revisionInfo);
         publish(EventFactory.bucketDeleted(deletedBucket));
 
         return Response.status(Response.Status.OK).entity(deletedBucket).build();
@@ -284,14 +241,9 @@
             response = Fields.class
     )
     public Response getAvailableBucketFields() {
-        final Set<String> bucketFields = registryService.getBucketFields();
+        final Set<String> bucketFields = serviceFacade.getBucketFields();
         final Fields fields = new Fields(bucketFields);
         return Response.status(Response.Status.OK).entity(fields).build();
     }
 
-    private void authorizeAccess(RequestAction actionType) throws AccessDeniedException {
-        final Authorizable bucketsAuthorizable = authorizableLookup.getBucketsAuthorizable();
-        authorizationService.authorize(bucketsAuthorizable, actionType);
-    }
-
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BundleResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BundleResource.java
index 48532fb..1edc0a7 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BundleResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BundleResource.java
@@ -24,7 +24,6 @@
 import io.swagger.annotations.Authorization;
 import io.swagger.annotations.Extension;
 import io.swagger.annotations.ExtensionProperty;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.event.EventFactory;
 import org.apache.nifi.registry.event.EventService;
 import org.apache.nifi.registry.extension.bundle.Bundle;
@@ -33,11 +32,8 @@
 import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams;
 import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata;
 import org.apache.nifi.registry.extension.component.ExtensionMetadata;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
-import org.apache.nifi.registry.web.security.PermissionsService;
+import org.apache.nifi.registry.web.service.ServiceFacade;
+import org.apache.nifi.registry.web.service.StreamingContent;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -51,10 +47,7 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.StreamingOutput;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 import java.util.SortedSet;
 
 @Component
@@ -64,23 +57,13 @@
         description = "Gets metadata about extension bundles and their versions. ",
         authorizations = { @Authorization("Authorization") }
 )
-public class BundleResource extends AuthorizableApplicationResource {
+public class BundleResource extends ApplicationResource {
 
     public static final String CONTENT_DISPOSITION_HEADER = "content-disposition";
-    private final RegistryService registryService;
-    private final LinkService linkService;
-    private final PermissionsService permissionsService;
 
     @Autowired
-    public BundleResource(final RegistryService registryService,
-                          final LinkService linkService,
-                          final PermissionsService permissionsService,
-                          final AuthorizationService authorizationService,
-                          final EventService eventService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
-        this.permissionsService = permissionsService;
+    public BundleResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     // ---------- Extension Bundles ----------
@@ -111,21 +94,10 @@
                     "such as 'nifi-%' to select all bundles where the artifactId starts with 'nifi-'.")
                 final String artifactId) {
 
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
-        }
-
         final BundleFilterParams filterParams = BundleFilterParams.of(bucketName, groupId, artifactId);
 
-        List<Bundle> bundles = registryService.getBundles(authorizedBucketIds, filterParams);
-        if (bundles == null) {
-            bundles = Collections.emptyList();
-        }
-        permissionsService.populateItemPermissions(bundles);
-        linkService.populateLinks(bundles);
-
+        // Service facade will return only bundles from authorized buckets
+        final List<Bundle> bundles = serviceFacade.getBundles(filterParams);
         return Response.status(Response.Status.OK).entity(bundles).build();
     }
 
@@ -155,10 +127,7 @@
             @ApiParam("The extension bundle identifier")
                 final String bundleId) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        permissionsService.populateItemPermissions(bundle);
-        linkService.populateLinks(bundle);
-
+        final Bundle bundle = serviceFacade.getBundle(bundleId);
         return Response.status(Response.Status.OK).entity(bundle).build();
     }
 
@@ -188,14 +157,8 @@
             @ApiParam("The extension bundle identifier")
                 final String bundleId) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-
-        final Bundle deletedBundle = registryService.deleteBundle(bundle);
+        final Bundle deletedBundle = serviceFacade.deleteBundle(bundleId);
         publish(EventFactory.extensionBundleDeleted(deletedBundle));
-
-        permissionsService.populateItemPermissions(deletedBundle);
-        linkService.populateLinks(deletedBundle);
-
         return Response.status(Response.Status.OK).entity(deletedBundle).build();
     }
 
@@ -228,16 +191,8 @@
                 final String version
             ) {
 
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
-        }
-
         final BundleVersionFilterParams filterParams = BundleVersionFilterParams.of(groupId, artifactId, version);
-        final SortedSet<BundleVersionMetadata> bundleVersions = registryService.getBundleVersions(authorizedBucketIds, filterParams);
-        linkService.populateLinks(bundleVersions);
-
+        final SortedSet<BundleVersionMetadata> bundleVersions = serviceFacade.getBundleVersions(filterParams);
         return Response.status(Response.Status.OK).entity(bundleVersions).build();
     }
 
@@ -268,10 +223,7 @@
             @ApiParam("The extension bundle identifier")
                 final String bundleId) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final SortedSet<BundleVersionMetadata> bundleVersions = registryService.getBundleVersions(bundle.getIdentifier());
-        linkService.populateLinks(bundleVersions);
-
+        final SortedSet<BundleVersionMetadata> bundleVersions = serviceFacade.getBundleVersions(bundleId);
         return Response.status(Response.Status.OK).entity(bundleVersions).build();
     }
 
@@ -304,10 +256,7 @@
             @ApiParam("The version of the bundle")
                 final String version) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bundle.getBucketIdentifier(), bundleId, version);
-        linkService.populateLinks(bundleVersion);
-
+        final BundleVersion bundleVersion = serviceFacade.getBundleVersion(bundleId, version);
         return Response.ok(bundleVersion).build();
     }
 
@@ -340,13 +289,13 @@
             @ApiParam("The version of the bundle")
                 final String version) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bundle.getBucketIdentifier(), bundleId, version);
+        final StreamingContent streamingContent = serviceFacade.getBundleVersionContent(bundleId, version);
 
-        final StreamingOutput streamingOutput = (output) -> registryService.writeBundleVersionContent(bundleVersion, output);
+        final String filename = streamingContent.getFilename();
+        final StreamingOutput output = streamingContent.getOutput();
 
-        return Response.ok(streamingOutput)
-                .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + bundleVersion.getFilename())
+        return Response.ok(output)
+                .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + filename)
                 .build();
     }
 
@@ -379,13 +328,8 @@
             @ApiParam("The version of the bundle")
                 final String version) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bundle.getBucketIdentifier(), bundleId, version);
-
-        final BundleVersion deletedBundleVersion = registryService.deleteBundleVersion(bundleVersion);
+        final BundleVersion deletedBundleVersion = serviceFacade.deleteBundleVersion(bundleId, version);
         publish(EventFactory.extensionBundleVersionDeleted(deletedBundleVersion));
-        linkService.populateLinks(deletedBundleVersion);
-
         return Response.status(Response.Status.OK).entity(deletedBundleVersion).build();
     }
 
@@ -419,11 +363,7 @@
             @ApiParam("The version of the bundle")
                 final String version) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bundle.getBucketIdentifier(), bundleId, version);
-
-        final SortedSet<ExtensionMetadata> extensions = registryService.getExtensionMetadata(bundleVersion);
-        linkService.populateLinks(extensions);
+        final SortedSet<ExtensionMetadata> extensions = serviceFacade.getExtensionMetadata(bundleId, version);
         return Response.ok(extensions).build();
     }
 
@@ -461,10 +401,8 @@
                 final String name
             ) {
 
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bundle.getBucketIdentifier(), bundleId, version);
-
-        final org.apache.nifi.registry.extension.component.manifest.Extension extension = registryService.getExtension(bundleVersion, name);
+        final org.apache.nifi.registry.extension.component.manifest.Extension extension =
+                serviceFacade.getExtension(bundleId, version, name);
         return Response.ok(extension).build();
     }
 
@@ -499,10 +437,7 @@
             @ApiParam("The fully qualified name of the extension")
                 final String name
     ) {
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bundle.getBucketIdentifier(), bundleId, version);
-
-        final StreamingOutput streamingOutput = (output) -> registryService.writeExtensionDocs(bundleVersion, name, output);
+        final StreamingOutput streamingOutput = serviceFacade.getExtensionDocs(bundleId, version, name);
         return Response.ok(streamingOutput).build();
     }
 
@@ -537,29 +472,8 @@
             @ApiParam("The fully qualified name of the extension")
                 final String name
     ) {
-        final Bundle bundle = getBundleWithBucketReadAuthorization(bundleId);
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bundle.getBucketIdentifier(), bundleId, version);
-
-        final StreamingOutput streamingOutput = (output) -> registryService.writeAdditionalDetailsDocs(bundleVersion, name, output);
+        final StreamingOutput streamingOutput = serviceFacade.getAdditionalDetailsDocs(bundleId, version, name);
         return Response.ok(streamingOutput).build();
     }
 
-    /**
-     * Retrieves the extension bundle with the given id and ensures the current user has authorization to read the bucket it belongs to.
-     *
-     * @param bundleId the bundle id
-     * @return the extension bundle
-     */
-    private Bundle getBundleWithBucketReadAuthorization(final String bundleId) {
-        final Bundle bundle = registryService.getBundle(bundleId);
-
-        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow isn't returned
-        if (StringUtils.isBlank(bundle.getBucketIdentifier())) {
-            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
-        }
-
-        authorizeBucketAccess(RequestAction.READ, bundle.getBucketIdentifier());
-        return bundle;
-    }
-
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java
index 48f3518..37482c6 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java
@@ -25,12 +25,7 @@
 import io.swagger.annotations.ExtensionProperty;
 import org.apache.nifi.registry.RegistryConfiguration;
 import org.apache.nifi.registry.event.EventService;
-import org.apache.nifi.registry.security.authorization.Authorizer;
-import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
-import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -48,13 +43,13 @@
         description = "Retrieves the configuration for this NiFi Registry.",
         authorizations = { @Authorization("Authorization") }
 )
-public class ConfigResource extends AuthorizableApplicationResource {
+public class ConfigResource extends ApplicationResource {
 
     @Autowired
     public ConfigResource(
-            final AuthorizationService authorizationService,
+            final ServiceFacade serviceFacade,
             final EventService eventService) {
-        super(authorizationService, eventService);
+        super(serviceFacade, eventService);
     }
 
     @GET
@@ -74,36 +69,7 @@
             @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
             @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
     public Response getConfiguration() {
-
-        final RegistryConfiguration config = new RegistryConfiguration();
-
-        boolean hasAnyConfigurationAccess = false;
-        AccessDeniedException lastAccessDeniedException = null;
-        final Authorizer authorizer = authorizationService.getAuthorizer();
-        try {
-            final Authorizable policyAuthorizer = authorizableLookup.getPoliciesAuthorizable();
-            authorizationService.authorize(policyAuthorizer, RequestAction.READ);
-            config.setSupportsManagedAuthorizer(AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer));
-            config.setSupportsConfigurableAuthorizer(AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer));
-            hasAnyConfigurationAccess = true;
-        } catch (AccessDeniedException e) {
-            lastAccessDeniedException = e;
-        }
-
-        try {
-            authorizationService.authorize(authorizableLookup.getTenantsAuthorizable(), RequestAction.READ);
-            config.setSupportsConfigurableUsersAndGroups(AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer));
-            hasAnyConfigurationAccess = true;
-        } catch (AccessDeniedException e) {
-            lastAccessDeniedException = e;
-        }
-
-        if (!hasAnyConfigurationAccess) {
-            // If the user doesn't have access to any configuration, then throw the exception.
-            // Otherwise, return what they can access.
-            throw lastAccessDeniedException;
-        }
-
+        final RegistryConfiguration config = serviceFacade.getRegistryConfiguration();
         return Response.status(Response.Status.OK).entity(config).build();
     }
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepoResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepoResource.java
index 1297dad..140794b 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepoResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepoResource.java
@@ -25,10 +25,8 @@
 import io.swagger.annotations.Extension;
 import io.swagger.annotations.ExtensionProperty;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.event.EventService;
 import org.apache.nifi.registry.exception.ResourceNotFoundException;
-import org.apache.nifi.registry.extension.bundle.BundleVersion;
 import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams;
 import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata;
 import org.apache.nifi.registry.extension.component.ExtensionMetadata;
@@ -38,10 +36,8 @@
 import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.service.ServiceFacade;
+import org.apache.nifi.registry.web.service.StreamingContent;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -50,13 +46,10 @@
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
-import javax.ws.rs.core.Link;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.StreamingOutput;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 import java.util.SortedSet;
 
 @Component
@@ -66,21 +59,13 @@
         description = "Interact with extension bundles via the hierarchy of bucket/group/artifact/version. ",
         authorizations = { @Authorization("Authorization") }
 )
-public class ExtensionRepoResource extends AuthorizableApplicationResource {
+public class ExtensionRepoResource extends ApplicationResource {
 
     public static final String CONTENT_DISPOSITION_HEADER = "content-disposition";
-    private final RegistryService registryService;
-    private final LinkService linkService;
 
     @Autowired
-    public ExtensionRepoResource(
-            final RegistryService registryService,
-            final LinkService linkService,
-            final AuthorizationService authorizationService,
-            final EventService eventService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
+    public ExtensionRepoResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     @GET
@@ -99,15 +84,7 @@
             @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response getExtensionRepoBuckets() {
-
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
-        }
-
-        final SortedSet<ExtensionRepoBucket> repoBuckets = registryService.getExtensionRepoBuckets(authorizedBucketIds);
-        linkService.populateFullLinks(repoBuckets, getBaseUri());
+        final SortedSet<ExtensionRepoBucket> repoBuckets = serviceFacade.getExtensionRepoBuckets(getBaseUri());
         return Response.status(Response.Status.OK).entity(repoBuckets).build();
     }
 
@@ -137,11 +114,7 @@
             @ApiParam("The bucket name")
                 final String bucketName
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final SortedSet<ExtensionRepoGroup> repoGroups = registryService.getExtensionRepoGroups(bucket);
-        linkService.populateFullLinks(repoGroups, getBaseUri());
+        final SortedSet<ExtensionRepoGroup> repoGroups = serviceFacade.getExtensionRepoGroups(getBaseUri(), bucketName);
         return Response.status(Response.Status.OK).entity(repoGroups).build();
     }
 
@@ -174,11 +147,7 @@
             @ApiParam("The group id")
                 final String groupId
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final SortedSet<ExtensionRepoArtifact> repoArtifacts = registryService.getExtensionRepoArtifacts(bucket, groupId);
-        linkService.populateFullLinks(repoArtifacts, getBaseUri());
+        final SortedSet<ExtensionRepoArtifact> repoArtifacts = serviceFacade.getExtensionRepoArtifacts(getBaseUri(), bucketName, groupId);
         return Response.status(Response.Status.OK).entity(repoArtifacts).build();
     }
 
@@ -214,11 +183,8 @@
             @ApiParam("The artifact identifier")
                 final String artifactId
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final SortedSet<ExtensionRepoVersionSummary> repoVersions = registryService.getExtensionRepoVersions(bucket, groupId, artifactId);
-        linkService.populateFullLinks(repoVersions, getBaseUri());
+        final SortedSet<ExtensionRepoVersionSummary> repoVersions = serviceFacade.getExtensionRepoVersions(
+                getBaseUri(), bucketName, groupId, artifactId);
         return Response.status(Response.Status.OK).entity(repoVersions).build();
     }
 
@@ -256,41 +222,8 @@
             @ApiParam("The version")
                 final String version
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
-
-        final String extensionsUri = generateResourceUri(
-                "extension-repository",
-                bundleVersion.getBucket().getName(),
-                bundleVersion.getBundle().getGroupId(),
-                bundleVersion.getBundle().getArtifactId(),
-                bundleVersion.getVersionMetadata().getVersion(),
-                "extensions");
-
-        final String downloadUri = generateResourceUri(
-                "extension-repository",
-                bundleVersion.getBucket().getName(),
-                bundleVersion.getBundle().getGroupId(),
-                bundleVersion.getBundle().getArtifactId(),
-                bundleVersion.getVersionMetadata().getVersion(),
-                "content");
-
-        final String sha256Uri = generateResourceUri(
-                "extension-repository",
-                bundleVersion.getBucket().getName(),
-                bundleVersion.getBundle().getGroupId(),
-                bundleVersion.getBundle().getArtifactId(),
-                bundleVersion.getVersionMetadata().getVersion(),
-                "sha256");
-
-        final ExtensionRepoVersion repoVersion = new ExtensionRepoVersion();
-        repoVersion.setExtensionsLink(Link.fromUri(extensionsUri).rel("extensions").build());
-        repoVersion.setDownloadLink(Link.fromUri(downloadUri).rel("content").build());
-        repoVersion.setSha256Link(Link.fromUri(sha256Uri).rel("sha256").build());
-        repoVersion.setSha256Supplied(bundleVersion.getVersionMetadata().getSha256Supplied());
-
+        final ExtensionRepoVersion repoVersion = serviceFacade.getExtensionRepoVersion(
+                getBaseUri(), bucketName, groupId, artifactId, version);
         return Response.ok(repoVersion).build();
     }
 
@@ -329,15 +262,10 @@
             @ApiParam("The version")
                 final String version
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
 
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
-        final SortedSet<ExtensionMetadata> extensions = registryService.getExtensionMetadata(bundleVersion);
-
-        final List<ExtensionRepoExtensionMetadata> extensionRepoExtensions = new ArrayList<>(extensions.size());
-        extensions.forEach(e -> extensionRepoExtensions.add(new ExtensionRepoExtensionMetadata(e)));
-        linkService.populateFullLinks(extensionRepoExtensions, getBaseUri());
+        final List<ExtensionRepoExtensionMetadata> extensionRepoExtensions =
+                serviceFacade.getExtensionRepoExtensions(
+                        getBaseUri(), bucketName, groupId, artifactId, version);
 
         return Response.ok(extensionRepoExtensions).build();
     }
@@ -380,11 +308,9 @@
             @ApiParam("The fully qualified name of the extension")
                 final String name
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
-        final org.apache.nifi.registry.extension.component.manifest.Extension extension = registryService.getExtension(bundleVersion, name);
+        final org.apache.nifi.registry.extension.component.manifest.Extension extension =
+                serviceFacade.getExtensionRepoExtension(
+                        getBaseUri(), bucketName, groupId, artifactId, version, name);
         return Response.ok(extension).build();
     }
 
@@ -426,11 +352,8 @@
             @ApiParam("The fully qualified name of the extension")
                 final String name
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
-        final StreamingOutput streamingOutput = (output) -> registryService.writeExtensionDocs(bundleVersion, name, output);
+        final StreamingOutput streamingOutput = serviceFacade.getExtensionRepoExtensionDocs(
+                getBaseUri(), bucketName, groupId, artifactId, version, name);
         return Response.ok(streamingOutput).build();
     }
 
@@ -472,11 +395,8 @@
             @ApiParam("The fully qualified name of the extension")
                 final String name
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
-        final StreamingOutput streamingOutput = (output) -> registryService.writeAdditionalDetailsDocs(bundleVersion, name, output);
+        final StreamingOutput streamingOutput = serviceFacade.getExtensionRepoExtensionAdditionalDocs(
+                getBaseUri(), bucketName, groupId, artifactId, version, name);
         return Response.ok(streamingOutput).build();
     }
 
@@ -514,14 +434,14 @@
             @ApiParam("The version")
                 final String version
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+        final StreamingContent streamingContent = serviceFacade.getExtensionRepoVersionContent(
+            bucketName, groupId, artifactId, version);
 
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
-        final StreamingOutput streamingOutput = (output) -> registryService.writeBundleVersionContent(bundleVersion, output);
+        final String filename = streamingContent.getFilename();
+        final StreamingOutput streamingOutput = streamingContent.getOutput();
 
         return Response.ok(streamingOutput)
-                .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + bundleVersion.getFilename())
+                .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + filename)
                 .build();
     }
 
@@ -560,11 +480,7 @@
             @ApiParam("The version")
                 final String version
     ) {
-        final Bucket bucket = registryService.getBucketByName(bucketName);
-        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
-
-        final BundleVersion bundleVersion = registryService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
-        final String sha256Hex = bundleVersion.getVersionMetadata().getSha256();
+        final String sha256Hex = serviceFacade.getExtensionRepoVersionSha256(bucketName, groupId, artifactId, version);
         return Response.ok(sha256Hex, MediaType.TEXT_PLAIN).build();
     }
 
@@ -596,12 +512,6 @@
             @ApiParam("The version")
                 final String version
     ) {
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
-        }
-
         // Since we are using the filter params which are optional in the service layer, we need to validate these path params here
 
         if (StringUtils.isBlank(groupId)) {
@@ -618,7 +528,7 @@
 
         final BundleVersionFilterParams filterParams = BundleVersionFilterParams.of(groupId, artifactId, version);
 
-        final SortedSet<BundleVersionMetadata> bundleVersions = registryService.getBundleVersions(authorizedBucketIds, filterParams);
+        final SortedSet<BundleVersionMetadata> bundleVersions = serviceFacade.getBundleVersions(filterParams);
         if (bundleVersions.isEmpty()) {
             throw new ResourceNotFoundException("An extension bundle version does not exist with the specific group, artifact, and version");
         } else {
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
index 8f95453..52dbe71 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
@@ -31,10 +31,7 @@
 import org.apache.nifi.registry.extension.component.TagCount;
 import org.apache.nifi.registry.extension.component.manifest.ExtensionType;
 import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.stereotype.Component;
 
 import javax.ws.rs.Consumes;
@@ -44,7 +41,6 @@
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Set;
 import java.util.SortedSet;
@@ -56,18 +52,10 @@
         description = "Find and retrieve extensions. ",
         authorizations = { @Authorization("Authorization") }
 )
-public class ExtensionResource extends AuthorizableApplicationResource {
+public class ExtensionResource extends ApplicationResource {
 
-    private final RegistryService registryService;
-    private final LinkService linkService;
-
-    public ExtensionResource(final AuthorizationService authorizationService,
-                             final EventService eventService,
-                             final RegistryService registryService,
-                             final LinkService linkService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
+    public ExtensionResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     @GET
@@ -98,20 +86,13 @@
                 final Set<String> tags
             ) {
 
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
-        }
-
         final ExtensionFilterParams filterParams = new ExtensionFilterParams.Builder()
                 .bundleType(bundleType)
                 .extensionType(extensionType)
                 .addTags(tags == null ? Collections.emptyList() : tags)
                 .build();
 
-        final SortedSet<ExtensionMetadata> extensionMetadata = registryService.getExtensionMetadata(authorizedBucketIds, filterParams);
-        linkService.populateLinks(extensionMetadata);
+        final SortedSet<ExtensionMetadata> extensionMetadata = serviceFacade.getExtensionMetadata(filterParams);
 
         final ExtensionMetadataContainer container = new ExtensionMetadataContainer();
         container.setExtensions(extensionMetadata);
@@ -152,21 +133,13 @@
             @ApiParam(value = "The version of the bundle containing the service API class", required = true)
                 final String version
     ) {
-
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
-        }
-
         final ProvidedServiceAPI serviceAPI = new ProvidedServiceAPI();
         serviceAPI.setClassName(className);
         serviceAPI.setGroupId(groupId);
         serviceAPI.setArtifactId(artifactId);
         serviceAPI.setVersion(version);
 
-        final SortedSet<ExtensionMetadata> extensionMetadata = registryService.getExtensionMetadata(authorizedBucketIds, serviceAPI);
-        linkService.populateLinks(extensionMetadata);
+        final SortedSet<ExtensionMetadata> extensionMetadata = serviceFacade.getExtensionMetadata(serviceAPI);
 
         final ExtensionMetadataContainer container = new ExtensionMetadataContainer();
         container.setExtensions(extensionMetadata);
@@ -193,7 +166,7 @@
             @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response getTags() {
-        final SortedSet<TagCount> tags = registryService.getExtensionTags();
+        final SortedSet<TagCount> tags = serviceFacade.getExtensionTags();
         return Response.status(Response.Status.OK).entity(tags).build();
     }
 
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
index ec42130..c805988 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
@@ -24,18 +24,12 @@
 import io.swagger.annotations.Authorization;
 import io.swagger.annotations.Extension;
 import io.swagger.annotations.ExtensionProperty;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.event.EventService;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
-import org.apache.nifi.registry.web.security.PermissionsService;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -56,22 +50,11 @@
         description = "Gets metadata about flows.",
         authorizations = { @Authorization("Authorization") }
 )
-public class FlowResource extends AuthorizableApplicationResource {
-
-    private final RegistryService registryService;
-    private final LinkService linkService;
-    private final PermissionsService permissionsService;
+public class FlowResource extends ApplicationResource {
 
     @Autowired
-    public FlowResource(final RegistryService registryService,
-                        final LinkService linkService,
-                        final PermissionsService permissionsService,
-                        final AuthorizationService authorizationService,
-                        final EventService eventService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
-        this.permissionsService = permissionsService;
+    public FlowResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
     @GET
@@ -84,7 +67,7 @@
             response = Fields.class
     )
     public Response getAvailableFlowFields() {
-        final Set<String> flowFields = registryService.getFlowFields();
+        final Set<String> flowFields = serviceFacade.getFlowFields();
         final Fields fields = new Fields(flowFields);
         return Response.status(Response.Status.OK).entity(fields).build();
     }
@@ -113,20 +96,9 @@
     public Response getFlow(
             @PathParam("flowId")
             @ApiParam("The flow identifier")
-            final String flowId) {
+                final String flowId) {
 
-        final VersionedFlow flow = registryService.getFlow(flowId);
-
-        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow isn't returned
-        if (StringUtils.isBlank(flow.getBucketIdentifier())) {
-            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
-        }
-
-        authorizeBucketAccess(RequestAction.READ, flow.getBucketIdentifier());
-
-        permissionsService.populateItemPermissions(flow);
-        linkService.populateLinks(flow);
-
+        final VersionedFlow flow = serviceFacade.getFlow(flowId);
         return Response.status(Response.Status.OK).entity(flow).build();
     }
 
@@ -154,22 +126,9 @@
     public Response getFlowVersions(
             @PathParam("flowId")
             @ApiParam("The flow identifier")
-            final String flowId) {
+                final String flowId) {
 
-        final VersionedFlow flow = registryService.getFlow(flowId);
-
-        final String bucketId = flow.getBucketIdentifier();
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
-        }
-
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketId, flowId);
-        if (snapshots != null ) {
-            linkService.populateLinks(snapshots);
-        }
-
+        final SortedSet<VersionedFlowSnapshotMetadata> snapshots = serviceFacade.getFlowSnapshots(flowId);
         return Response.status(Response.Status.OK).entity(snapshots).build();
     }
 
@@ -197,22 +156,12 @@
     public Response getFlowVersion(
             @PathParam("flowId")
             @ApiParam("The flow identifier")
-            final String flowId,
+                final String flowId,
             @PathParam("versionNumber")
             @ApiParam("The version number")
-            final Integer versionNumber) {
+                final Integer versionNumber) {
 
-        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
-
-        final String bucketId = latestMetadata.getBucketIdentifier();
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
-        }
-
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketId, flowId, versionNumber);
-        populateLinksAndPermissions(snapshot);
+        final VersionedFlowSnapshot snapshot = serviceFacade.getFlowSnapshot(flowId, versionNumber);
         return Response.status(Response.Status.OK).entity(snapshot).build();
     }
 
@@ -239,20 +188,9 @@
     public Response getLatestFlowVersion(
             @PathParam("flowId")
             @ApiParam("The flow identifier")
-            final String flowId) {
+                final String flowId) {
 
-        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
-
-        final String bucketId = latestMetadata.getBucketIdentifier();
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
-        }
-
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final VersionedFlowSnapshot lastSnapshot = registryService.getFlowSnapshot(bucketId, flowId, latestMetadata.getVersion());
-        populateLinksAndPermissions(lastSnapshot);
-
+        final VersionedFlowSnapshot lastSnapshot = serviceFacade.getLatestFlowSnapshot(flowId);
         return Response.status(Response.Status.OK).entity(lastSnapshot).build();
     }
 
@@ -279,43 +217,10 @@
     public Response getLatestFlowVersionMetadata(
             @PathParam("flowId")
             @ApiParam("The flow identifier")
-            final String flowId) {
+                final String flowId) {
 
-        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
-
-        final String bucketId = latestMetadata.getBucketIdentifier();
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
-        }
-
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        linkService.populateLinks(latestMetadata);
+        final VersionedFlowSnapshotMetadata latestMetadata = serviceFacade.getLatestFlowSnapshotMetadata(flowId);
         return Response.status(Response.Status.OK).entity(latestMetadata).build();
     }
 
-    // override the base implementation so we can provide a different error message that doesn't include the bucket id
-    protected void authorizeBucketAccess(RequestAction action, String bucketId) {
-        try {
-            super.authorizeBucketAccess(RequestAction.READ, bucketId);
-        } catch (AccessDeniedException e) {
-            throw new AccessDeniedException("User not authorized to view the specified flow.", e);
-        }
-    }
-
-    private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
-        if (snapshot.getSnapshotMetadata() != null) {
-            linkService.populateLinks(snapshot.getSnapshotMetadata());
-        }
-
-        if (snapshot.getFlow() != null) {
-            linkService.populateLinks(snapshot.getFlow());
-        }
-
-        if (snapshot.getBucket() != null) {
-            permissionsService.populateBucketPermissions(snapshot.getBucket());
-            linkService.populateLinks(snapshot.getBucket());
-        }
-
-    }
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
index 56ee286..9548074 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
@@ -27,13 +27,7 @@
 import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.event.EventService;
 import org.apache.nifi.registry.field.Fields;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.apache.nifi.registry.service.RegistryService;
-import org.apache.nifi.registry.web.link.LinkService;
-import org.apache.nifi.registry.web.security.PermissionsService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -46,8 +40,6 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -58,28 +50,14 @@
         description = "Retrieve items across all buckets for which the user is authorized.",
         authorizations = { @Authorization("Authorization") }
 )
-public class ItemResource extends AuthorizableApplicationResource {
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(ItemResource.class);
+public class ItemResource extends ApplicationResource {
 
     @Context
     UriInfo uriInfo;
 
-    private final LinkService linkService;
-    private final PermissionsService permissionsService;
-    private final RegistryService registryService;
-
     @Autowired
-    public ItemResource(
-            final RegistryService registryService,
-            final LinkService linkService,
-            final PermissionsService permissionsService,
-            final AuthorizationService authorizationService,
-            final EventService eventService) {
-        super(authorizationService, eventService);
-        this.registryService = registryService;
-        this.linkService = linkService;
-        this.permissionsService = permissionsService;
+    public ItemResource(final ServiceFacade serviceFacade, final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
 
@@ -95,7 +73,7 @@
     )
     @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
     public Response getItems() {
-
+        // Service facade with return only items from authorized buckets
         // Note: We don't explicitly check for access to (READ, /buckets) or
         // (READ, /items ) because a user might have access to individual buckets
         // without top-level access. For example, a user that has
@@ -103,20 +81,7 @@
         // get a 403 error returned from this endpoint. This has the side effect
         // that a user with no access to any buckets gets an empty array returned
         // from this endpoint instead of 403 as one might expect.
-
-        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
-        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
-            // not authorized for any bucket, return empty list of items
-            return Response.status(Response.Status.OK).entity(new ArrayList<BucketItem>()).build();
-        }
-
-        List<BucketItem> items = registryService.getBucketItems(authorizedBucketIds);
-        if (items == null) {
-            items = Collections.emptyList();
-        }
-        permissionsService.populateItemPermissions(items);
-        linkService.populateLinks(items);
-
+        final List<BucketItem> items = serviceFacade.getBucketItems();
         return Response.status(Response.Status.OK).entity(items).build();
     }
 
@@ -146,12 +111,7 @@
             @ApiParam("The bucket identifier")
             final String bucketId) {
 
-        authorizeBucketAccess(RequestAction.READ, bucketId);
-
-        final List<BucketItem> items = registryService.getBucketItems(bucketId);
-        permissionsService.populateItemPermissions(items);
-        linkService.populateLinks(items);
-
+        final List<BucketItem> items = serviceFacade.getBucketItems(bucketId);
         return Response.status(Response.Status.OK).entity(items).build();
     }
 
@@ -165,7 +125,7 @@
             response = Fields.class
     )
     public Response getAvailableBucketItemFields() {
-        final Set<String> bucketFields = registryService.getBucketItemFields();
+        final Set<String> bucketFields = serviceFacade.getBucketItemFields();
         final Fields fields = new Fields(bucketFields);
         return Response.status(Response.Status.OK).entity(fields).build();
     }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
index d9d3521..5d7052b 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
@@ -29,26 +29,24 @@
 import org.apache.nifi.registry.authorization.UserGroup;
 import org.apache.nifi.registry.event.EventFactory;
 import org.apache.nifi.registry.event.EventService;
-import org.apache.nifi.registry.exception.ResourceNotFoundException;
-import org.apache.nifi.registry.security.authorization.Authorizer;
-import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.service.AuthorizationService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.revision.web.ClientIdParameter;
+import org.apache.nifi.registry.revision.web.LongParameter;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -65,16 +63,12 @@
         description = "Endpoint for managing users and user groups.",
         authorizations = { @Authorization("Authorization") }
 )
-public class TenantResource extends AuthorizableApplicationResource {
-
-    private static final Logger logger = LoggerFactory.getLogger(TenantResource.class);
-
-    private Authorizer authorizer;
+public class TenantResource extends ApplicationResource {
 
     @Autowired
-    public TenantResource(AuthorizationService authorizationService, EventService eventService) {
-        super(authorizationService, eventService);
-        authorizer = authorizationService.getAuthorizer();
+    public TenantResource(final ServiceFacade serviceFacade,
+                          final EventService eventService) {
+        super(serviceFacade, eventService);
     }
 
 
@@ -109,25 +103,11 @@
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response createUser(
             @Context
-            final HttpServletRequest httpServletRequest,
+                final HttpServletRequest httpServletRequest,
             @ApiParam(value = "The user configuration details.", required = true)
-            final User requestUser) {
+                final User requestUser) {
 
-        verifyAuthorizerSupportsConfigurableUserGroups();
-
-        if (requestUser == null) {
-            throw new IllegalArgumentException("User details must be specified when creating a new user.");
-        }
-        if (requestUser.getIdentifier() != null) {
-            throw new IllegalArgumentException("User identifier cannot be specified when creating a new user.");
-        }
-        if (StringUtils.isBlank(requestUser.getIdentity())) {
-            throw new IllegalArgumentException("User identity must be specified when creating a new user.");
-        }
-
-        authorizeAccess(RequestAction.WRITE);
-
-        User createdUser = authorizationService.createUser(requestUser);
+        final User createdUser = serviceFacade.createUser(requestUser);
         publish(EventFactory.userCreated(createdUser));
 
         String locationUri = generateUserUri(createdUser);
@@ -160,12 +140,8 @@
             @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response getUsers() {
-        verifyAuthorizerIsManaged();
-
-        authorizeAccess(RequestAction.READ);
-
         // get all the users
-        final List<User> users = authorizationService.getUsers();
+        final List<User> users = serviceFacade.getUsers();
 
         // generate the response
         return generateOkResponse(users).build();
@@ -200,15 +176,7 @@
     public Response getUser(
             @ApiParam(value = "The user id.", required = true)
             @PathParam("id") final String identifier) {
-        verifyAuthorizerIsManaged();
-        authorizeAccess(RequestAction.READ);
-
-        final User user = authorizationService.getUser(identifier);
-        if (user == null) {
-            logger.warn("The specified user id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
-        }
+        final User user = serviceFacade.getUser(identifier);
         return generateOkResponse(user).build();
     }
 
@@ -249,9 +217,6 @@
             @ApiParam(value = "The user configuration details.", required = true)
             final User requestUser) {
 
-        verifyAuthorizerSupportsConfigurableUserGroups();
-        authorizeAccess(RequestAction.WRITE);
-
         if (requestUser == null) {
             throw new IllegalArgumentException("User details must be specified when updating a user.");
         }
@@ -260,14 +225,8 @@
                     + "user id of the requested resource (%s).", requestUser.getIdentifier(), identifier));
         }
 
-        final User updatedUser = authorizationService.updateUser(requestUser);
-        if (updatedUser == null) {
-            logger.warn("The specified user id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
-        }
+        final User updatedUser = serviceFacade.updateUser(requestUser);
         publish(EventFactory.userUpdated(updatedUser));
-
         return generateOkResponse(updatedUser).build();
     }
 
@@ -300,22 +259,21 @@
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response removeUser(
             @Context
-            final HttpServletRequest httpServletRequest,
+                final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true)
+            @QueryParam(VERSION)
+                final LongParameter version,
+            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.")
+            @QueryParam(CLIENT_ID)
+            @DefaultValue(StringUtils.EMPTY)
+                final ClientIdParameter clientId,
             @ApiParam(value = "The user id.", required = true)
             @PathParam("id")
-            final String identifier) {
+                final String identifier) {
 
-        verifyAuthorizerSupportsConfigurableUserGroups();
-        authorizeAccess(RequestAction.DELETE);
-
-        final User user = authorizationService.deleteUser(identifier);
-        if (user == null) {
-            logger.warn("The specified user id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
-        }
+        final RevisionInfo revisionInfo = getRevisionInfo(version, clientId);
+        final User user = serviceFacade.deleteUser(identifier, revisionInfo);
         publish(EventFactory.userDeleted(user));
-
         return generateOkResponse(user).build();
     }
 
@@ -351,27 +309,14 @@
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response createUserGroup(
             @Context
-            final HttpServletRequest httpServletRequest,
+                final HttpServletRequest httpServletRequest,
             @ApiParam(value = "The user group configuration details.", required = true)
-            final UserGroup requestUserGroup) {
+                final UserGroup requestUserGroup) {
 
-        verifyAuthorizerSupportsConfigurableUserGroups();
-        authorizeAccess(RequestAction.WRITE);
-
-        if (requestUserGroup == null) {
-            throw new IllegalArgumentException("User group details must be specified when creating a new group.");
-        }
-        if (requestUserGroup.getIdentifier() != null) {
-            throw new IllegalArgumentException("User group ID cannot be specified when creating a new group.");
-        }
-        if (StringUtils.isBlank(requestUserGroup.getIdentity())) {
-            throw new IllegalArgumentException("User group identity must be specified when creating a new group.");
-        }
-
-        UserGroup createdGroup = authorizationService.createUserGroup(requestUserGroup);
+        final UserGroup createdGroup = serviceFacade.createUserGroup(requestUserGroup);
         publish(EventFactory.userGroupCreated(createdGroup));
 
-        String locationUri = generateUserGroupUri(createdGroup);
+        final String locationUri = generateUserGroupUri(createdGroup);
         return generateCreatedResponse(URI.create(locationUri), createdGroup).build();
     }
 
@@ -402,10 +347,7 @@
             @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response getUserGroups() {
-        verifyAuthorizerIsManaged();
-        authorizeAccess(RequestAction.READ);
-
-        final List<UserGroup> userGroups = authorizationService.getUserGroups();
+        final List<UserGroup> userGroups = serviceFacade.getUserGroups();
         return generateOkResponse(userGroups).build();
     }
 
@@ -438,16 +380,7 @@
     public Response getUserGroup(
             @ApiParam(value = "The user group id.", required = true)
             @PathParam("id") final String identifier) {
-        verifyAuthorizerIsManaged();
-        authorizeAccess(RequestAction.READ);
-
-        final UserGroup userGroup = authorizationService.getUserGroup(identifier);
-        if (userGroup == null) {
-            logger.warn("The specified user group id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
-        }
-
+        final UserGroup userGroup = serviceFacade.getUserGroup(identifier);
         return generateOkResponse(userGroup).build();
     }
 
@@ -488,8 +421,6 @@
             @ApiParam(value = "The user group configuration details.", required = true)
             final UserGroup requestUserGroup) {
 
-        verifyAuthorizerSupportsConfigurableUserGroups();
-
         if (requestUserGroup == null) {
             throw new IllegalArgumentException("User group details must be specified to update a user group.");
         }
@@ -498,16 +429,8 @@
                     + "user group id of the requested resource (%s).", requestUserGroup.getIdentifier(), identifier));
         }
 
-        authorizeAccess(RequestAction.WRITE);
-
-        UserGroup updatedUserGroup = authorizationService.updateUserGroup(requestUserGroup);
-        if (updatedUserGroup == null) {
-            logger.warn("The specified user group id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
-        }
+        final UserGroup updatedUserGroup = serviceFacade.updateUserGroup(requestUserGroup);
         publish(EventFactory.userGroupUpdated(updatedUserGroup));
-
         return generateOkResponse(updatedUserGroup).build();
     }
 
@@ -540,42 +463,24 @@
             @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
     public Response removeUserGroup(
             @Context
-            final HttpServletRequest httpServletRequest,
+                final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true)
+            @QueryParam(VERSION)
+                final LongParameter version,
+            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.")
+            @QueryParam(CLIENT_ID)
+            @DefaultValue(StringUtils.EMPTY)
+                final ClientIdParameter clientId,
             @ApiParam(value = "The user group id.", required = true)
             @PathParam("id")
-            final String identifier) {
-        verifyAuthorizerSupportsConfigurableUserGroups();
-        authorizeAccess(RequestAction.DELETE);
+                final String identifier) {
 
-        final UserGroup userGroup = authorizationService.deleteUserGroup(identifier);
-        if (userGroup == null) {
-            logger.warn("The specified user group id [{}] does not exist.", identifier);
-
-            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
-        }
+        final RevisionInfo revisionInfo = getRevisionInfo(version, clientId);
+        final UserGroup userGroup = serviceFacade.deleteUserGroup(identifier, revisionInfo);
         publish(EventFactory.userGroupDeleted(userGroup));
-
         return generateOkResponse(userGroup).build();
     }
 
-
-    private void verifyAuthorizerIsManaged() {
-        if (!AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)) {
-            throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER);
-        }
-    }
-
-    private void verifyAuthorizerSupportsConfigurableUserGroups() {
-        if (!AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer)) {
-            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_USERS);
-        }
-    }
-
-    private void authorizeAccess(RequestAction actionType) {
-        final Authorizable tenantsAuthorizable = authorizableLookup.getTenantsAuthorizable();
-        authorizationService.authorize(tenantsAuthorizable, actionType);
-    }
-
     private String generateUserUri(final User user) {
         return generateResourceUri("tenants", "users", user.getIdentifier());
     }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidRevisionExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidRevisionExceptionMapper.java
new file mode 100644
index 0000000..e9fd628
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidRevisionExceptionMapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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.nifi.registry.web.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.revision.api.InvalidRevisionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps invalid revision exceptions into client responses.
+ */
+@Provider
+public class InvalidRevisionExceptionMapper implements ExceptionMapper<InvalidRevisionException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(InvalidRevisionExceptionMapper.class);
+
+    @Override
+    public Response toResponse(final InvalidRevisionException exception) {
+        // log the error
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
index b792830..5bbaa8d 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.registry.web.security;
 
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.resource.ResourceType;
 import org.apache.nifi.registry.service.AuthorizationService;
 import org.apache.nifi.registry.web.security.authentication.AnonymousIdentityFilter;
@@ -63,6 +64,9 @@
     @Autowired
     private AuthorizationService authorizationService;
 
+    @Autowired
+    private Authorizer authorizer;
+
     private AnonymousIdentityFilter anonymousAuthenticationFilter = new AnonymousIdentityFilter();
 
     @Autowired
@@ -142,7 +146,7 @@
 
     private IdentityAuthenticationProvider x509AuthenticationProvider() {
         if (x509AuthenticationProvider == null) {
-            x509AuthenticationProvider = new X509IdentityAuthenticationProvider(properties, authorizationService.getAuthorizer(), x509IdentityProvider);
+            x509AuthenticationProvider = new X509IdentityAuthenticationProvider(properties, authorizer, x509IdentityProvider);
         }
         return x509AuthenticationProvider;
     }
@@ -156,7 +160,7 @@
 
     private IdentityAuthenticationProvider jwtAuthenticationProvider() {
         if (jwtAuthenticationProvider == null) {
-            jwtAuthenticationProvider = new IdentityAuthenticationProvider(properties, authorizationService.getAuthorizer(), jwtIdentityProvider);
+            jwtAuthenticationProvider = new IdentityAuthenticationProvider(properties, authorizer, jwtIdentityProvider);
         }
         return jwtAuthenticationProvider;
     }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java
index 1e00ee1..a15fea3 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java
@@ -16,9 +16,9 @@
  */
 package org.apache.nifi.registry.web.security;
 
+import org.apache.nifi.registry.authorization.Permissions;
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.bucket.BucketItem;
-import org.apache.nifi.registry.authorization.Permissions;
 import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
 import org.apache.nifi.registry.security.authorization.resource.Authorizable;
 import org.apache.nifi.registry.service.AuthorizationService;
@@ -64,29 +64,24 @@
     }
 
     private void populateBucketPermissions(final Bucket bucket, final Permissions knownPermissions) {
-
         if (bucket == null) {
             return;
         }
 
         Permissions bucketPermissions = createPermissionsForBucketId(bucket.getIdentifier(), knownPermissions);
         bucket.setPermissions(bucketPermissions);
-
     }
 
     private void populateItemPermissions(final BucketItem bucketItem, final Permissions knownPermissions) {
-
         if (bucketItem == null) {
             return;
         }
 
         Permissions bucketItemPermissions = createPermissionsForBucketId(bucketItem.getBucketIdentifier(), knownPermissions);
         bucketItem.setPermissions(bucketItemPermissions);
-
     }
 
     private Permissions createPermissionsForBucketId(String bucketId, final Permissions knownPermissions) {
-
         Authorizable bucketResource = authorizableLookup.getBucketAuthorizable(bucketId);
 
         Permissions permissions = knownPermissions == null
@@ -94,7 +89,6 @@
                 : authorizationService.getPermissionsForResource(bucketResource, knownPermissions);
 
         return permissions;
-
     }
 
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java
new file mode 100644
index 0000000..cc0edf9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java
@@ -0,0 +1,43 @@
+/*
+ * 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.nifi.registry.web.service;
+
+import org.apache.nifi.registry.revision.api.RevisionManager;
+import org.apache.nifi.registry.revision.entity.RevisableEntityService;
+import org.apache.nifi.registry.revision.entity.StandardRevisableEntityService;
+import org.apache.nifi.registry.revision.jdbc.JdbcRevisionManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Creates the beans needed for revision management.
+ */
+@Configuration
+public class RevisionConfiguration {
+
+    @Bean
+    public synchronized RevisionManager getRevisionManager(final JdbcTemplate jdbcTemplate) {
+        return new JdbcRevisionManager(jdbcTemplate);
+    }
+
+    @Bean
+    public synchronized RevisableEntityService getRevisableEntityService(final RevisionManager revisionManager) {
+        return new StandardRevisableEntityService(revisionManager);
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java
new file mode 100644
index 0000000..b1faac4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java
@@ -0,0 +1,235 @@
+/*
+ * 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.nifi.registry.web.service;
+
+import org.apache.nifi.registry.RegistryConfiguration;
+import org.apache.nifi.registry.authorization.AccessPolicy;
+import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.authorization.Resource;
+import org.apache.nifi.registry.authorization.User;
+import org.apache.nifi.registry.authorization.UserGroup;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.extension.bundle.Bundle;
+import org.apache.nifi.registry.extension.bundle.BundleFilterParams;
+import org.apache.nifi.registry.extension.bundle.BundleType;
+import org.apache.nifi.registry.extension.bundle.BundleVersion;
+import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams;
+import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata;
+import org.apache.nifi.registry.extension.component.ExtensionFilterParams;
+import org.apache.nifi.registry.extension.component.ExtensionMetadata;
+import org.apache.nifi.registry.extension.component.TagCount;
+import org.apache.nifi.registry.extension.component.manifest.Extension;
+import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+
+import javax.ws.rs.core.StreamingOutput;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+public interface ServiceFacade {
+
+    // ---------------------- Bucket methods ----------------------------------------------
+
+    Bucket createBucket(Bucket bucket);
+
+    Bucket getBucket(String bucketIdentifier);
+
+    List<Bucket> getBuckets();
+
+    Bucket updateBucket(Bucket bucket);
+
+    Bucket deleteBucket(String bucketIdentifier, RevisionInfo revisionInfo);
+
+    // ---------------------- BucketItem methods ----------------------------------------------
+
+    List<BucketItem> getBucketItems(String bucketIdentifier);
+
+    List<BucketItem> getBucketItems();
+
+    // ---------------------- Flow methods ----------------------------------------------
+
+    VersionedFlow createFlow(String bucketIdentifier, VersionedFlow versionedFlow);
+
+    VersionedFlow getFlow(String bucketIdentifier, String flowIdentifier);
+
+    VersionedFlow getFlow(String flowIdentifier);
+
+    List<VersionedFlow> getFlows(String bucketId);
+
+    VersionedFlow updateFlow(VersionedFlow versionedFlow);
+
+    VersionedFlow deleteFlow(String bucketIdentifier, String flowIdentifier, RevisionInfo revisionInfo);
+
+    // ---------------------- Flow Snapshot methods ----------------------------------------------
+
+    VersionedFlowSnapshot createFlowSnapshot(VersionedFlowSnapshot flowSnapshot);
+
+    VersionedFlowSnapshot getFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer version);
+
+    VersionedFlowSnapshot getFlowSnapshot(String flowIdentifier, Integer version);
+
+    VersionedFlowSnapshot getLatestFlowSnapshot(String bucketIdentifier, String flowIdentifier);
+
+    VersionedFlowSnapshot getLatestFlowSnapshot(String flowIdentifier);
+
+    SortedSet<VersionedFlowSnapshotMetadata> getFlowSnapshots(String bucketIdentifier, String flowIdentifier);
+
+    SortedSet<VersionedFlowSnapshotMetadata> getFlowSnapshots(String flowIdentifier);
+
+    VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(String bucketIdentifier, String flowIdentifier);
+
+    VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(String flowIdentifier);
+
+    VersionedFlowDifference getFlowDiff(String bucketIdentifier, String flowIdentifier, Integer versionA, Integer versionB);
+
+    // ---------------------- Bundle methods ----------------------------------------------
+
+    List<Bundle> getBundles(BundleFilterParams filterParams);
+
+    List<Bundle> getBundlesByBucket(String bucketIdentifier);
+
+    Bundle getBundle(String bundleIdentifier);
+
+    Bundle deleteBundle(String bundleIdentifier);
+
+    // ---------------------- Bundle Version methods ----------------------------------------------
+
+    BundleVersion createBundleVersion(String bucketIdentifier, BundleType bundleType, InputStream inputStream, String clientSha256) throws IOException;
+
+    SortedSet<BundleVersionMetadata> getBundleVersions(BundleVersionFilterParams filterParams);
+
+    SortedSet<BundleVersionMetadata> getBundleVersions(String bundleIdentifier);
+
+    BundleVersion getBundleVersion(String bundleId, String version);
+
+    StreamingContent getBundleVersionContent(String bundleId, String version);
+
+    BundleVersion deleteBundleVersion(String bundleId, String version);
+
+    // ---------------------- Extension methods ----------------------------------------------
+
+    SortedSet<ExtensionMetadata> getExtensionMetadata(ExtensionFilterParams filterParams);
+
+    SortedSet<ExtensionMetadata> getExtensionMetadata(ProvidedServiceAPI serviceAPI);
+
+    SortedSet<ExtensionMetadata> getExtensionMetadata(String bundleIdentifier, String version);
+
+    Extension getExtension(String bundleIdentifier, String version, String name);
+
+    StreamingOutput getExtensionDocs(String bundleIdentifier, String version, String name);
+
+    StreamingOutput getAdditionalDetailsDocs(String bundleIdentifier, String version, String name);
+
+    SortedSet<TagCount> getExtensionTags();
+
+    // ---------------------- Extension Repository methods ----------------------------------------------
+
+    SortedSet<ExtensionRepoBucket> getExtensionRepoBuckets(URI baseUri);
+
+    SortedSet<ExtensionRepoGroup> getExtensionRepoGroups(URI baseUri, String bucketName);
+
+    SortedSet<ExtensionRepoArtifact> getExtensionRepoArtifacts(URI baseUri, String bucketName, String groupId);
+
+    SortedSet<ExtensionRepoVersionSummary> getExtensionRepoVersions(URI baseUri, String bucketName, String groupId, String artifactId);
+
+    ExtensionRepoVersion getExtensionRepoVersion(URI baseUri, String bucketName, String groupId, String artifactId, String version);
+
+    StreamingContent getExtensionRepoVersionContent(String bucketName, String groupId, String artifactId, String version);
+
+    String getExtensionRepoVersionSha256(String bucketName, String groupId, String artifactId, String version);
+
+    List<ExtensionRepoExtensionMetadata> getExtensionRepoExtensions(URI baseUri, String bucketName, String groupId, String artifactId, String version);
+
+    Extension getExtensionRepoExtension(URI baseUri, String bucketName, String groupId, String artifactId, String version, String extensionName);
+
+    StreamingOutput getExtensionRepoExtensionDocs(URI baseUri, String bucketName, String groupId, String artifactId, String version, String extensionName);
+
+    StreamingOutput getExtensionRepoExtensionAdditionalDocs(URI baseUri, String bucketName, String groupId, String artifactId, String version, String extensionName);
+
+    // ---------------------- Field methods ---------------------------------------------
+
+    Set<String> getBucketFields();
+
+    Set<String> getBucketItemFields();
+
+    Set<String> getFlowFields();
+
+    // ---------------------- User methods ----------------------------------------------
+
+    User createUser(User user);
+
+    List<User> getUsers();
+
+    User getUser(String identifier);
+
+    User updateUser(User user);
+
+    User deleteUser(String identifier, RevisionInfo revisionInfo);
+
+    // ---------------------- User Group methods --------------------------------------
+
+    UserGroup createUserGroup(UserGroup userGroup);
+
+    List<UserGroup> getUserGroups();
+
+    UserGroup getUserGroup(String identifier);
+
+    UserGroup updateUserGroup(UserGroup userGroup);
+
+    UserGroup deleteUserGroup(String identifier, RevisionInfo revisionInfo);
+
+    // ---------------------- Access Policy methods ----------------------------------------
+
+    AccessPolicy createAccessPolicy(AccessPolicy accessPolicy);
+
+    AccessPolicy getAccessPolicy(String identifier);
+
+    AccessPolicy getAccessPolicy(String resource, RequestAction action);
+
+    List<AccessPolicy> getAccessPolicies();
+
+    AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy);
+
+    AccessPolicy deleteAccessPolicy(String identifier, RevisionInfo revisionInfo);
+
+    List<Resource> getResources();
+
+    // ---------------------- Permission methods ----------------------------------------
+
+    CurrentUser getCurrentUser();
+
+    // ----------------------  Configuration methods ------------------------------------
+
+    RegistryConfiguration getRegistryConfiguration();
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java
new file mode 100644
index 0000000..7c908e4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java
@@ -0,0 +1,1180 @@
+/*
+ * 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.nifi.registry.web.service;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.RegistryConfiguration;
+import org.apache.nifi.registry.authorization.AccessPolicy;
+import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.authorization.Resource;
+import org.apache.nifi.registry.authorization.User;
+import org.apache.nifi.registry.authorization.UserGroup;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.extension.bundle.Bundle;
+import org.apache.nifi.registry.extension.bundle.BundleFilterParams;
+import org.apache.nifi.registry.extension.bundle.BundleType;
+import org.apache.nifi.registry.extension.bundle.BundleVersion;
+import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams;
+import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata;
+import org.apache.nifi.registry.extension.component.ExtensionFilterParams;
+import org.apache.nifi.registry.extension.component.ExtensionMetadata;
+import org.apache.nifi.registry.extension.component.TagCount;
+import org.apache.nifi.registry.extension.component.manifest.Extension;
+import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.revision.api.InvalidRevisionException;
+import org.apache.nifi.registry.revision.entity.RevisableEntity;
+import org.apache.nifi.registry.revision.entity.RevisableEntityService;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.service.extension.ExtensionService;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.security.PermissionsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Isolation;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.StreamingOutput;
+import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.UUID;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * A wrapper around the service layer that applies validation, authorization, and revision management to all services.
+ *
+ * All REST resources should access the service layer through this facade.
+ */
+@Service
+@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Throwable.class)
+public class StandardServiceFacade implements ServiceFacade {
+
+    private static final String INVALID_REVISION_MSG = "The %s you attempted to %s with id '%s' is out of date with the server. " +
+            "You may need to refresh your client and try again.";
+
+    public static final String USER_GROUP_ENTITY_TYPE = "User Group";
+    public static final String USER_ENTITY_TYPE = "User";
+    public static final String ACCESS_POLICY_ENTITY_TYPE = "Access Policy";
+    public static final String VERSIONED_FLOW_ENTITY_TYPE = "Versioned Flow";
+    public static final String BUCKET_ENTITY_TYPE = "Bucket";
+
+    private final RegistryService registryService;
+    private final ExtensionService extensionService;
+    private final AuthorizationService authorizationService;
+    private final AuthorizableLookup authorizableLookup;
+    private final RevisableEntityService entityService;
+    private final PermissionsService permissionsService;
+    private final LinkService linkService;
+
+    @Autowired
+    public StandardServiceFacade(final RegistryService registryService,
+                                 final ExtensionService extensionService,
+                                 final AuthorizationService authorizationService,
+                                 final AuthorizableLookup authorizableLookup,
+                                 final RevisableEntityService entityService,
+                                 final PermissionsService permissionsService,
+                                 final LinkService linkService) {
+        this.registryService = registryService;
+        this.extensionService = extensionService;
+        this.authorizationService = authorizationService;
+        this.authorizableLookup = authorizableLookup;
+        this.entityService = entityService;
+        this.permissionsService = permissionsService;
+        this.linkService = linkService;
+    }
+
+    private String currentUserIdentity() {
+        return NiFiUserUtils.getNiFiUserIdentity();
+    }
+
+    // ---------------------- Bucket methods ----------------------------------------------
+
+    @Override
+    public Bucket createBucket(final Bucket bucket) {
+        authorizeBucketsAccess(RequestAction.WRITE);
+        validateCreationOfRevisableEntity(bucket, BUCKET_ENTITY_TYPE);
+
+        bucket.setIdentifier(UUID.randomUUID().toString());
+
+        final Bucket createdBucket = createRevisableEntity(bucket, BUCKET_ENTITY_TYPE, currentUserIdentity(),
+                () -> registryService.createBucket(bucket));
+        permissionsService.populateBucketPermissions(createdBucket);
+        linkService.populateLinks(createdBucket);
+        return createdBucket;
+    }
+
+    @Override
+    public Bucket getBucket(final String bucketIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final Bucket bucket = entityService.get(() -> registryService.getBucket(bucketIdentifier));
+        permissionsService.populateBucketPermissions(bucket);
+        linkService.populateLinks(bucket);
+        return bucket;
+    }
+
+    @Override
+    public List<Bucket> getBuckets() {
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return Collections.emptyList();
+        }
+
+        final List<Bucket> buckets = entityService.getEntities(() -> registryService.getBuckets(authorizedBucketIds));
+        permissionsService.populateBucketPermissions(buckets);
+        linkService.populateLinks(buckets);
+        return buckets;
+    }
+
+    @Override
+    public Bucket updateBucket(final Bucket bucket) {
+        authorizeBucketAccess(RequestAction.WRITE, bucket.getIdentifier());
+        validateUpdateOfRevisableEntity(bucket, BUCKET_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        registryService.verifyBucketExists(bucket.getIdentifier());
+
+        final Bucket updatedBucket = updateRevisableEntity(bucket, BUCKET_ENTITY_TYPE, currentUserIdentity(),
+                () -> registryService.updateBucket(bucket));
+        permissionsService.populateBucketPermissions(updatedBucket);
+        linkService.populateLinks(updatedBucket);
+        return  updatedBucket;
+    }
+
+    @Override
+    public Bucket deleteBucket(final String bucketIdentifier, final RevisionInfo revisionInfo) {
+        authorizeBucketAccess(RequestAction.DELETE, bucketIdentifier);
+        validateDeleteOfRevisableEntity(bucketIdentifier, revisionInfo, BUCKET_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        registryService.verifyBucketExists(bucketIdentifier);
+
+        return deleteRevisableEntity(bucketIdentifier, BUCKET_ENTITY_TYPE, revisionInfo,
+                () -> registryService.deleteBucket(bucketIdentifier));
+    }
+
+    // ---------------------- BucketItem methods ----------------------------------------------
+
+    @Override
+    public List<BucketItem> getBucketItems(final String bucketIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final List<BucketItem> items = registryService.getBucketItems(bucketIdentifier);
+        entityService.populateRevisions(items);
+        permissionsService.populateItemPermissions(items);
+        linkService.populateLinks(items);
+        return items;
+    }
+
+    @Override
+    public List<BucketItem> getBucketItems() {
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return new ArrayList<>();
+        }
+
+        final List<BucketItem> items = registryService.getBucketItems(authorizedBucketIds);
+        entityService.populateRevisions(items);
+        permissionsService.populateItemPermissions(items);
+        linkService.populateLinks(items);
+        return items;
+    }
+
+    // ---------------------- Flow methods ----------------------------------------------
+
+    @Override
+    public VersionedFlow createFlow(final String bucketIdentifier, final VersionedFlow versionedFlow) {
+        authorizeBucketAccess(RequestAction.WRITE, bucketIdentifier);
+        validateCreationOfRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE);
+
+        versionedFlow.setIdentifier(UUID.randomUUID().toString());
+
+        final VersionedFlow createdFlow = createRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE, currentUserIdentity(),
+                () -> registryService.createFlow(bucketIdentifier, versionedFlow));
+        permissionsService.populateItemPermissions(createdFlow);
+        linkService.populateLinks(createdFlow);
+        return createdFlow;
+    }
+
+    @Override
+    public VersionedFlow getFlow(final String bucketIdentifier, final String flowIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final VersionedFlow flow = entityService.get(
+                () -> registryService.getFlow(bucketIdentifier, flowIdentifier));
+        permissionsService.populateItemPermissions(flow);
+        linkService.populateLinks(flow);
+        return flow;
+    }
+
+    @Override
+    public VersionedFlow getFlow(final String flowIdentifier) {
+        final VersionedFlow flow =  entityService.get(() -> registryService.getFlow(flowIdentifier));
+        authorizeBucketAccess(RequestAction.READ, flow);
+
+        permissionsService.populateItemPermissions(flow);
+        linkService.populateLinks(flow);
+        return flow;
+    }
+
+    @Override
+    public List<VersionedFlow> getFlows(final String bucketIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final List<VersionedFlow> flows = entityService.getEntities(() -> registryService.getFlows(bucketIdentifier));
+        permissionsService.populateItemPermissions(flows);
+        linkService.populateLinks(flows);
+        return flows;
+    }
+
+    @Override
+    public VersionedFlow updateFlow(final VersionedFlow versionedFlow) {
+        authorizeBucketAccess(RequestAction.WRITE, versionedFlow);
+        validateUpdateOfRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        registryService.verifyFlowExists(versionedFlow.getIdentifier());
+
+        final VersionedFlow updatedFlow =  updateRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE, currentUserIdentity(),
+                () -> registryService.updateFlow(versionedFlow));
+        permissionsService.populateItemPermissions(updatedFlow);
+        linkService.populateLinks(updatedFlow);
+        return updatedFlow;
+    }
+
+    @Override
+    public VersionedFlow deleteFlow(final String bucketIdentifier, final String flowIdentifier, final RevisionInfo revisionInfo) {
+        authorizeBucketAccess(RequestAction.DELETE, bucketIdentifier);
+        validateDeleteOfRevisableEntity(flowIdentifier, revisionInfo, VERSIONED_FLOW_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        registryService.verifyFlowExists(flowIdentifier);
+
+        return deleteRevisableEntity(flowIdentifier, VERSIONED_FLOW_ENTITY_TYPE, revisionInfo,
+                () -> registryService.deleteFlow(bucketIdentifier, flowIdentifier));
+    }
+
+    // ---------------------- Flow Snapshot methods ----------------------------------------------
+
+    @Override
+    public VersionedFlowSnapshot createFlowSnapshot(final VersionedFlowSnapshot flowSnapshot) {
+        authorizeBucketAccess(RequestAction.WRITE, flowSnapshot);
+
+        final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(flowSnapshot);
+        populateLinksAndPermissions(createdSnapshot);
+        return createdSnapshot;
+    }
+
+    @Override
+    public VersionedFlowSnapshot getFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketIdentifier, flowIdentifier, version);
+        populateLinksAndPermissions(snapshot);
+        return snapshot;
+    }
+
+    @Override
+    public VersionedFlowSnapshot getFlowSnapshot(final String flowIdentifier, final Integer version) {
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowIdentifier);
+        authorizeBucketAccess(RequestAction.READ, latestMetadata);
+
+        final String bucketIdentifier = latestMetadata.getBucketIdentifier();
+        final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketIdentifier, flowIdentifier, version);
+        populateLinksAndPermissions(snapshot);
+        return snapshot;
+    }
+
+    @Override
+    public VersionedFlowSnapshot getLatestFlowSnapshot(final String bucketIdentifier, final String flowIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final VersionedFlowSnapshotMetadata latestMetadata = getLatestFlowSnapshotMetadata(bucketIdentifier, flowIdentifier);
+        final VersionedFlowSnapshot lastSnapshot = getFlowSnapshot(bucketIdentifier, flowIdentifier, latestMetadata.getVersion());
+        populateLinksAndPermissions(lastSnapshot);
+        return lastSnapshot;
+    }
+
+    @Override
+    public VersionedFlowSnapshot getLatestFlowSnapshot(final String flowIdentifier) {
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowIdentifier);
+        authorizeBucketAccess(RequestAction.READ, latestMetadata);
+
+        final String bucketIdentifier = latestMetadata.getBucketIdentifier();
+        final Integer latestVersion = latestMetadata.getVersion();
+
+        final VersionedFlowSnapshot lastSnapshot = registryService.getFlowSnapshot(bucketIdentifier, flowIdentifier, latestVersion);
+        populateLinksAndPermissions(lastSnapshot);
+        return lastSnapshot;
+    }
+
+    @Override
+    public SortedSet<VersionedFlowSnapshotMetadata> getFlowSnapshots(final String bucketIdentifier, final String flowIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketIdentifier, flowIdentifier);
+        linkService.populateLinks(snapshots);
+        return snapshots;
+    }
+
+    @Override
+    public SortedSet<VersionedFlowSnapshotMetadata> getFlowSnapshots(final String flowIdentifier) {
+        final VersionedFlow flow = registryService.getFlow(flowIdentifier);
+        authorizeBucketAccess(RequestAction.READ, flow);
+
+        final String bucketIdentifier = flow.getBucketIdentifier();
+        final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketIdentifier, flowIdentifier);
+        linkService.populateLinks(snapshots);
+        return snapshots;
+    }
+
+    @Override
+    public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String bucketIdentifier, final String flowIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final VersionedFlowSnapshotMetadata latest = registryService.getLatestFlowSnapshotMetadata(bucketIdentifier, flowIdentifier);
+        linkService.populateLinks(latest);
+        return latest;
+    }
+
+    @Override
+    public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String flowIdentifier) {
+        final VersionedFlowSnapshotMetadata latest = registryService.getLatestFlowSnapshotMetadata(flowIdentifier);
+        authorizeBucketAccess(RequestAction.READ, latest);
+
+        linkService.populateLinks(latest);
+        return latest;
+    }
+
+    @Override
+    public VersionedFlowDifference getFlowDiff(final String bucketIdentifier, final String flowIdentifier, final Integer versionA, final Integer versionB) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+        return registryService.getFlowDiff(bucketIdentifier, flowIdentifier, versionA, versionB);
+    }
+
+    private void populateLinksAndPermissions(final VersionedFlowSnapshot snapshot) {
+        if (snapshot == null) {
+            return;
+        }
+
+        if (snapshot.getSnapshotMetadata() != null) {
+            linkService.populateLinks(snapshot.getSnapshotMetadata());
+        }
+
+        if (snapshot.getFlow() != null) {
+            linkService.populateLinks(snapshot.getFlow());
+        }
+
+        if (snapshot.getBucket() != null) {
+            permissionsService.populateBucketPermissions(snapshot.getBucket());
+            linkService.populateLinks(snapshot.getBucket());
+        }
+    }
+
+    // ---------------------- Bundle methods ----------------------------------------------
+
+    @Override
+    public List<Bundle> getBundles(final BundleFilterParams filterParams) {
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return new ArrayList<>();
+        }
+
+        final List<Bundle> bundles = extensionService.getBundles(authorizedBucketIds, filterParams);
+        permissionsService.populateItemPermissions(bundles);
+        linkService.populateLinks(bundles);
+        return bundles;
+    }
+
+    @Override
+    public List<Bundle> getBundlesByBucket(final String bucketIdentifier) {
+        authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
+
+        final List<Bundle> bundles = extensionService.getBundlesByBucket(bucketIdentifier);
+        permissionsService.populateItemPermissions(bundles);
+        linkService.populateLinks(bundles);
+        return bundles;
+    }
+
+    @Override
+    public Bundle getBundle(final String bundleIdentifier) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        permissionsService.populateItemPermissions(bundle);
+        linkService.populateLinks(bundle);
+        return bundle;
+    }
+
+    @Override
+    public Bundle deleteBundle(final String bundleIdentifier) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+        authorizeBucketAccess(RequestAction.DELETE, bundle);
+
+        final Bundle deletedBundle = extensionService.deleteBundle(bundle);
+        permissionsService.populateItemPermissions(deletedBundle);
+        linkService.populateLinks(deletedBundle);
+        return deletedBundle;
+    }
+
+    // ---------------------- Bundle Version methods ----------------------------------------------
+
+    @Override
+    public BundleVersion createBundleVersion(final String bucketIdentifier, final BundleType bundleType,
+                                             final InputStream inputStream, final String clientSha256) throws IOException {
+
+        authorizeBucketAccess(RequestAction.WRITE, bucketIdentifier);
+
+        final BundleVersion createdBundleVersion = extensionService.createBundleVersion(
+                bucketIdentifier, bundleType, inputStream, clientSha256);
+
+        linkService.populateLinks(createdBundleVersion.getVersionMetadata());
+        linkService.populateLinks(createdBundleVersion.getBundle());
+        linkService.populateLinks(createdBundleVersion.getBucket());
+
+        permissionsService.populateItemPermissions(createdBundleVersion.getBundle());
+
+        return createdBundleVersion;
+    }
+
+    @Override
+    public SortedSet<BundleVersionMetadata> getBundleVersions(final BundleVersionFilterParams filterParams) {
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return Collections.emptySortedSet();
+        }
+
+        final SortedSet<BundleVersionMetadata> bundleVersions = extensionService.getBundleVersions(authorizedBucketIds, filterParams);
+        linkService.populateLinks(bundleVersions);
+        return bundleVersions;
+    }
+
+    @Override
+    public SortedSet<BundleVersionMetadata> getBundleVersions(final String bundleIdentifier) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        final SortedSet<BundleVersionMetadata> bundleVersions = extensionService.getBundleVersions(bundleIdentifier);
+        linkService.populateLinks(bundleVersions);
+        return bundleVersions;
+    }
+
+    @Override
+    public BundleVersion getBundleVersion(final String bundleIdentifier, final String version) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        final String bucketIdentifier = bundle.getBucketIdentifier();
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version);
+        linkService.populateLinks(bundleVersion);
+        return bundleVersion;
+    }
+
+    @Override
+    public StreamingContent getBundleVersionContent(final String bundleIdentifier, final String version) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        final String bucketIdentifier = bundle.getBucketIdentifier();
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version);
+
+        final StreamingOutput streamingOutput = (output) -> extensionService.writeBundleVersionContent(bundleVersion, output);
+        return new StreamingContent(streamingOutput, bundleVersion.getFilename());
+    }
+
+    @Override
+    public BundleVersion deleteBundleVersion(final String bundleIdentifier, final String version) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+        authorizeBucketAccess(RequestAction.DELETE, bundle);
+
+        final String bucketIdentifier = bundle.getBucketIdentifier();
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version);
+
+        final BundleVersion deletedBundleVersion = extensionService.deleteBundleVersion(bundleVersion);
+        linkService.populateLinks(deletedBundleVersion);
+        return deletedBundleVersion;
+    }
+
+    // ---------------------- Extension methods ----------------------------------------------
+
+    @Override
+    public SortedSet<ExtensionMetadata> getExtensionMetadata(final ExtensionFilterParams filterParams) {
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return Collections.emptySortedSet();
+        }
+
+        final SortedSet<ExtensionMetadata> metadata = extensionService.getExtensionMetadata(authorizedBucketIds, filterParams);
+        linkService.populateLinks(metadata);
+        return metadata;
+    }
+
+    @Override
+    public SortedSet<ExtensionMetadata> getExtensionMetadata(final ProvidedServiceAPI serviceAPI) {
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return Collections.emptySortedSet();
+        }
+
+        final SortedSet<ExtensionMetadata> metadata = extensionService.getExtensionMetadata(authorizedBucketIds, serviceAPI);
+        linkService.populateLinks(metadata);
+        return metadata;
+    }
+
+    @Override
+    public SortedSet<ExtensionMetadata> getExtensionMetadata(final String bundleIdentifier, final String version) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        final String bucketIdentifier = bundle.getBucketIdentifier();
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version);
+
+        final SortedSet<ExtensionMetadata> extensions = extensionService.getExtensionMetadata(bundleVersion);
+        linkService.populateLinks(extensions);
+        return extensions;
+    }
+
+    @Override
+    public Extension getExtension(final String bundleIdentifier, final String version, final String name) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        final String bucketIdentifier = bundle.getBucketIdentifier();
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version);
+        return extensionService.getExtension(bundleVersion, name);
+    }
+
+    @Override
+    public StreamingOutput getExtensionDocs(final String bundleIdentifier, final String version, final String name) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        final String bucketIdentifier = bundle.getBucketIdentifier();
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version);
+
+        final StreamingOutput streamingOutput = (output) -> extensionService.writeExtensionDocs(bundleVersion, name, output);
+        return streamingOutput;
+    }
+
+    @Override
+    public StreamingOutput getAdditionalDetailsDocs(final String bundleIdentifier, final String version, final String name) {
+        final Bundle bundle = extensionService.getBundle(bundleIdentifier);
+        authorizeBucketAccess(RequestAction.READ, bundle);
+
+        final String bucketIdentifier = bundle.getBucketIdentifier();
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version);
+
+        final StreamingOutput streamingOutput = (output) -> extensionService.writeAdditionalDetailsDocs(bundleVersion, name, output);
+        return streamingOutput;
+    }
+
+    @Override
+    public SortedSet<TagCount> getExtensionTags() {
+        return extensionService.getExtensionTags();
+    }
+
+    // ---------------------- Extension Repository methods ----------------------------------------------
+
+    @Override
+    public SortedSet<ExtensionRepoBucket> getExtensionRepoBuckets(final URI baseUri) {
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return Collections.emptySortedSet();
+        }
+
+        final SortedSet<ExtensionRepoBucket> repoBuckets = extensionService.getExtensionRepoBuckets(authorizedBucketIds);
+        linkService.populateFullLinks(repoBuckets, baseUri);
+        return repoBuckets;
+    }
+
+    @Override
+    public SortedSet<ExtensionRepoGroup> getExtensionRepoGroups(final URI baseUri, final String bucketName) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final SortedSet<ExtensionRepoGroup> repoGroups = extensionService.getExtensionRepoGroups(bucket);
+        linkService.populateFullLinks(repoGroups, baseUri);
+        return extensionService.getExtensionRepoGroups(bucket);
+    }
+
+    @Override
+    public SortedSet<ExtensionRepoArtifact> getExtensionRepoArtifacts(final URI baseUri, final String bucketName, final String groupId) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final SortedSet<ExtensionRepoArtifact> repoArtifacts = extensionService.getExtensionRepoArtifacts(bucket, groupId);
+        linkService.populateFullLinks(repoArtifacts, baseUri);
+        return repoArtifacts;
+    }
+
+    @Override
+    public SortedSet<ExtensionRepoVersionSummary> getExtensionRepoVersions(final URI baseUri, final String bucketName, final String groupId,
+                                                                           final String artifactId) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final SortedSet<ExtensionRepoVersionSummary> repoVersions = extensionService.getExtensionRepoVersions(bucket, groupId, artifactId);
+        linkService.populateFullLinks(repoVersions, baseUri);
+        return repoVersions;
+    }
+
+    @Override
+    public ExtensionRepoVersion getExtensionRepoVersion(final URI baseUri, final String bucketName, final String groupId,
+                                                        final String artifactId, final String version) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
+
+        final String extensionsUri = generateResourceUri(baseUri,
+                "extension-repository",
+                bundleVersion.getBucket().getName(),
+                bundleVersion.getBundle().getGroupId(),
+                bundleVersion.getBundle().getArtifactId(),
+                bundleVersion.getVersionMetadata().getVersion(),
+                "extensions");
+
+        final String downloadUri = generateResourceUri(baseUri,
+                "extension-repository",
+                bundleVersion.getBucket().getName(),
+                bundleVersion.getBundle().getGroupId(),
+                bundleVersion.getBundle().getArtifactId(),
+                bundleVersion.getVersionMetadata().getVersion(),
+                "content");
+
+        final String sha256Uri = generateResourceUri(baseUri,
+                "extension-repository",
+                bundleVersion.getBucket().getName(),
+                bundleVersion.getBundle().getGroupId(),
+                bundleVersion.getBundle().getArtifactId(),
+                bundleVersion.getVersionMetadata().getVersion(),
+                "sha256");
+
+        final ExtensionRepoVersion repoVersion = new ExtensionRepoVersion();
+        repoVersion.setExtensionsLink(Link.fromUri(extensionsUri).rel("extensions").build());
+        repoVersion.setDownloadLink(Link.fromUri(downloadUri).rel("content").build());
+        repoVersion.setSha256Link(Link.fromUri(sha256Uri).rel("sha256").build());
+        repoVersion.setSha256Supplied(bundleVersion.getVersionMetadata().getSha256Supplied());
+        return repoVersion;
+    }
+
+    @Override
+    public StreamingContent getExtensionRepoVersionContent(final String bucketName, final String groupId, final String artifactId,
+                                                           final String version) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
+        final StreamingOutput streamingOutput = (output) -> extensionService.writeBundleVersionContent(bundleVersion, output);
+        return new StreamingContent(streamingOutput, bundleVersion.getFilename());
+    }
+
+    @Override
+    public String getExtensionRepoVersionSha256(final String bucketName, final String groupId, final String artifactId,
+                                                final String version) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
+        final String sha256Hex = bundleVersion.getVersionMetadata().getSha256();
+        return sha256Hex;
+    }
+
+    @Override
+    public List<ExtensionRepoExtensionMetadata> getExtensionRepoExtensions(final URI baseUri, final String bucketName, final String groupId,
+                                                                           final String artifactId, final String version) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
+        final SortedSet<ExtensionMetadata> extensions = extensionService.getExtensionMetadata(bundleVersion);
+
+        final List<ExtensionRepoExtensionMetadata> extensionRepoExtensions = new ArrayList<>(extensions.size());
+        extensions.forEach(e -> extensionRepoExtensions.add(new ExtensionRepoExtensionMetadata(e)));
+        linkService.populateFullLinks(extensionRepoExtensions, baseUri);
+        return extensionRepoExtensions;
+    }
+
+    @Override
+    public Extension getExtensionRepoExtension(final URI baseUri, final String bucketName, final String groupId,
+                                               final String artifactId, final String version, final String extensionName) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
+        final Extension extension = extensionService.getExtension(bundleVersion, extensionName);
+        return extension;
+    }
+
+    @Override
+    public StreamingOutput getExtensionRepoExtensionDocs(final URI baseUri, final String bucketName, final String groupId,
+                                                         final String artifactId, final String version, final String extensionName) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
+        final StreamingOutput streamingOutput = (output) -> extensionService.writeExtensionDocs(bundleVersion, extensionName, output);
+        return streamingOutput;
+    }
+
+    @Override
+    public StreamingOutput getExtensionRepoExtensionAdditionalDocs(final URI baseUri, final String bucketName, final String groupId,
+                                                                   final String artifactId, final String version, final String extensionName) {
+        final Bucket bucket = registryService.getBucketByName(bucketName);
+        authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+        final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version);
+        final StreamingOutput streamingOutput = (output) -> extensionService.writeAdditionalDetailsDocs(bundleVersion, extensionName, output);
+        return streamingOutput;
+    }
+
+
+    // ---------------------- Field methods ----------------------------------------------
+
+    @Override
+    public Set<String> getBucketFields() {
+        return registryService.getBucketFields();
+    }
+
+    @Override
+    public Set<String> getBucketItemFields() {
+        return registryService.getBucketItemFields();
+    }
+
+    @Override
+    public Set<String> getFlowFields() {
+        return registryService.getFlowFields();
+    }
+
+    // ---------------------- User methods ----------------------------------------------
+
+    @Override
+    public User createUser(final User user) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeTenantsAccess(RequestAction.WRITE);
+        validateCreationOfRevisableEntity(user, USER_ENTITY_TYPE);
+
+        user.setIdentifier(UUID.randomUUID().toString());
+        return createRevisableEntity(user, USER_ENTITY_TYPE, currentUserIdentity(), () -> authorizationService.createUser(user));
+    }
+
+    @Override
+    public List<User> getUsers() {
+        verifyAuthorizerIsManaged();
+        authorizeTenantsAccess(RequestAction.READ);
+        return entityService.getEntities(() -> authorizationService.getUsers());
+    }
+
+    @Override
+    public User getUser(final String identifier) {
+        verifyAuthorizerIsManaged();
+        authorizeTenantsAccess(RequestAction.READ);
+        return entityService.get(() -> authorizationService.getUser(identifier));
+    }
+
+    @Override
+    public User updateUser(final User user) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeTenantsAccess(RequestAction.WRITE);
+        validateUpdateOfRevisableEntity(user, USER_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        authorizationService.verifyUserExists(user.getIdentifier());
+
+        return updateRevisableEntity(user, USER_ENTITY_TYPE, currentUserIdentity(), () -> authorizationService.updateUser(user));
+    }
+
+    @Override
+    public User deleteUser(final String identifier, final RevisionInfo revisionInfo) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeTenantsAccess(RequestAction.DELETE);
+        validateDeleteOfRevisableEntity(identifier, revisionInfo, USER_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        authorizationService.verifyUserExists(identifier);
+
+        return deleteRevisableEntity(identifier, USER_ENTITY_TYPE, revisionInfo, () -> authorizationService.deleteUser(identifier));
+    }
+
+    // ---------------------- UserGroup methods ----------------------------------------------
+
+    @Override
+    public UserGroup createUserGroup(final UserGroup userGroup) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeTenantsAccess(RequestAction.WRITE);
+        validateCreationOfRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE);
+
+        userGroup.setIdentifier(UUID.randomUUID().toString());
+        return createRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE, currentUserIdentity(),
+                () -> authorizationService.createUserGroup(userGroup));
+    }
+
+    @Override
+    public List<UserGroup> getUserGroups() {
+        verifyAuthorizerIsManaged();
+        authorizeTenantsAccess(RequestAction.READ);
+        return entityService.getEntities(() -> authorizationService.getUserGroups());
+    }
+
+    @Override
+    public UserGroup getUserGroup(final String identifier) {
+        verifyAuthorizerIsManaged();
+        authorizeTenantsAccess(RequestAction.READ);
+        return entityService.get(() -> authorizationService.getUserGroup(identifier));
+    }
+
+    @Override
+    public UserGroup updateUserGroup(final UserGroup userGroup) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeTenantsAccess(RequestAction.WRITE);
+        validateUpdateOfRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        authorizationService.verifyUserGroupExists(userGroup.getIdentifier());
+
+        return updateRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE, currentUserIdentity(),
+                () -> authorizationService.updateUserGroup(userGroup));
+    }
+
+    @Override
+    public UserGroup deleteUserGroup(final String identifier, final RevisionInfo revisionInfo) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeTenantsAccess(RequestAction.DELETE);
+        validateDeleteOfRevisableEntity(identifier, revisionInfo, USER_GROUP_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        authorizationService.verifyUserGroupExists(identifier);
+
+        return deleteRevisableEntity(identifier, USER_GROUP_ENTITY_TYPE, revisionInfo,
+                () -> authorizationService.deleteUserGroup(identifier));
+    }
+
+    // ---------------------- AccessPolicy methods ----------------------------------------------
+
+    @Override
+    public AccessPolicy createAccessPolicy(final AccessPolicy accessPolicy) {
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizePoliciesAccess(RequestAction.WRITE);
+        validateCreationOfRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE);
+
+        accessPolicy.setIdentifier(UUID.randomUUID().toString());
+        return createRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE, currentUserIdentity(),
+                () -> authorizationService.createAccessPolicy(accessPolicy));
+    }
+
+    @Override
+    public AccessPolicy getAccessPolicy(final String identifier) {
+        verifyAuthorizerIsManaged();
+        authorizePoliciesAccess(RequestAction.READ);
+        return entityService.get(() -> authorizationService.getAccessPolicy(identifier));
+    }
+
+    @Override
+    public AccessPolicy getAccessPolicy(final String resource, final RequestAction action) {
+        verifyAuthorizerIsManaged();
+        authorizePoliciesAccess(RequestAction.READ);
+        return entityService.get(() -> authorizationService.getAccessPolicy(resource, action));
+    }
+
+    @Override
+    public List<AccessPolicy> getAccessPolicies() {
+        verifyAuthorizerIsManaged();
+        authorizePoliciesAccess(RequestAction.READ);
+        return entityService.getEntities(() -> authorizationService.getAccessPolicies());
+    }
+
+    @Override
+    public AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) {
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizePoliciesAccess(RequestAction.WRITE);
+        validateUpdateOfRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        authorizationService.verifyAccessPolicyExists(accessPolicy.getIdentifier());
+
+        return updateRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE, currentUserIdentity(),
+                () -> authorizationService.updateAccessPolicy(accessPolicy));
+    }
+
+    @Override
+    public AccessPolicy deleteAccessPolicy(final String identifier, final RevisionInfo revisionInfo) {
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizePoliciesAccess(RequestAction.DELETE);
+        validateDeleteOfRevisableEntity(identifier, revisionInfo, ACCESS_POLICY_ENTITY_TYPE);
+
+        // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException
+        authorizationService.verifyAccessPolicyExists(identifier);
+
+        return deleteRevisableEntity(identifier, ACCESS_POLICY_ENTITY_TYPE, revisionInfo,
+                () -> authorizationService.deleteAccessPolicy(identifier));
+    }
+
+    @Override
+    public List<Resource> getResources() {
+        authorizePoliciesAccess(RequestAction.READ);
+        return authorizationService.getResources();
+    }
+
+    // ---------------------- Permission methods -----------------------------
+
+    @Override
+    public CurrentUser getCurrentUser() {
+        return authorizationService.getCurrentUser();
+    }
+
+
+    // ---------------------- Authorization methods -----------------------------
+
+    private void verifyAuthorizerIsManaged() {
+        authorizationService.verifyAuthorizerIsManaged();
+    }
+
+    private void verifyAuthorizerSupportsConfigurablePolicies() {
+        authorizationService.verifyAuthorizerSupportsConfigurablePolicies();
+    }
+
+    private void verifyAuthorizerSupportsConfigurableUserGroups() {
+        authorizationService.verifyAuthorizerSupportsConfigurableUserGroups();
+    }
+
+    // ---------------------- Configuration methods -----------------------------
+
+    @Override
+    public RegistryConfiguration getRegistryConfiguration() {
+        final RegistryConfiguration config = new RegistryConfiguration();
+
+        boolean hasAnyConfigurationAccess = false;
+        AccessDeniedException lastAccessDeniedException = null;
+        try {
+            final Authorizable policyAuthorizer = authorizableLookup.getPoliciesAuthorizable();
+            authorizationService.authorize(policyAuthorizer, RequestAction.READ);
+            config.setSupportsManagedAuthorizer(authorizationService.isManagedAuthorizer());
+            config.setSupportsConfigurableAuthorizer(authorizationService.isConfigurableAccessPolicyProvider());
+            hasAnyConfigurationAccess = true;
+        } catch (AccessDeniedException e) {
+            lastAccessDeniedException = e;
+        }
+
+        try {
+            authorizationService.authorize(authorizableLookup.getTenantsAuthorizable(), RequestAction.READ);
+            config.setSupportsConfigurableUsersAndGroups(authorizationService.isConfigurableUserGroupProvider());
+            hasAnyConfigurationAccess = true;
+        } catch (AccessDeniedException e) {
+            lastAccessDeniedException = e;
+        }
+
+        if (!hasAnyConfigurationAccess) {
+            // If the user doesn't have access to any configuration, then throw the exception.
+            // Otherwise, return what they can access.
+            throw lastAccessDeniedException;
+        }
+
+        return config;
+    }
+
+    // ---------------------- Helper methods -------------------------------------
+
+    private void authorizeBucketsAccess(RequestAction actionType) throws AccessDeniedException {
+        final Authorizable bucketsAuthorizable = authorizableLookup.getBucketsAuthorizable();
+        authorizationService.authorize(bucketsAuthorizable, actionType);
+    }
+
+    private void authorizeBucketAccess(final RequestAction actionType, final String bucketIdentifier) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketIdentifier);
+        authorizationService.authorize(bucketAuthorizable, actionType);
+    }
+
+    private void authorizeBucketAccess(final RequestAction action, final Bundle bundle) {
+        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the bundle isn't returned
+        if (bundle == null) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(action, bundle.getBucketIdentifier());
+    }
+
+    private void authorizeBucketAccess(final RequestAction action, final VersionedFlow flow) {
+        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow isn't returned
+        if (flow == null) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(action, flow.getBucketIdentifier());
+    }
+
+    private void authorizeBucketAccess(final RequestAction action, final VersionedFlowSnapshot flowSnapshot) {
+        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow snapshot isn't returned
+        if (flowSnapshot == null || flowSnapshot.getSnapshotMetadata() == null) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(action, flowSnapshot.getSnapshotMetadata().getBucketIdentifier());
+    }
+
+    private void authorizeBucketAccess(final RequestAction action, final VersionedFlowSnapshotMetadata flowSnapshotMetadata) {
+        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow snapshot isn't returned
+        if (flowSnapshotMetadata == null) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(action, flowSnapshotMetadata.getBucketIdentifier());
+    }
+
+    private Set<String> getAuthorizedBucketIds(final RequestAction actionType) {
+        return authorizationService.getAuthorizedResources(actionType, ResourceType.Bucket)
+                .stream()
+                .map(StandardServiceFacade::extractBucketIdFromResource)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toSet());
+    }
+
+    private static String extractBucketIdFromResource(Resource resource) {
+        if (resource == null || resource.getIdentifier() == null || !resource.getIdentifier().startsWith("/buckets/")) {
+            return null;
+        }
+
+        final String[] pathComponents = resource.getIdentifier().split("/");
+        if (pathComponents.length < 3) {
+            return null;
+        }
+        return pathComponents[2];
+    }
+
+    private String generateResourceUri(final URI baseUri, final String... path) {
+        final URI fullUri = UriBuilder.fromUri(baseUri).segment(path).build();
+        return fullUri.toString();
+    }
+
+    private void authorizePoliciesAccess(final RequestAction actionType) {
+        final Authorizable policiesAuthorizable = authorizableLookup.getPoliciesAuthorizable();
+        authorizationService.authorize(policiesAuthorizable, actionType);
+    }
+
+    private void authorizeTenantsAccess(RequestAction actionType) {
+        final Authorizable tenantsAuthorizable = authorizableLookup.getTenantsAuthorizable();
+        authorizationService.authorize(tenantsAuthorizable, actionType);
+    }
+
+    private void validateCreationOfRevisableEntity(final RevisableEntity entity, final String entityTypeName) {
+        if (entity == null) {
+            throw new IllegalArgumentException(entityTypeName + " cannot be null");
+        }
+        if (entity.getIdentifier() != null) {
+            throw new IllegalArgumentException(entityTypeName + " identifier cannot be specified when creating a new "
+                    + entityTypeName.toLowerCase() + ".");
+        }
+
+        if (entity.getRevision() == null
+                || entity.getRevision().getVersion() == null
+                || entity.getRevision().getVersion().longValue() != 0) {
+            throw new IllegalArgumentException("A revision of 0 must be specified when creating a new " + entityTypeName + ".");
+        }
+    }
+
+    private void validateUpdateOfRevisableEntity(final RevisableEntity entity, final String entityTypeName) {
+        if (entity == null) {
+            throw new IllegalArgumentException(entityTypeName + " cannot be null");
+        }
+
+        if (entity.getRevision() == null || entity.getRevision().getVersion() == null) {
+            throw new IllegalArgumentException("Revision info must be specified.");
+        }
+    }
+
+    private void validateDeleteOfRevisableEntity(final String identifier, final RevisionInfo revision, final String entityTypeName) {
+        if (identifier == null || identifier.trim().isEmpty()) {
+            throw new IllegalArgumentException(entityTypeName + " identifier is required");
+        }
+
+        if (revision == null || revision.getVersion() == null) {
+            throw new IllegalArgumentException("Revision info must be specified.");
+        }
+    }
+
+    private <T extends RevisableEntity> T createRevisableEntity(final T requestEntity, final String entityTypeName,
+                                                                final String creatorIdentity, final Supplier<T> createEntity) {
+        try {
+            return entityService.create(requestEntity, creatorIdentity, createEntity);
+        } catch (InvalidRevisionException e) {
+            final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "create", requestEntity.getIdentifier());
+            throw new InvalidRevisionException(msg, e);
+        }
+    }
+
+    private <T extends RevisableEntity> T updateRevisableEntity(final T requestEntity, final String entityTypeName,
+                                                                final String updaterIdentity, final Supplier<T> updateEntity) {
+        try {
+            return entityService.update(requestEntity, updaterIdentity, updateEntity);
+        } catch (InvalidRevisionException e) {
+            final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "update", requestEntity.getIdentifier());
+            throw new InvalidRevisionException(msg, e);
+        }
+    }
+
+    private <T extends RevisableEntity> T deleteRevisableEntity(final String entityIdentifier, final String entityTypeName,
+                                                 final RevisionInfo revisionInfo, final Supplier<T> deleteEntity) {
+        try {
+            return entityService.delete(entityIdentifier, revisionInfo, deleteEntity);
+        } catch (InvalidRevisionException e) {
+            final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "delete", entityIdentifier);
+            throw new InvalidRevisionException(msg, e);
+        }
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StreamingContent.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StreamingContent.java
new file mode 100644
index 0000000..ed4d3e8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StreamingContent.java
@@ -0,0 +1,39 @@
+/*
+ * 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.nifi.registry.web.service;
+
+import javax.ws.rs.core.StreamingOutput;
+
+public class StreamingContent {
+
+    private final StreamingOutput output;
+
+    private final String filename;
+
+    public StreamingContent(final StreamingOutput output, final String filename) {
+        this.output = output;
+        this.filename = filename;
+    }
+
+    public StreamingOutput getOutput() {
+        return output;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java
index 7edd73e..2892d03 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.registry.web.api;
 
 import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.junit.Test;
 import org.skyscreamer.jsonassert.JSONAssert;
 import org.springframework.test.annotation.IfProfileValue;
@@ -26,6 +27,8 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import java.util.UUID;
+
 import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertBucketsEqual;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -105,6 +108,8 @@
 
     @Test
     public void testCreateBucketGetBucket() throws Exception {
+        final String clientId = UUID.randomUUID().toString();
+        final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L);
 
         // Given:
 
@@ -112,6 +117,7 @@
         final Bucket bucket = new Bucket();
         bucket.setName("Integration Test Bucket");
         bucket.setDescription("A bucket created by an integration test.");
+        bucket.setRevision(initialRevision);
 
         // When: a bucket is created on the server
 
@@ -127,6 +133,9 @@
         assertTrue(createdBucket.getCreatedTimestamp() - testStartTime > 0L); // both server and client in same JVM, so there shouldn't be skew
         assertNotNull(createdBucket.getLink());
         assertNotNull(createdBucket.getLink().getUri());
+        assertNotNull(createdBucket.getRevision());
+        assertEquals(initialRevision.getVersion() + 1, createdBucket.getRevision().getVersion().longValue());
+        assertEquals(initialRevision.getClientId(), createdBucket.getRevision().getClientId());
 
         // And when /buckets is queried, then the newly created bucket is returned in the list
 
@@ -153,16 +162,23 @@
                 .request()
                 .get(Bucket.class);
         assertBucketsEqual(createdBucket, bucketById, true);
+        assertNotNull(bucketById.getRevision());
+        assertEquals(initialRevision.getVersion() + 1, bucketById.getRevision().getVersion().longValue());
+        assertEquals(initialRevision.getClientId(), bucketById.getRevision().getClientId());
     }
 
     @Test
     public void testUpdateBucket() throws Exception {
+        final String clientId = UUID.randomUUID().toString();
+        final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L);
 
         // Given: a bucket exists on the server
 
         final Bucket bucket = new Bucket();
         bucket.setName("Integration Test Bucket");
         bucket.setDescription("A bucket created by an integration test.");
+        bucket.setRevision(initialRevision);
+
         Bucket createdBucket = client
                 .target(createURL("buckets"))
                 .request()
@@ -181,17 +197,55 @@
         // Then: the server returns the updated bucket
 
         assertBucketsEqual(createdBucket, updatedBucket, true);
+    }
 
+    @Test
+    public void testUpdateBucketWithIncorrectRevision() throws Exception {
+        final String clientId = UUID.randomUUID().toString();
+        final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L);
+
+        // Given: a bucket exists on the server
+
+        final Bucket bucket = new Bucket();
+        bucket.setName("Integration Test Bucket");
+        bucket.setDescription("A bucket created by an integration test.");
+        bucket.setRevision(initialRevision);
+
+        Bucket createdBucket = client
+                .target(createURL("buckets"))
+                .request()
+                .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        // When: the bucket is modified by the client and updated on the server
+
+        createdBucket.setName("Renamed Bucket");
+        createdBucket.setDescription("This bucket has been updated by an integration test.");
+
+        // Change version to incorrect number and don't send a client id
+        createdBucket.getRevision().setClientId(null);
+        createdBucket.getRevision().setVersion(99L);
+
+        final Response response = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .request()
+                .put(Entity.entity(createdBucket, MediaType.APPLICATION_JSON));
+
+        // Then: we get a bad request for sending a wrong revision
+
+        assertEquals(400, response.getStatus());
     }
 
     @Test
     public void testDeleteBucket() throws Exception {
+        final String clientId = UUID.randomUUID().toString();
+        final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L);
 
         // Given: a bucket has been created
 
         final Bucket bucket = new Bucket();
         bucket.setName("Integration Test Bucket");
         bucket.setDescription("A bucket created by an integration test.");
+        bucket.setRevision(initialRevision);
 
         Bucket createdBucket = client
                 .target(createURL("buckets"))
@@ -202,6 +256,7 @@
 
         final Bucket deletedBucket = client
                 .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .queryParam("version", createdBucket.getRevision().getVersion().longValue())
                 .request()
                 .delete(Bucket.class);
 
@@ -220,6 +275,36 @@
     }
 
     @Test
+    public void testDeleteBucketWithIncorrectRevision() throws Exception {
+        final String clientId = UUID.randomUUID().toString();
+        final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L);
+
+        // Given: a bucket has been created
+
+        final Bucket bucket = new Bucket();
+        bucket.setName("Integration Test Bucket");
+        bucket.setDescription("A bucket created by an integration test.");
+        bucket.setRevision(initialRevision);
+
+        Bucket createdBucket = client
+                .target(createURL("buckets"))
+                .request()
+                .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        // When: that bucket deleted
+
+        final Response response = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .queryParam("version", 99L)
+                .request()
+                .delete();
+
+        // Then: we get a bad request for sending the wrong revision version
+
+        assertEquals(400, response.getStatus());
+    }
+
+    @Test
     public void getBucketFields() throws Exception {
 
         // Given: the server is configured to return this fixed response
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/DBFlowStorageIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/DBFlowStorageIT.java
index b438eaa..5afccc3 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/DBFlowStorageIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/DBFlowStorageIT.java
@@ -26,6 +26,7 @@
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -82,16 +83,20 @@
 
     @Test
     public void testAll() throws IOException, NiFiRegistryException {
+        final RevisionInfo initialRevision = new RevisionInfo("DBFlowStorageIT", 0L);
+
         // Create two buckets...
 
         final Bucket b1 = new Bucket();
         b1.setName("b1");
+        b1.setRevision(initialRevision);
 
         final Bucket createdB1 = client.getBucketClient().create(b1);
         assertNotNull(createdB1);
 
         final Bucket b2 = new Bucket();
         b2.setName("b2");
+        b2.setRevision(initialRevision);
 
         final Bucket createdB2 = client.getBucketClient().create(b2);
         assertNotNull(createdB2);
@@ -101,6 +106,7 @@
         final VersionedFlow f1 = new VersionedFlow();
         f1.setName("f1");
         f1.setBucketIdentifier(createdB1.getIdentifier());
+        f1.setRevision(initialRevision);
 
         final VersionedFlow createdF1 = client.getFlowClient().create(f1);
         assertNotNull(createdF1);
@@ -108,6 +114,7 @@
         final VersionedFlow f2 = new VersionedFlow();
         f2.setName("f2");
         f2.setBucketIdentifier(createdB2.getIdentifier());
+        f2.setRevision(initialRevision);
 
         final VersionedFlow createdF2 = client.getFlowClient().create(f2);
         assertNotNull(createdF2);
@@ -144,7 +151,7 @@
 
         // Verify deleting a flow...
 
-        client.getFlowClient().delete(createdB1.getIdentifier(), createdF1.getIdentifier());
+        client.getFlowClient().delete(createdB1.getIdentifier(), createdF1.getIdentifier(), createdF1.getRevision());
 
         // All versions of f1 should be deleted
         try {
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
index 5770a15..4471494 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
@@ -21,6 +21,7 @@
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.junit.Assert;
 import org.junit.Test;
 import org.skyscreamer.jsonassert.JSONAssert;
@@ -108,6 +109,7 @@
 
     @Test
     public void testCreateFlowGetFlow() throws Exception {
+        final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L);
 
         // Given: an empty bucket with id "3" (see FlowsIT.sql)
 
@@ -120,6 +122,7 @@
         flow.setBucketIdentifier(bucketId);
         flow.setName("Test Flow");
         flow.setDescription("This is a flow created by an integration test.");
+        flow.setRevision(initialRevision);
 
         final VersionedFlow createdFlow = client
                 .target(createURL("buckets/{bucketId}/flows"))
@@ -138,6 +141,9 @@
         assertEquals(createdFlow.getCreatedTimestamp(), createdFlow.getModifiedTimestamp());
         assertNotNull(createdFlow.getLink());
         assertNotNull(createdFlow.getLink().getUri());
+        assertNotNull(createdFlow.getRevision());
+        assertEquals(initialRevision.getClientId(), createdFlow.getRevision().getClientId());
+        assertEquals(initialRevision.getVersion() + 1, createdFlow.getRevision().getVersion().longValue());
 
         // And when .../flows is queried, then the newly created flow is returned in the list
 
@@ -167,11 +173,15 @@
                 .request()
                 .get(VersionedFlow.class);
         assertFlowsEqual(createdFlow, flowById, true);
+        assertNotNull(flowById.getRevision());
+        assertEquals(initialRevision.getClientId(), flowById.getRevision().getClientId());
+        assertEquals(initialRevision.getVersion() + 1, flowById.getRevision().getVersion().longValue());
 
     }
 
     @Test
     public void testUpdateFlow() throws Exception {
+        final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L);
 
         // Given: a flow exists on the server
 
@@ -180,6 +190,8 @@
         flow.setBucketIdentifier(bucketId);
         flow.setName("Test Flow");
         flow.setDescription("This is a flow created by an integration test.");
+        flow.setRevision(initialRevision);
+
         final VersionedFlow createdFlow = client
                 .target(createURL("buckets/{bucketId}/flows"))
                 .resolveTemplate("bucketId", bucketId)
@@ -203,11 +215,11 @@
         assertTrue(updatedFlow.getModifiedTimestamp() > createdFlow.getModifiedTimestamp());
         createdFlow.setModifiedTimestamp(updatedFlow.getModifiedTimestamp());
         assertFlowsEqual(createdFlow, updatedFlow, true);
-
     }
 
     @Test
-    public void testDeleteBucket() throws Exception {
+    public void testUpdateFlowWithIncorrectRevision() throws Exception {
+        final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L);
 
         // Given: a flow exists on the server
 
@@ -216,6 +228,47 @@
         flow.setBucketIdentifier(bucketId);
         flow.setName("Test Flow");
         flow.setDescription("This is a flow created by an integration test.");
+        flow.setRevision(initialRevision);
+
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        // When: the flow revision has no clientId and the incorrect version
+
+        createdFlow.setName("Renamed Flow");
+        createdFlow.setDescription("This flow has been updated by an integration test.");
+        createdFlow.getRevision().setClientId(null);
+        createdFlow.getRevision().setVersion(99L);
+
+        final Response response = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .put(Entity.entity(createdFlow, MediaType.APPLICATION_JSON));
+
+        // Then: 400 bad request because of the incorrect version sent
+
+        assertEquals(400, response.getStatus());
+
+    }
+
+    @Test
+    public void testDeleteFlow() throws Exception {
+        final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L);
+
+        // Given: a flow exists on the server
+
+        final String bucketId = "3";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+        flow.setRevision(initialRevision);
+
         final VersionedFlow createdFlow = client
                 .target(createURL("buckets/{bucketId}/flows"))
                 .resolveTemplate("bucketId", bucketId)
@@ -228,6 +281,7 @@
                 .target(createURL("buckets/{bucketId}/flows/{flowId}"))
                 .resolveTemplate("bucketId", bucketId)
                 .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .queryParam("version", createdFlow.getRevision().getVersion().longValue())
                 .request()
                 .delete(VersionedFlow.class);
 
@@ -248,6 +302,40 @@
     }
 
     @Test
+    public void testDeleteFlowWithIncorrectRevision() throws Exception {
+        final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L);
+
+        // Given: a flow exists on the server
+
+        final String bucketId = "3";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+        flow.setRevision(initialRevision);
+
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        // When: the flow is deleted
+
+        final Response response = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .queryParam("version", 99L)
+                .request()
+                .delete();
+
+        // Then: 400 bad request because of the incorrect version sent
+
+        assertEquals(400, response.getStatus());
+    }
+
+    @Test
     public void testGetFlowVersionsEmpty() throws Exception {
 
         // Given: a Bucket "2" containing a flow "3" with no snapshots (see FlowsIT.sql)
@@ -312,6 +400,7 @@
 
     @Test
     public void testCreateFlowVersionGetFlowVersion() throws Exception {
+        final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L);
 
         // Given: an empty Bucket "3" (see FlowsIT.sql) with a newly created flow
 
@@ -321,6 +410,8 @@
         flow.setBucketIdentifier(bucketId);
         flow.setName("Test Flow for creating snapshots");
         flow.setDescription("This is a randomly named flow created by an integration test for the purpose of holding snapshots.");
+        flow.setRevision(initialRevision);
+
         final VersionedFlow createdFlow = client
                 .target(createURL("buckets/{bucketId}/flows"))
                 .resolveTemplate("bucketId", bucketId)
@@ -422,6 +513,7 @@
         flow.setBucketIdentifier(bucketId);
         flow.setName(flowName);
         flow.setDescription("This is a flow created by an integration test.");
+        flow.setRevision(new RevisionInfo("FlowsIT", 0L));
 
         // saving this flow to bucket 3 should work because bucket 3 is empty
 
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
index 095d258..7f81215 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
@@ -21,6 +21,7 @@
 import org.apache.nifi.registry.authorization.Tenant;
 import org.apache.nifi.registry.authorization.User;
 import org.apache.nifi.registry.authorization.UserGroup;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.skyscreamer.jsonassert.JSONAssert;
@@ -33,6 +34,8 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import java.util.UUID;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
@@ -108,8 +111,13 @@
 
         // Given: the server has been configured with FileUserGroupProvider, which is configurable,
         //   and: the initial admin client wants to create a tenant
+        Long initialVersion = new Long(0);
+        String clientId = UUID.randomUUID().toString();
+        RevisionInfo revisionInfo = new RevisionInfo(clientId, initialVersion);
+
         Tenant tenant = new Tenant();
         tenant.setIdentity("New User");
+        tenant.setRevision(revisionInfo);
 
         // When: the POST /tenants/users endpoint is accessed
         final Response createUserResponse = client
@@ -121,6 +129,9 @@
         assertEquals(201, createUserResponse.getStatus());
         User actualUser = createUserResponse.readEntity(User.class);
         assertNotNull(actualUser.getIdentifier());
+        assertNotNull(actualUser.getRevision());
+        assertNotNull(actualUser.getRevision().getVersion());
+
         try {
             assertEquals(tenant.getIdentity(), actualUser.getIdentity());
             assertEquals(true, actualUser.getConfigurable());
@@ -129,7 +140,8 @@
             assertEquals(new ResourcePermissions(), actualUser.getResourcePermissions());
         } finally {
             // cleanup user for other tests
-            client.target(createURL("tenants/users/" + actualUser.getIdentifier()))
+            final long version = actualUser.getRevision().getVersion();
+            client.target(createURL("tenants/users/" + actualUser.getIdentifier() + "?version=" + version))
                     .request()
                     .delete();
         }
@@ -141,8 +153,13 @@
 
         // Given: the server has been configured with FileUserGroupProvider, which is configurable,
         //   and: the initial admin client wants to create a tenant
+        Long initialVersion = new Long(0);
+        String clientId = UUID.randomUUID().toString();
+        RevisionInfo revisionInfo = new RevisionInfo(clientId, initialVersion);
+
         Tenant tenant = new Tenant();
         tenant.setIdentity("New Group");
+        tenant.setRevision(revisionInfo);
 
         // When: the POST /tenants/user-groups endpoint is used
         final Response createUserGroupResponse = client
@@ -154,6 +171,9 @@
         assertEquals(201, createUserGroupResponse.getStatus());
         UserGroup actualUserGroup = createUserGroupResponse.readEntity(UserGroup.class);
         assertNotNull(actualUserGroup.getIdentifier());
+        assertNotNull(actualUserGroup.getRevision());
+        assertNotNull(actualUserGroup.getRevision().getVersion());
+
         try {
             assertEquals(tenant.getIdentity(), actualUserGroup.getIdentity());
             assertEquals(true, actualUserGroup.getConfigurable());
@@ -162,7 +182,8 @@
             assertEquals(new ResourcePermissions(), actualUserGroup.getResourcePermissions());
         } finally {
             // cleanup user for other tests
-            client.target(createURL("tenants/user-groups/" + actualUserGroup.getIdentifier()))
+            final long version = actualUserGroup.getRevision().getVersion();
+            client.target(createURL("tenants/user-groups/" + actualUserGroup.getIdentifier() + "?version=" + version))
                     .request()
                     .delete();
         }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
index c6172a1..4147b3d 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -28,6 +28,7 @@
 import org.apache.nifi.registry.properties.AESSensitivePropertyProvider;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
 import org.apache.nifi.registry.properties.SensitivePropertyProvider;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.AuthorizerFactory;
 import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider;
@@ -38,6 +39,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.skyscreamer.jsonassert.JSONAssert;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.context.TestConfiguration;
@@ -59,6 +62,7 @@
 import java.util.Base64;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -83,6 +87,8 @@
 @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql")
 public class SecureLdapIT extends IntegrationTestBase {
 
+    private static Logger LOGGER = LoggerFactory.getLogger(SecureLdapIT.class);
+
     private static final String tokenLoginPath = "access/token/login";
     private static final String tokenIdentityProviderPath = "access/token/identity-provider";
 
@@ -95,7 +101,10 @@
         @Primary
         @Bean
         @DependsOn({"directoryServer"}) // Can't load LdapUserGroupProvider until the embedded LDAP server, which creates the "directoryServer" bean, is running
-        public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties properties, ExtensionManager extensionManager, RegistryService registryService) throws Exception {
+        public static Authorizer getAuthorizer(
+                @Autowired NiFiRegistryProperties properties,
+                ExtensionManager extensionManager,
+                RegistryService registryService) throws Exception {
             if (authorizerFactory == null) {
                 authorizerFactory = new AuthorizerFactory(properties, extensionManager, sensitivePropertyProvider(), registryService);
             }
@@ -367,11 +376,15 @@
 
     @Test
     public void testCreateTenantFails() throws Exception {
+        Long initialVersion = new Long(0);
+        String clientId = UUID.randomUUID().toString();
+        RevisionInfo initialRevisionInfo = new RevisionInfo(clientId, initialVersion);
 
         // Given: the server has been configured with the LdapUserGroupProvider, which is non-configurable,
         //   and: the client wants to create a tenant
         Tenant tenant = new Tenant();
         tenant.setIdentity("new_tenant");
+        tenant.setRevision(initialRevisionInfo);
 
         // When: the POST /tenants/users endpoint is accessed
         final Response createUserResponse = client
@@ -396,6 +409,9 @@
 
     @Test
     public void testAccessPolicyCreation() throws Exception {
+        Long initialVersion = new Long(0);
+        String clientId = UUID.randomUUID().toString();
+        RevisionInfo initialRevisionInfo = new RevisionInfo(clientId, initialVersion);
 
         // Given: the server has been configured with an initial admin "nifiadmin" and a user with no accessPolicies "nobel"
         String nobelId = getTenantIdentifierByIdentity("nobel");
@@ -425,6 +441,8 @@
         final Bucket bucket = new Bucket();
         bucket.setName("Integration Test Bucket");
         bucket.setDescription("A bucket created by an integration test.");
+        bucket.setRevision(new RevisionInfo(null, 0L));
+
         Response adminCreatesBucketResponse = client
                 .target(createURL("buckets"))
                 .request()
@@ -453,6 +471,8 @@
         readPolicy.setResource("/buckets/" + createdBucket.getIdentifier());
         readPolicy.setAction("read");
         readPolicy.addUserGroups(Arrays.asList(new Tenant(chemistsId, "chemists")));
+        readPolicy.setRevision(initialRevisionInfo);
+
         Response adminGrantsReadAccessResponse = client
                 .target(createURL("policies"))
                 .request()
@@ -496,6 +516,8 @@
         writePolicy.setResource("/buckets/" + createdBucket.getIdentifier());
         writePolicy.setAction("write");
         writePolicy.addUsers(Arrays.asList(new Tenant(nobelId, "nobel")));
+        writePolicy.setRevision(initialRevisionInfo);
+
         Response adminGrantsWriteAccessResponse = client
                 .target(createURL("policies"))
                 .request()
@@ -609,9 +631,8 @@
                 .map(AccessPolicy::getIdentifier)
                 .collect(Collectors.toSet());
 
-        Set<String> policiesToDelete = currentAccessPolicies.stream()
+        Set<AccessPolicy> policiesToDelete = currentAccessPolicies.stream()
                 .filter(p -> !policiesToRestore.contains(p.getIdentifier()))
-                .map(AccessPolicy::getIdentifier)
                 .collect(Collectors.toSet());
 
         for (AccessPolicy originalPolicy : accessPoliciesSnapshot) {
@@ -638,14 +659,17 @@
 
         }
 
-        for (String id : policiesToDelete) {
+        for (AccessPolicy policyToDelete : policiesToDelete) {
             try {
-                client.target(createURL("policies/" + id))
+                final RevisionInfo revisionInfo = policyToDelete.getRevision();
+                final Long version = revisionInfo == null ? 0 : revisionInfo.getVersion();
+                client.target(createURL("policies/" + policyToDelete.getIdentifier()))
+                        .queryParam("version", version.longValue())
                         .request()
                         .header("Authorization", "Bearer " + adminAuthToken)
                         .delete();
             } catch (Exception e) {
-                // do nothing
+                LOGGER.error("Error cleaning up policies after test due to: " + e.getMessage(), e);
             }
         }
 
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
index 62aa098..dfffd7d 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
@@ -32,6 +32,7 @@
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -105,23 +106,28 @@
         final Bucket bucket = new Bucket();
         bucket.setName("Bucket 1 " + System.currentTimeMillis());
         bucket.setDescription("This is bucket 1");
+        bucket.setRevision(new RevisionInfo(null, 0L));
 
         final BucketClient bucketClient = client.getBucketClient();
         final Bucket createdBucket = bucketClient.create(bucket);
         Assert.assertNotNull(createdBucket);
         Assert.assertNotNull(createdBucket.getIdentifier());
+        Assert.assertNotNull(createdBucket.getRevision());
 
         final List<Bucket> buckets = bucketClient.getAll();
         Assert.assertEquals(4, buckets.size());
+        buckets.forEach(b -> Assert.assertNotNull(b.getRevision()));
 
         final VersionedFlow flow = new VersionedFlow();
         flow.setBucketIdentifier(createdBucket.getIdentifier());
         flow.setName("Flow 1 - " + System.currentTimeMillis());
+        flow.setRevision(new RevisionInfo(null, 0L));
 
         final FlowClient flowClient = client.getFlowClient();
         final VersionedFlow createdFlow = flowClient.create(flow);
         Assert.assertNotNull(createdFlow);
         Assert.assertNotNull(createdFlow.getIdentifier());
+        Assert.assertNotNull(createdFlow.getRevision());
 
         final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
         snapshotMetadata.setBucketIdentifier(createdFlow.getBucketIdentifier());
@@ -160,6 +166,7 @@
         final Bucket bucket = new Bucket();
         bucket.setName("Bucket 1");
         bucket.setDescription("This is bucket 1");
+        bucket.setRevision(new RevisionInfo(null, 0L));
 
         try {
             bucketClient.create(bucket);
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/TenantResourceTest.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/TenantResourceTest.java
index 2e3f5fd..99350bb 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/TenantResourceTest.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/TenantResourceTest.java
@@ -20,20 +20,18 @@
 import org.apache.nifi.registry.authorization.UserGroup;
 import org.apache.nifi.registry.event.EventFactory;
 import org.apache.nifi.registry.event.EventService;
-import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
-import org.apache.nifi.registry.security.authorization.Authorizer;
-import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider;
-import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider;
-import org.apache.nifi.registry.security.authorization.ManagedAuthorizer;
-import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
+import org.apache.nifi.registry.revision.web.ClientIdParameter;
+import org.apache.nifi.registry.revision.web.LongParameter;
+import org.apache.nifi.registry.web.service.ServiceFacade;
 import org.junit.Before;
 import org.junit.Test;
 
 import javax.servlet.http.HttpServletRequest;
-
 import java.net.URI;
 import java.net.URISyntaxException;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -42,28 +40,15 @@
 public class TenantResourceTest {
 
     private TenantResource tenantResource;
-    private AuthorizationService authorizationService;
-    private Authorizer authorizer;
-    private ConfigurableAccessPolicyProvider accessPolicyProvider;
     private EventService eventService;
-    private ConfigurableUserGroupProvider userGroupProvider;
-    private AuthorizableLookup authorizableLookup;
+    private ServiceFacade serviceFacade;
 
     @Before
     public void setUp() {
-        authorizationService = mock(AuthorizationService.class);
-        authorizer = mock(ManagedAuthorizer.class);
-        authorizableLookup = mock(AuthorizableLookup.class);
-        accessPolicyProvider = mock(ConfigurableAccessPolicyProvider.class);
-        userGroupProvider = mock(ConfigurableUserGroupProvider.class);
         eventService = mock(EventService.class);
+        serviceFacade = mock(ServiceFacade.class);
 
-        when(accessPolicyProvider.getUserGroupProvider()).thenReturn(userGroupProvider);
-        when(((ManagedAuthorizer) authorizer).getAccessPolicyProvider()).thenReturn(accessPolicyProvider);
-        when(authorizationService.getAuthorizer()).thenReturn(authorizer);
-        when(authorizationService.getAuthorizableLookup()).thenReturn(authorizableLookup);
-
-        tenantResource = new TenantResource(authorizationService, eventService) {
+        tenantResource = new TenantResource(serviceFacade, eventService) {
 
             @Override
             protected URI getBaseUri() {
@@ -79,27 +64,36 @@
     @Test
     public void testCreateUser() {
         HttpServletRequest request = mock(HttpServletRequest.class);
-        User user = new User(null, "identity");
-        User result = new User("identifier", user.getIdentity());
 
-        when(authorizationService.createUser(user)).thenReturn(result);
+        RevisionInfo revisionInfo = new RevisionInfo("client1", 0L);
+        User user = new User(null, "identity");
+        user.setRevision(revisionInfo);
+
+        RevisionInfo resultRevisionInfo = new RevisionInfo("client1", 1L);
+        User result = new User("identifier", user.getIdentity());
+        result.setRevision(resultRevisionInfo);
+
+        when(serviceFacade.createUser(user)).thenReturn(result);
 
         tenantResource.createUser(request, user);
 
-        verify(authorizationService).createUser(user);
+        verify(serviceFacade).createUser(user);
         verify(eventService).publish(eq(EventFactory.userCreated(result)));
     }
 
     @Test
     public void testUpdateUser() {
         HttpServletRequest request = mock(HttpServletRequest.class);
-        User user = new User("identifier", "new-identity");
 
-        when(authorizationService.updateUser(user)).thenReturn(user);
+        RevisionInfo revisionInfo = new RevisionInfo("client1", 1L);
+        User user = new User("identifier", "new-identity");
+        user.setRevision(revisionInfo);
+
+        when(serviceFacade.updateUser(user)).thenReturn(user);
 
         tenantResource.updateUser(request, user.getIdentifier(), user);
 
-        verify(authorizationService).updateUser(user);
+        verify(serviceFacade).updateUser(user);
         verify(eventService).publish(eq(EventFactory.userUpdated(user)));
     }
 
@@ -107,39 +101,50 @@
     public void testDeleteUser() {
         HttpServletRequest request = mock(HttpServletRequest.class);
         User user = new User("identifier", "identity");
+        Long version = new Long(0);
+        ClientIdParameter clientId = new ClientIdParameter();
 
-        when(authorizationService.deleteUser(user.getIdentifier())).thenReturn(user);
+        when(serviceFacade.deleteUser(eq(user.getIdentifier()), any(RevisionInfo.class))).thenReturn(user);
 
-        tenantResource.removeUser(request, user.getIdentifier());
+        tenantResource.removeUser(request, new LongParameter(version.toString()), clientId, user.getIdentifier());
 
-        verify(authorizationService).deleteUser(user.getIdentifier());
+        verify(serviceFacade).deleteUser(eq(user.getIdentifier()), any(RevisionInfo.class));
         verify(eventService).publish(eq(EventFactory.userDeleted(user)));
     }
 
     @Test
     public void testCreateUserGroup() {
         HttpServletRequest request = mock(HttpServletRequest.class);
-        UserGroup userGroup = new UserGroup(null, "identity");
-        UserGroup result = new UserGroup("identifier", userGroup.getIdentity());
 
-        when(authorizationService.createUserGroup(userGroup)).thenReturn(result);
+        RevisionInfo revisionInfo = new RevisionInfo("client1", 0L);
+        UserGroup userGroup = new UserGroup(null, "identity");
+        userGroup.setRevision(revisionInfo);
+
+        RevisionInfo resultRevisionInfo = new RevisionInfo("client1", 1L);
+        UserGroup result = new UserGroup("identifier", userGroup.getIdentity());
+        result.setRevision(resultRevisionInfo);
+
+        when(serviceFacade.createUserGroup(userGroup)).thenReturn(result);
 
         tenantResource.createUserGroup(request, userGroup);
 
-        verify(authorizationService).createUserGroup(userGroup);
+        verify(serviceFacade).createUserGroup(userGroup);
         verify(eventService).publish(eq(EventFactory.userGroupCreated(result)));
     }
 
     @Test
     public void testUpdateUserGroup() {
         HttpServletRequest request = mock(HttpServletRequest.class);
-        UserGroup userGroup = new UserGroup("identifier", "new-identity");
 
-        when(authorizationService.updateUserGroup(userGroup)).thenReturn(userGroup);
+        RevisionInfo revisionInfo = new RevisionInfo("client1", 1L);
+        UserGroup userGroup = new UserGroup("identifier", "new-identity");
+        userGroup.setRevision(revisionInfo);
+
+        when(serviceFacade.updateUserGroup(userGroup)).thenReturn(userGroup);
 
         tenantResource.updateUserGroup(request, userGroup.getIdentifier(), userGroup);
 
-        verify(authorizationService).updateUserGroup(userGroup);
+        verify(serviceFacade).updateUserGroup(userGroup);
         verify(eventService).publish(eq(EventFactory.userGroupUpdated(userGroup)));
     }
 
@@ -147,12 +152,14 @@
     public void testDeleteUserGroup() {
         HttpServletRequest request = mock(HttpServletRequest.class);
         UserGroup userGroup = new UserGroup("identifier", "identity");
+        Long version = new Long(0);
+        ClientIdParameter clientId = new ClientIdParameter();
 
-        when(authorizationService.deleteUserGroup(userGroup.getIdentifier())).thenReturn(userGroup);
+        when(serviceFacade.deleteUserGroup(eq(userGroup.getIdentifier()), any(RevisionInfo.class))).thenReturn(userGroup);
 
-        tenantResource.removeUserGroup(request, userGroup.getIdentifier());
+        tenantResource.removeUserGroup(request, new LongParameter(version.toString()), clientId, userGroup.getIdentifier());
 
-        verify(authorizationService).deleteUserGroup(userGroup.getIdentifier());
+        verify(serviceFacade).deleteUserGroup(eq(userGroup.getIdentifier()), any(RevisionInfo.class));
         verify(eventService).publish(eq(EventFactory.userGroupDeleted(userGroup)));
     }
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
index 0bcf0a2..547f63b 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
@@ -69,6 +69,7 @@
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.VersionedProcessor;
 import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.apache.nifi.registry.util.FileUtils;
 import org.bouncycastle.util.encoders.Hex;
 import org.glassfish.jersey.media.multipart.MultiPartFeature;
@@ -114,10 +115,12 @@
 
     static final Logger LOGGER = LoggerFactory.getLogger(UnsecuredNiFiRegistryClientIT.class);
 
+    static final String CLIENT_ID = "UnsecuredNiFiRegistryClientIT";
+
     private NiFiRegistryClient client;
 
     @Before
-    public void setup() throws IOException {
+    public void setup() {
         final String baseUrl = createBaseURL();
         LOGGER.info("Using base url = " + baseUrl);
 
@@ -189,6 +192,7 @@
         for (final Bucket bucket : createdBuckets) {
             final Bucket retrievedBucket = bucketClient.get(bucket.getIdentifier());
             assertNotNull(retrievedBucket);
+            assertNotNull(retrievedBucket.getRevision());
             assertFalse(retrievedBucket.isAllowBundleRedeploy());
             LOGGER.info("Retrieved bucket " + retrievedBucket.getIdentifier());
         }
@@ -213,6 +217,7 @@
             final Bucket bucketUpdate = new Bucket();
             bucketUpdate.setIdentifier(bucket.getIdentifier());
             bucketUpdate.setDescription(bucket.getDescription() + " UPDATE");
+            bucketUpdate.setRevision(bucket.getRevision());
 
             final Bucket updatedBucket = bucketClient.update(bucketUpdate);
             assertNotNull(updatedBucket);
@@ -251,6 +256,7 @@
         final VersionedFlow flow1Update = new VersionedFlow();
         flow1Update.setIdentifier(flow1.getIdentifier());
         flow1Update.setName(flow1.getName() + " UPDATED");
+        flow1Update.setRevision(retrievedFlow1.getRevision());
 
         final VersionedFlow updatedFlow1 = flowClient.update(flowsBucket.getIdentifier(), flow1Update);
         assertNotNull(updatedFlow1);
@@ -793,11 +799,11 @@
 
         // ---------------------- DELETE DATA --------------------------//
 
-        final VersionedFlow deletedFlow1 = flowClient.delete(flowsBucket.getIdentifier(), flow1.getIdentifier());
+        final VersionedFlow deletedFlow1 = flowClient.delete(flowsBucket.getIdentifier(), updatedFlow1.getIdentifier(), updatedFlow1.getRevision());
         assertNotNull(deletedFlow1);
         LOGGER.info("Deleted flow " + deletedFlow1.getIdentifier());
 
-        final VersionedFlow deletedFlow2 = flowClient.delete(flowsBucket.getIdentifier(), flow2.getIdentifier());
+        final VersionedFlow deletedFlow2 = flowClient.delete(flowsBucket.getIdentifier(), flow2.getIdentifier(), flow2.getRevision());
         assertNotNull(deletedFlow2);
         LOGGER.info("Deleted flow " + deletedFlow2.getIdentifier());
 
@@ -811,7 +817,7 @@
 
         // delete each bucket
         for (final Bucket bucket : createdBuckets) {
-            final Bucket deletedBucket = bucketClient.delete(bucket.getIdentifier());
+            final Bucket deletedBucket = bucketClient.delete(bucket.getIdentifier(), bucket.getRevision());
             assertNotNull(deletedBucket);
             LOGGER.info("Deleted bucket " + deletedBucket.getIdentifier());
         }
@@ -823,9 +829,12 @@
 
     @Test
     public void testFlowSnapshotsWithParameterContextAndEncodingVersion() throws IOException, NiFiRegistryException {
+        final RevisionInfo initialRevision = new RevisionInfo(null, 0L);
+
         // Create a bucket
         final Bucket bucket = new Bucket();
         bucket.setName("Test Bucket");
+        bucket.setRevision(initialRevision);
 
         final Bucket createdBucket = client.getBucketClient().create(bucket);
         assertNotNull(createdBucket);
@@ -834,6 +843,7 @@
         final VersionedFlow flow = new VersionedFlow();
         flow.setName("My Flow");
         flow.setBucketIdentifier(createdBucket.getIdentifier());
+        flow.setRevision(initialRevision);
 
         final VersionedFlow createdFlow = client.getFlowClient().create(flow);
         assertNotNull(createdFlow);
@@ -961,6 +971,7 @@
         final Bucket bucket = new Bucket();
         bucket.setName("Bucket #" + num);
         bucket.setDescription("This is bucket #" + num);
+        bucket.setRevision(new RevisionInfo(CLIENT_ID, 0L));
         return bucketClient.create(bucket);
     }
 
@@ -969,6 +980,7 @@
         versionedFlow.setName(bucket.getName() + " Flow #" + num);
         versionedFlow.setDescription("This is " + bucket.getName() + " flow #" + num);
         versionedFlow.setBucketIdentifier(bucket.getIdentifier());
+        versionedFlow.setRevision(new RevisionInfo(CLIENT_ID, 0L));
         return client.create(versionedFlow);
     }
 
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js
index 0461bb0..c05510f 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js
@@ -18,10 +18,9 @@
 import { TdDataTableService } from '@covalent/core/data-table';
 import NfRegistryApi from 'services/nf-registry.api';
 import { Component } from '@angular/core';
-import { FdsSnackBarService } from '@nifi-fds/core';
+import { FdsDialogService, FdsSnackBarService } from '@nifi-fds/core';
 import NfRegistryService from 'services/nf-registry.service';
 import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
-import $ from 'jquery';
 
 /**
  * NfRegistryAddUserToGroups constructor.
@@ -30,22 +29,23 @@
  * @param tdDataTableService    The covalent data table service module.
  * @param nfRegistryService     The nf-registry.service module.
  * @param matDialogRef          The angular material dialog ref.
+ * @param fdsDialogService      The FDS dialog service.
  * @param fdsSnackBarService    The FDS snack bar service module.
  * @param data                  The data passed into this component.
  * @constructor
  */
-function NfRegistryAddUserToGroups(nfRegistryApi, tdDataTableService, nfRegistryService, matDialogRef, fdsSnackBarService, data) {
+function NfRegistryAddUserToGroups(nfRegistryApi, tdDataTableService, nfRegistryService, matDialogRef, fdsDialogService, fdsSnackBarService, data) {
     //Services
     this.dataTableService = tdDataTableService;
     this.snackBarService = fdsSnackBarService;
+    this.dialogService = fdsDialogService;
     this.nfRegistryService = nfRegistryService;
     this.nfRegistryApi = nfRegistryApi;
     this.dialogRef = matDialogRef;
     this.data = data;
 
     // local state
-    //make an independent copy of the groups for sorting and selecting within the scope of this component
-    this.groups = $.extend(true, [], this.nfRegistryService.groups);
+    this.groups = [];
     this.filteredUserGroups = [];
     this.isAddToSelectedGroupsDisabled = true;
     this.userGroupsSearchTerms = [];
@@ -70,21 +70,35 @@
     ngOnInit: function () {
         var self = this;
 
-        // filter out any groups that
-        // 1) that are not configurable
-        self.groups = self.groups.filter(function (group) {
-            return !!(group.configurable);
-        });
-        // 2) the user already belongs to
-        this.data.user.userGroups.forEach(function (userGroup) {
-            self.groups = self.groups.filter(function (group) {
-                return (group.identifier !== userGroup.identifier);
-            });
-        });
+        // retrieve the fresh list of groups
+        self.nfRegistryApi.getUserGroups().subscribe(function (response) {
+            if (!response.status || response.status === 200) {
+                self.groups = response;
 
-        this.filterGroups();
-        this.deselectAllUserGroups();
-        this.determineAllUserGroupsSelectedState();
+                // filter out any groups that
+                // 1) that are not configurable
+                self.groups = self.groups.filter(function (group) {
+                    return !!(group.configurable);
+                });
+                // 2) the user already belongs to
+                self.data.user.userGroups.forEach(function (userGroup) {
+                    self.groups = self.groups.filter(function (group) {
+                        return (group.identifier !== userGroup.identifier);
+                    });
+                });
+
+                self.filterGroups();
+                self.deselectAllUserGroups();
+                self.determineAllUserGroupsSelectedState();
+            } else {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Close',
+                    acceptButtonColor: 'fds-warn'
+                });
+            }
+        });
     },
 
     /**
@@ -214,17 +228,26 @@
         });
         selectedGroups.forEach(function (selectedGroup) {
             selectedGroup.users.push(self.data.user);
-            self.nfRegistryApi.updateUserGroup(selectedGroup.identifier, selectedGroup.identity, selectedGroup.users).subscribe(function (group) {
+            self.nfRegistryApi.updateUserGroup(selectedGroup.identifier, selectedGroup.identity, selectedGroup.users, selectedGroup.revision).subscribe(function (response) {
                 self.dialogRef.close();
-                self.snackBarService.openCoaster({
-                    title: 'Success',
-                    message: 'User has been added to the ' + group.identity + ' group.',
-                    verticalPosition: 'bottom',
-                    horizontalPosition: 'right',
-                    icon: 'fa fa-check-circle-o',
-                    color: '#1EB475',
-                    duration: 3000
-                });
+                if (!response.status || response.status === 200) {
+                    self.snackBarService.openCoaster({
+                        title: 'Success',
+                        message: 'User has been added to the ' + response.identity + ' group.',
+                        verticalPosition: 'bottom',
+                        horizontalPosition: 'right',
+                        icon: 'fa fa-check-circle-o',
+                        color: '#1EB475',
+                        duration: 3000
+                    });
+                } else {
+                    self.dialogService.openConfirm({
+                        title: 'Error',
+                        message: response.error,
+                        acceptButton: 'Close',
+                        acceptButtonColor: 'fds-warn'
+                    });
+                }
             });
         });
     },
@@ -248,6 +271,7 @@
     TdDataTableService,
     NfRegistryService,
     MatDialogRef,
+    FdsDialogService,
     FdsSnackBarService,
     MAT_DIALOG_DATA
 ];
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js
index 910cdfa..1f9f16c 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js
@@ -21,12 +21,13 @@
     from 'components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups';
 import { of } from 'rxjs';
 import { TdDataTableService } from '@covalent/core/data-table';
-import { FdsSnackBarService } from '@nifi-fds/core';
+import { FdsDialogService, FdsSnackBarService } from '@nifi-fds/core';
 
 describe('NfRegistryAddUserToGroups Component isolated unit tests', function () {
     var comp;
     var nfRegistryService;
     var nfRegistryApi;
+    var dialogService;
     var snackBarService;
     var dataTableService;
 
@@ -37,14 +38,17 @@
         nfRegistryService.groups = [{identifier: 1, identity: 'Group 1', configurable: true, checked: true, users: []}];
 
         nfRegistryApi = new NfRegistryApi();
+        dialogService = new FdsDialogService();
         snackBarService = new FdsSnackBarService();
         dataTableService = new TdDataTableService();
         comp = new NfRegistryAddUserToGroups(nfRegistryApi, dataTableService, nfRegistryService, {
             close: function () {
             }
-        }, snackBarService, {user: nfRegistryService.user});
+        }, dialogService, snackBarService, {user: nfRegistryService.user});
 
         // Spy
+        spyOn(nfRegistryApi, 'getUserGroups').and.callFake(function () {
+        }).and.returnValue(of([{identifier: 1, identity: 'Group 1', configurable: true, checked: true, users: []}]));
         spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
         }).and.returnValue(of({identifier: 1, identity: 'Group 1'}));
         spyOn(nfRegistryApi, 'updateUserGroup').and.callFake(function () {
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js
index b40af3d..4da70f9 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js
@@ -18,10 +18,9 @@
 import { TdDataTableService } from '@covalent/core/data-table';
 import NfRegistryApi from 'services/nf-registry.api';
 import { Component } from '@angular/core';
-import { FdsSnackBarService } from '@nifi-fds/core';
+import { FdsDialogService, FdsSnackBarService } from '@nifi-fds/core';
 import NfRegistryService from 'services/nf-registry.service';
 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
-import $ from 'jquery';
 
 /**
  * NfRegistryAddUsersToGroup constructor.
@@ -31,13 +30,15 @@
  * @param nfRegistryService     The nf-registry.service module.
  * @param matDialogRef          The angular material dialog ref.
  * @param fdsSnackBarService    The FDS snack bar service module.
+ * @param fdsDialogService      The FDS dialog service.
  * @param data                  The data passed into this component.
  * @constructor
  */
-function NfRegistryAddUsersToGroup(nfRegistryApi, tdDataTableService, nfRegistryService, matDialogRef, fdsSnackBarService, data) {
+function NfRegistryAddUsersToGroup(nfRegistryApi, tdDataTableService, nfRegistryService, matDialogRef, fdsDialogService, fdsSnackBarService, data) {
     //  Services
     this.dataTableService = tdDataTableService;
     this.snackBarService = fdsSnackBarService;
+    this.dialogService = fdsDialogService;
     this.nfRegistryService = nfRegistryService;
     this.nfRegistryApi = nfRegistryApi;
     this.dialogRef = matDialogRef;
@@ -45,7 +46,7 @@
 
     // local state
     //make an independent copy of the users for sorting and selecting within the scope of this component
-    this.users = $.extend(true, [], this.nfRegistryService.users);
+    this.users = [];
     this.filteredUsers = [];
     this.isAddSelectedUsersToGroupDisabled = true;
     this.usersSearchTerms = [];
@@ -70,15 +71,29 @@
     ngOnInit: function () {
         var self = this;
 
-        this.data.group.users.forEach(function (groupUser) {
-            self.users = self.users.filter(function (user) {
-                return (user.identifier !== groupUser.identifier);
-            });
-        });
+        // retrieve the fresh list of users
+        self.nfRegistryApi.getUsers().subscribe(function (response) {
+            if (!response.status || response.status === 200) {
+                self.users = response;
 
-        this.filterUsers();
-        this.deselectAllUsers();
-        this.determineAllUsersSelectedState();
+                self.data.group.users.forEach(function (groupUser) {
+                    self.users = self.users.filter(function (user) {
+                        return (user.identifier !== groupUser.identifier);
+                    });
+                });
+
+                self.filterUsers();
+                self.deselectAllUsers();
+                self.determineAllUsersSelectedState();
+            } else {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Close',
+                    acceptButtonColor: 'fds-warn'
+                });
+            }
+        });
     },
 
     /**
@@ -208,17 +223,26 @@
         }).forEach(function (filteredUser) {
             self.data.group.users.push(filteredUser);
         });
-        this.nfRegistryApi.updateUserGroup(self.data.group.identifier, self.data.group.identity, self.data.group.users).subscribe(function (group) {
+        this.nfRegistryApi.updateUserGroup(self.data.group.identifier, self.data.group.identity, self.data.group.users, self.data.group.revision).subscribe(function (response) {
             self.dialogRef.close();
-            self.snackBarService.openCoaster({
-                title: 'Success',
-                message: 'Selected users have been added to the ' + self.data.group.identity + ' group.',
-                verticalPosition: 'bottom',
-                horizontalPosition: 'right',
-                icon: 'fa fa-check-circle-o',
-                color: '#1EB475',
-                duration: 3000
-            });
+            if (!response.status || response.status === 200) {
+                self.snackBarService.openCoaster({
+                    title: 'Success',
+                    message: 'Selected users have been added to the ' + self.data.group.identity + ' group.',
+                    verticalPosition: 'bottom',
+                    horizontalPosition: 'right',
+                    icon: 'fa fa-check-circle-o',
+                    color: '#1EB475',
+                    duration: 3000
+                });
+            } else {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Close',
+                    acceptButtonColor: 'fds-warn'
+                });
+            }
         });
     },
 
@@ -241,6 +265,7 @@
     TdDataTableService,
     NfRegistryService,
     MatDialogRef,
+    FdsDialogService,
     FdsSnackBarService,
     MAT_DIALOG_DATA
 ];
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js
index 0126892..f869f54 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js
@@ -19,13 +19,14 @@
 import NfRegistryService from 'services/nf-registry.service';
 import { of } from 'rxjs';
 import { TdDataTableService } from '@covalent/core/data-table';
-import { FdsSnackBarService } from '@nifi-fds/core';
+import { FdsDialogService, FdsSnackBarService } from '@nifi-fds/core';
 import NfRegistryAddUsersToGroup from 'components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group';
 
 describe('NfRegistryAddUsersToGroup Component isolated unit tests', function () {
     var comp;
     var nfRegistryService;
     var nfRegistryApi;
+    var dialogService;
     var snackBarService;
     var dataTableService;
 
@@ -36,14 +37,17 @@
         nfRegistryService.users = [{identifier: 2, identity: 'User 1', checked: true}];
 
         nfRegistryApi = new NfRegistryApi();
+        dialogService = new FdsDialogService();
         snackBarService = new FdsSnackBarService();
         dataTableService = new TdDataTableService();
         comp = new NfRegistryAddUsersToGroup(nfRegistryApi, dataTableService, nfRegistryService, {
             close: function () {
             }
-        }, snackBarService, {group: nfRegistryService.group});
+        }, dialogService, snackBarService, {group: nfRegistryService.group});
 
         // Spy
+        spyOn(nfRegistryApi, 'getUsers').and.callFake(function () {
+        }).and.returnValue(of([{identifier: 2, identity: 'User 1', checked: true}]));
         spyOn(nfRegistryApi, 'updateUserGroup').and.callFake(function () {
         }).and.returnValue(of({identifier: 1, identity: 'Group 1'}));
         spyOn(comp.dialogRef, 'close');
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js
index 836e186..614f030 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js
@@ -145,7 +145,7 @@
                                     // resource exists, let's update it
                                     policy.userGroups.push(self.nfRegistryService.group);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage buckets privileges updated!!!...now update the view
                                             response.userGroups.forEach(function (group) {
@@ -179,7 +179,7 @@
                                         return (group.identifier !== self.nfRegistryService.group.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage buckets privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
@@ -229,7 +229,7 @@
                                     // resource exists, let's update it
                                     policy.userGroups.push(self.nfRegistryService.group);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage tenants privileges updated!!!...now update the view
                                             response.userGroups.forEach(function (group) {
@@ -263,7 +263,7 @@
                                         return (group.identifier !== self.nfRegistryService.group.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage tenants privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
@@ -313,7 +313,7 @@
                                     // resource exists, let's update it
                                     policy.userGroups.push(self.nfRegistryService.group);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage policies privileges updated!!!...now update the view
                                             response.userGroups.forEach(function (group) {
@@ -347,7 +347,7 @@
                                         return (group.identifier !== self.nfRegistryService.group.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage policies privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
@@ -397,7 +397,7 @@
                                     // resource exists, let's update it
                                     policy.userGroups.push(self.nfRegistryService.group);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage proxy privileges updated!!!...now update the view
                                             response.userGroups.forEach(function (group) {
@@ -431,7 +431,7 @@
                                         return (group.identifier !== self.nfRegistryService.group.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage proxy privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
@@ -461,9 +461,18 @@
         }).afterClosed().subscribe(function () {
             self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier)
                 .subscribe(function (response) {
-                    self.nfRegistryService.group = response;
-                    self.groupname = response.identity;
-                    self.filterUsers();
+                    if (!response.status || response.status === 200) {
+                        self.nfRegistryService.group = response;
+                        self.groupname = response.identity;
+                        self.filterUsers();
+
+                        self.nfRegistryService.groups.filter(function (group) {
+                            return self.nfRegistryService.group.identifier === group.identifier;
+                        }).forEach(function (group) {
+                            group.identity = response.identity;
+                            group.revision = response.revision;
+                        });
+                    }
                 });
         });
     },
@@ -547,21 +556,30 @@
             return u.identifier !== user.identifier;
         });
 
-        this.nfRegistryApi.updateUserGroup(this.nfRegistryService.group.identifier, this.nfRegistryService.group.identity, users).subscribe(function (response) {
+        this.nfRegistryApi.updateUserGroup(this.nfRegistryService.group.identifier, this.nfRegistryService.group.identity, users, this.nfRegistryService.group.revision).subscribe(function (response) {
             self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier)
                 .subscribe(function (response) {
                     self.nfRegistryService.group = response;
                     self.filterUsers();
                 });
-            self.snackBarService.openCoaster({
-                title: 'Success',
-                message: 'The user has been removed from the ' + self.nfRegistryService.group.identity + ' group.',
-                verticalPosition: 'bottom',
-                horizontalPosition: 'right',
-                icon: 'fa fa-check-circle-o',
-                color: '#1EB475',
-                duration: 3000
-            });
+            if (!response.status || response.status === 200) {
+                self.snackBarService.openCoaster({
+                    title: 'Success',
+                    message: 'The user has been removed from the ' + self.nfRegistryService.group.identity + ' group.',
+                    verticalPosition: 'bottom',
+                    horizontalPosition: 'right',
+                    icon: 'fa fa-check-circle-o',
+                    color: '#1EB475',
+                    duration: 3000
+                });
+            } else {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                });
+            }
         });
     },
 
@@ -572,13 +590,14 @@
      */
     updateGroupName: function (groupname) {
         var self = this;
-        this.nfRegistryApi.updateUserGroup(this.nfRegistryService.group.identifier, groupname, this.nfRegistryService.group.users).subscribe(function (response) {
+        this.nfRegistryApi.updateUserGroup(this.nfRegistryService.group.identifier, groupname, this.nfRegistryService.group.users, this.nfRegistryService.group.revision).subscribe(function (response) {
             if (!response.status || response.status === 200) {
                 self.nfRegistryService.group = response;
                 self.nfRegistryService.groups.filter(function (group) {
                     return self.nfRegistryService.group.identifier === group.identifier;
                 }).forEach(function (group) {
                     group.identity = response.identity;
+                    group.revision = response.revision;
                 });
                 self.snackBarService.openCoaster({
                     title: 'Success',
@@ -597,6 +616,32 @@
                     acceptButton: 'Ok',
                     acceptButtonColor: 'fds-warn'
                 });
+            } else if (response.status === 404) {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                });
+            } else {
+                self.groupname = self.nfRegistryService.group.identity;
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                }).afterClosed().subscribe(function (accept) {
+                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier)
+                        .subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.nfRegistryService.group = response;
+                                self.groupname = response.identity;
+                                self.filterUsers();
+                            } else if (response.status === 404) {
+                                self.router.navigateByUrl('administration/users');
+                            }
+                        });
+                });
             }
         });
     },
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.js
index ae8b565..c90bef2 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.js
@@ -145,7 +145,7 @@
                                     // resource exists, let's update it
                                     policy.users.push(self.nfRegistryService.user);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage buckets privileges updated!!!...now update the view
                                             response.users.forEach(function (user) {
@@ -179,7 +179,7 @@
                                         return (user.identifier !== self.nfRegistryService.user.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage buckets privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUser(self.nfRegistryService.user.identifier).subscribe(function (response) {
@@ -229,7 +229,7 @@
                                     // resource exists, let's update it
                                     policy.users.push(self.nfRegistryService.user);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage tenants privileges updated!!!...now update the view
                                             response.users.forEach(function (user) {
@@ -263,7 +263,7 @@
                                         return (user.identifier !== self.nfRegistryService.user.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage tenants privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUser(self.nfRegistryService.user.identifier).subscribe(function (response) {
@@ -313,7 +313,7 @@
                                     // resource exists, let's update it
                                     policy.users.push(self.nfRegistryService.user);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage policies privileges updated!!!...now update the view
                                             response.users.forEach(function (user) {
@@ -347,7 +347,7 @@
                                         return (user.identifier !== self.nfRegistryService.user.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage policies privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUser(self.nfRegistryService.user.identifier).subscribe(function (response) {
@@ -397,7 +397,7 @@
                                     // resource exists, let's update it
                                     policy.users.push(self.nfRegistryService.user);
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // can manage proxy privileges updated!!!...now update the view
                                             response.users.forEach(function (user) {
@@ -431,7 +431,7 @@
                                         return (user.identifier !== self.nfRegistryService.user.identifier);
                                     });
                                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                         function (response) {
                                             // administrator privileges updated!!!...now update the view
                                             self.nfRegistryApi.getUser(self.nfRegistryService.user.identifier).subscribe(function (response) {
@@ -462,9 +462,18 @@
         }).afterClosed().subscribe(function () {
             self.nfRegistryApi.getUser(self.nfRegistryService.user.identifier)
                 .subscribe(function (response) {
-                    self.nfRegistryService.user = response;
-                    self.username = response.identity;
-                    self.filterGroups(this.sortBy, this.sortOrder);
+                    if (!response.status || response.status === 200) {
+                        self.nfRegistryService.user = response;
+                        self.username = response.identity;
+                        self.filterGroups(this.sortBy, this.sortOrder);
+
+                        self.nfRegistryService.users.filter(function (user) {
+                            return self.nfRegistryService.user.identifier === user.identifier;
+                        }).forEach(function (user) {
+                            user.identity = response.identity;
+                            user.revision = response.revision;
+                        });
+                    }
                 });
         });
     },
@@ -550,21 +559,32 @@
                 var users = fullGroup.users.filter(function (user) {
                     return self.nfRegistryService.user.identifier !== user.identifier;
                 });
-                self.nfRegistryApi.updateUserGroup(fullGroup.identifier, fullGroup.identity, users).subscribe(function (response) {
+                self.nfRegistryApi.updateUserGroup(fullGroup.identifier, fullGroup.identity, users, fullGroup.revision).subscribe(function (response) {
                     self.nfRegistryApi.getUser(self.nfRegistryService.user.identifier)
                         .subscribe(function (response) {
                             self.nfRegistryService.user = response;
+                            self.username = response.identity;
                             self.filterGroups(this.sortBy, this.sortOrder);
                         });
-                    self.snackBarService.openCoaster({
-                        title: 'Success',
-                        message: 'This user has been removed from the ' + group.identity + ' group.',
-                        verticalPosition: 'bottom',
-                        horizontalPosition: 'right',
-                        icon: 'fa fa-check-circle-o',
-                        color: '#1EB475',
-                        duration: 3000
-                    });
+
+                    if (!response.status || response.status === 200) {
+                        self.snackBarService.openCoaster({
+                            title: 'Success',
+                            message: 'This user has been removed from the ' + group.identity + ' group.',
+                            verticalPosition: 'bottom',
+                            horizontalPosition: 'right',
+                            icon: 'fa fa-check-circle-o',
+                            color: '#1EB475',
+                            duration: 3000
+                        });
+                    } else {
+                        self.dialogService.openConfirm({
+                            title: 'Error',
+                            message: response.error,
+                            acceptButton: 'Ok',
+                            acceptButtonColor: 'fds-warn'
+                        });
+                    }
                 });
             }
         });
@@ -577,13 +597,14 @@
      */
     updateUserName: function (username) {
         var self = this;
-        this.nfRegistryApi.updateUser(this.nfRegistryService.user.identifier, username).subscribe(function (response) {
+        this.nfRegistryApi.updateUser(this.nfRegistryService.user.identifier, username, this.nfRegistryService.user.revision).subscribe(function (response) {
             if (!response.status || response.status === 200) {
                 self.nfRegistryService.user = response;
                 self.nfRegistryService.users.filter(function (user) {
                     return self.nfRegistryService.user.identifier === user.identifier;
                 }).forEach(function (user) {
                     user.identity = response.identity;
+                    user.revision = response.revision;
                 });
                 self.snackBarService.openCoaster({
                     title: 'Success',
@@ -602,6 +623,32 @@
                     acceptButton: 'Ok',
                     acceptButtonColor: 'fds-warn'
                 });
+            } else if (response.status === 404) {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                });
+            } else {
+                self.username = self.nfRegistryService.user.identity;
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                }).afterClosed().subscribe(function (accept) {
+                    self.nfRegistryApi.getUser(self.nfRegistryService.user.identifier)
+                        .subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.nfRegistryService.user = response;
+                                self.username = response.identity;
+                                self.filterGroups(this.sortBy, this.sortOrder);
+                            } else if (response.status === 404) {
+                                self.router.navigateByUrl('administration/users');
+                            }
+                        });
+                });
             }
         });
     },
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/add-policy-to-bucket/nf-registry-add-policy-to-bucket.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/add-policy-to-bucket/nf-registry-add-policy-to-bucket.js
index a437b08..4edfc0a 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/add-policy-to-bucket/nf-registry-add-policy-to-bucket.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/add-policy-to-bucket/nf-registry-add-policy-to-bucket.js
@@ -200,7 +200,7 @@
                         policy.userGroups.push(self.userOrGroup);
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
@@ -261,7 +261,7 @@
                         policy.userGroups.push(self.userOrGroup);
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
@@ -322,7 +322,7 @@
                         policy.userGroups.push(self.userOrGroup);
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/edit-bucket-policy/nf-registry-edit-bucket-policy.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/edit-bucket-policy/nf-registry-edit-bucket-policy.js
index dfda7b9..206ba7f 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/edit-bucket-policy/nf-registry-edit-bucket-policy.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/edit-bucket-policy/nf-registry-edit-bucket-policy.js
@@ -130,7 +130,7 @@
                         policy.userGroups.push(self.userOrGroup);
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
@@ -158,7 +158,7 @@
                         });
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
@@ -201,7 +201,7 @@
                         policy.userGroups.push(self.userOrGroup);
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
@@ -229,7 +229,7 @@
                         });
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
@@ -272,7 +272,7 @@
                         policy.userGroups.push(self.userOrGroup);
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
@@ -300,7 +300,7 @@
                         });
                     }
                     self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                        policy.resource, policy.users, policy.userGroups).subscribe(
+                        policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                         function (response) {
                             // policy updated!!!...now update the view
                             self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier).subscribe(function (response) {
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js
index f4efc49..0478b4e 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js
@@ -355,7 +355,7 @@
                                     return (group.identity !== userOrGroup.identity);
                                 });
                                 self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
-                                    policy.resource, policy.users, policy.userGroups).subscribe(
+                                    policy.resource, policy.users, policy.userGroups, policy.revision).subscribe(
                                     function (response) {
                                         // policy removed!!!...now update the view
                                         self.nfRegistryApi.getPolicies().subscribe(function (response) {
@@ -412,14 +412,16 @@
         this.nfRegistryApi.updateBucket({
             'identifier': this.nfRegistryService.bucket.identifier,
             'name': bucketname,
+            'revision': this.nfRegistryService.bucket.revision
         }).subscribe(function (response) {
             if (!response.status || response.status === 200) {
                 self.nfRegistryService.bucket = response;
-                // update the bucket identity in the buckets table
+                // update the bucket identity and revision in the buckets table
                 self.nfRegistryService.buckets.filter(function (bucket) {
                     return self.nfRegistryService.bucket.identifier === bucket.identifier;
                 }).forEach(function (bucket) {
                     bucket.name = response.name;
+                    bucket.revision = response.revision;
                 });
                 self.snackBarService.openCoaster({
                     title: 'Success',
@@ -441,17 +443,32 @@
                     acceptButton: 'Ok',
                     acceptButtonColor: 'fds-warn'
                 });
-            } else if (response.status === 400) {
-                self.bucketname = self.nfRegistryService.bucket.name;
-                self.allowBundleRedeploy = self.nfRegistryService.bucket.allowBundleRedeploy;
-                self.allowPublicRead = self.nfRegistryService.bucket.allowPublicRead;
-
+            } else if (response.status === 404) {
                 self.dialogService.openConfirm({
                     title: 'Error',
                     message: response.error,
                     acceptButton: 'Ok',
                     acceptButtonColor: 'fds-warn'
                 });
+            } else {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                }).afterClosed().subscribe(function (accept) {
+                    self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier)
+                        .subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.nfRegistryService.bucket = response;
+                                self.bucketname = self.nfRegistryService.bucket.name;
+                                self.allowBundleRedeploy = self.nfRegistryService.bucket.allowBundleRedeploy;
+                                self.allowPublicRead = self.nfRegistryService.bucket.allowPublicRead;
+                            } else if (response.status === 404) {
+                                self.router.navigateByUrl('administration/workflow');
+                            }
+                        });
+                });
             }
         });
     },
@@ -466,9 +483,16 @@
         this.nfRegistryApi.updateBucket({
             'identifier': this.nfRegistryService.bucket.identifier,
             'allowBundleRedeploy': event.checked,
+            'revision': this.nfRegistryService.bucket.revision
         }).subscribe(function (response) {
             if (!response.status || response.status === 200) {
                 self.nfRegistryService.bucket = response;
+                // update the bucket revision in the buckets table
+                self.nfRegistryService.buckets.filter(function (bucket) {
+                    return self.nfRegistryService.bucket.identifier === bucket.identifier;
+                }).forEach(function (bucket) {
+                    bucket.revision = response.revision;
+                });
                 self.snackBarService.openCoaster({
                     title: 'Success',
                     message: 'Bundle settings have been updated.',
@@ -478,14 +502,32 @@
                     color: '#1EB475',
                     duration: 3000
                 });
-            } else if (response.status === 400) {
-                self.allowBundleRedeploy = !event.checked;
+            } else if (response.status === 404) {
                 self.dialogService.openConfirm({
                     title: 'Error',
                     message: response.error,
                     acceptButton: 'Ok',
                     acceptButtonColor: 'fds-warn'
                 });
+            } else {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                }).afterClosed().subscribe(function (accept) {
+                    self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier)
+                        .subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.nfRegistryService.bucket = response;
+                                self.bucketname = self.nfRegistryService.bucket.name;
+                                self.allowBundleRedeploy = self.nfRegistryService.bucket.allowBundleRedeploy;
+                                self.allowPublicRead = self.nfRegistryService.bucket.allowPublicRead;
+                            } else if (response.status === 404) {
+                                self.router.navigateByUrl('administration/workflow');
+                            }
+                        });
+                });
             }
         });
     },
@@ -500,9 +542,16 @@
         this.nfRegistryApi.updateBucket({
             'identifier': this.nfRegistryService.bucket.identifier,
             'allowPublicRead': event.checked,
+            'revision': this.nfRegistryService.bucket.revision
         }).subscribe(function (response) {
             if (!response.status || response.status === 200) {
                 self.nfRegistryService.bucket = response;
+                // update the bucket revision in the buckets table
+                self.nfRegistryService.buckets.filter(function (bucket) {
+                    return self.nfRegistryService.bucket.identifier === bucket.identifier;
+                }).forEach(function (bucket) {
+                    bucket.revision = response.revision;
+                });
                 self.snackBarService.openCoaster({
                     title: 'Success',
                     message: 'Bucket settings have been updated.',
@@ -512,14 +561,32 @@
                     color: '#1EB475',
                     duration: 3000
                 });
-            } else if (response.status === 400) {
-                self.allowPublicRead = !event.checked;
+            } else if (response.status === 404) {
                 self.dialogService.openConfirm({
                     title: 'Error',
                     message: response.error,
                     acceptButton: 'Ok',
                     acceptButtonColor: 'fds-warn'
                 });
+            } else {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                }).afterClosed().subscribe(function (accept) {
+                    self.nfRegistryApi.getBucket(self.nfRegistryService.bucket.identifier)
+                        .subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.nfRegistryService.bucket = response;
+                                self.bucketname = self.nfRegistryService.bucket.name;
+                                self.allowBundleRedeploy = self.nfRegistryService.bucket.allowBundleRedeploy;
+                                self.allowPublicRead = self.nfRegistryService.bucket.allowPublicRead;
+                            } else if (response.status === 404) {
+                                self.router.navigateByUrl('administration/workflow');
+                            }
+                        });
+                });
             }
         });
     },
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
index 8ec0c6a..676824e 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
@@ -162,7 +162,13 @@
      */
     createBucket: function (name, allowPublicRead) {
         var self = this;
-        return this.http.post('../nifi-registry-api/buckets', {'name': name, 'allowPublicRead': allowPublicRead}, headers).pipe(
+        return this.http.post('../nifi-registry-api/buckets', {
+            'name': name,
+            'allowPublicRead': allowPublicRead,
+            'revision': {
+                'version': 0
+            }
+        }, headers).pipe(
             map(function (response) {
                 return response;
             }),
@@ -182,11 +188,12 @@
      * Delete an existing bucket in the registry, along with all the objects it is storing.
      *
      * @param {string} bucketId     The identifier of the bucket to be deleted.
+     * @param {int} version         The version from the revision of the bucket
      * @returns {*}
      */
-    deleteBucket: function (bucketId) {
+    deleteBucket: function (bucketId, version) {
         var self = this;
-        return this.http.delete('../nifi-registry-api/buckets/' + bucketId, headers).pipe(
+        return this.http.delete('../nifi-registry-api/buckets/' + bucketId + '?version=' + version, headers).pipe(
             map(function (response) {
                 return response;
             }),
@@ -331,6 +338,9 @@
                     canWrite: false,
                     canDelete: false
                 }
+            },
+            revision: {
+                version: 0
             }
         }, headers).pipe(
             map(function (response) {
@@ -353,12 +363,14 @@
      *
      * @param {string} identifier   The identifier of the user.
      * @param {string} identity     The identity of the user.
+     * @param {string} revision     The revision of the user.
      * @returns {*}
      */
-    updateUser: function (identifier, identity) {
+    updateUser: function (identifier, identity, revision) {
         return this.http.put('../nifi-registry-api/tenants/users/' + identifier, {
             'identifier': identifier,
-            'identity': identity
+            'identity': identity,
+            'revision': revision
         }, headers).pipe(
             map(function (response) {
                 return response;
@@ -396,11 +408,12 @@
      * Delete an existing user from the registry.
      *
      * @param {string} userId     The identifier of the user to be deleted.
+     * @param {int} version       The version from the user's revision
      * @returns {*}
      */
-    deleteUser: function (userId) {
+    deleteUser: function (userId, version) {
         var self = this;
-        return this.http.delete('../nifi-registry-api/tenants/users/' + userId, headers).pipe(
+        return this.http.delete('../nifi-registry-api/tenants/users/' + userId + '?version=' + version, headers).pipe(
             map(function (response) {
                 return response;
             }),
@@ -467,11 +480,12 @@
      * Delete an existing user group from the registry.
      *
      * @param {string} userGroupId     The identifier of the user group to be deleted.
+     * @param {int} version            The version from the group's revision
      * @returns {*}
      */
-    deleteUserGroup: function (userGroupId) {
+    deleteUserGroup: function (userGroupId, version) {
         var self = this;
-        return this.http.delete('../nifi-registry-api/tenants/user-groups/' + userGroupId, headers).pipe(
+        return this.http.delete('../nifi-registry-api/tenants/user-groups/' + userGroupId + '?version=' + version, headers).pipe(
             map(function (response) {
                 return response;
             }),
@@ -500,7 +514,10 @@
         return this.http.post('../nifi-registry-api/tenants/user-groups', {
             'identifier': identifier,
             'identity': identity,
-            'users': users
+            'users': users,
+            'revision': {
+                'version': 0
+            }
         }, headers).pipe(
             map(function (response) {
                 return response;
@@ -522,14 +539,16 @@
      *
      * @param {string}  identifier   The identifier of the group.
      * @param {string}  identity     The identity of the group.
-     * @param {array}   users         The array of users in the new group.
+     * @param {array}   users        The array of users in the new group.
+     * @param {string}  revision     The revision of the group.
      * @returns {*}
      */
-    updateUserGroup: function (identifier, identity, users) {
+    updateUserGroup: function (identifier, identity, users, revision) {
         return this.http.put('../nifi-registry-api/tenants/user-groups/' + identifier, {
             'identifier': identifier,
             'identity': identity,
-            'users': users
+            'users': users,
+            'revision': revision
         }, headers).pipe(
             map(function (response) {
                 return response;
@@ -604,14 +623,15 @@
      * @param {string} userGroups   The user groups with resource privileges.
      * @returns {*}
      */
-    putPolicyActionResource: function (identifier, action, resource, users, userGroups) {
+    putPolicyActionResource: function (identifier, action, resource, users, userGroups, revision) {
         var self = this;
         return this.http.put('../nifi-registry-api/policies/' + identifier, {
             'identifier': identifier,
             'resource': resource,
             'action': action,
             'users': users,
-            'userGroups': userGroups
+            'userGroups': userGroups,
+            'revision': revision
         }, headers).pipe(
             map(function (response) {
                 return response;
@@ -643,7 +663,10 @@
             'resource': resource,
             'action': action,
             'users': users,
-            'userGroups': userGroups
+            'userGroups': userGroups,
+            'revision': {
+                'version': 0
+            }
         }, headers).pipe(
             map(function (response) {
                 return response;
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js
index e13be06..b27ee11 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js
@@ -438,10 +438,10 @@
 
     it('should DELETE a bucket.', inject([HttpTestingController], function (httpMock) {
         // api call
-        nfRegistryApi.deleteBucket('1234').subscribe(function (response) {
+        nfRegistryApi.deleteBucket('1234', 0).subscribe(function (response) {
         });
         // the request it made
-        req = httpMock.expectOne('../nifi-registry-api/buckets/1234');
+        req = httpMock.expectOne('../nifi-registry-api/buckets/1234?version=0');
         expect(req.request.method).toEqual('DELETE');
 
         // Next, fulfill the request by transmitting a response.
@@ -457,20 +457,20 @@
         });
 
         // api call
-        nfRegistryApi.deleteBucket('1234').subscribe(function (response) {
-            expect(response.message).toEqual('Http failure response for ../nifi-registry-api/buckets/1234: 401 DELETE bucket mock error');
+        nfRegistryApi.deleteBucket('1234', 0).subscribe(function (response) {
+            expect(response.message).toEqual('Http failure response for ../nifi-registry-api/buckets/1234?version=0: 401 DELETE bucket mock error');
             var dialogServiceCall = nfRegistryApi.dialogService.openConfirm.calls.first();
             expect(dialogServiceCall.args[0].title).toBe('Error');
-            expect(dialogServiceCall.args[0].message).toBe('Http failure response for ../nifi-registry-api/buckets/1234: 401 DELETE bucket mock error');
+            expect(dialogServiceCall.args[0].message).toBe('Http failure response for ../nifi-registry-api/buckets/1234?version=0: 401 DELETE bucket mock error');
             expect(dialogServiceCall.args[0].acceptButton).toBe('Ok');
             expect(dialogServiceCall.args[0].acceptButtonColor).toBe('fds-warn');
         });
         // the request it made
-        req = httpMock.expectOne('../nifi-registry-api/buckets/1234');
+        req = httpMock.expectOne('../nifi-registry-api/buckets/1234?version=0');
         expect(req.request.method).toEqual('DELETE');
 
         // Next, fulfill the request by transmitting a response.
-        req.flush('Http failure response for ../nifi-registry-api/buckets/1234: 401 DELETE bucket mock error', {status: 401, statusText: 'DELETE bucket mock error'});
+        req.flush('Http failure response for ../nifi-registry-api/buckets/1234?version=0: 401 DELETE bucket mock error', {status: 401, statusText: 'DELETE bucket mock error'});
 
         // Finally, assert that there are no outstanding requests.
         httpMock.verify();
@@ -799,12 +799,12 @@
 
     it('should DELETE users.', inject([HttpTestingController], function (httpMock) {
         // api call
-        nfRegistryApi.deleteUser(123).subscribe(function (response) {
+        nfRegistryApi.deleteUser(123, 0).subscribe(function (response) {
             expect(response.identity).toEqual('User #1');
         });
 
         // the request it made
-        req = httpMock.expectOne('../nifi-registry-api/tenants/users/123');
+        req = httpMock.expectOne('../nifi-registry-api/tenants/users/123?version=0');
         expect(req.request.method).toEqual('DELETE');
 
         // Next, fulfill the request by transmitting a response.
@@ -823,21 +823,21 @@
         });
 
         // api call
-        nfRegistryApi.deleteUser(123).subscribe(function (response) {
-            expect(response.message).toEqual('Http failure response for ../nifi-registry-api/tenants/users/123: 401 DELETE users mock error');
+        nfRegistryApi.deleteUser(123, 0).subscribe(function (response) {
+            expect(response.message).toEqual('Http failure response for ../nifi-registry-api/tenants/users/123?version=0: 401 DELETE users mock error');
             var dialogServiceCall = nfRegistryApi.dialogService.openConfirm.calls.first();
             expect(dialogServiceCall.args[0].title).toBe('Error');
-            expect(dialogServiceCall.args[0].message).toBe('Http failure response for ../nifi-registry-api/tenants/users/123: 401 DELETE users mock error');
+            expect(dialogServiceCall.args[0].message).toBe('Http failure response for ../nifi-registry-api/tenants/users/123?version=0: 401 DELETE users mock error');
             expect(dialogServiceCall.args[0].acceptButton).toBe('Ok');
             expect(dialogServiceCall.args[0].acceptButtonColor).toBe('fds-warn');
         });
 
         // the request it made
-        req = httpMock.expectOne('../nifi-registry-api/tenants/users/123');
+        req = httpMock.expectOne('../nifi-registry-api/tenants/users/123?version=0');
         expect(req.request.method).toEqual('DELETE');
 
         // Next, fulfill the request by transmitting a response.
-        req.flush('Http failure response for ../nifi-registry-api/tenants/users/123: 401 DELETE users mock error', {status: 401, statusText: 'DELETE users mock error'});
+        req.flush('Http failure response for ../nifi-registry-api/tenants/users/123?version=0: 401 DELETE users mock error', {status: 401, statusText: 'DELETE users mock error'});
 
         // Finally, assert that there are no outstanding requests.
         httpMock.verify();
@@ -1188,12 +1188,12 @@
 
     it('should DELETE a user group.', inject([HttpTestingController], function (httpMock) {
         // api call
-        nfRegistryApi.deleteUserGroup(123).subscribe(function (response) {
+        nfRegistryApi.deleteUserGroup(123, 0).subscribe(function (response) {
             expect(response.identity).toEqual('Group #1');
         });
 
         // the request it made
-        req = httpMock.expectOne('../nifi-registry-api/tenants/user-groups/123');
+        req = httpMock.expectOne('../nifi-registry-api/tenants/user-groups/123?version=0');
         expect(req.request.method).toEqual('DELETE');
 
         // Next, fulfill the request by transmitting a response.
@@ -1212,21 +1212,21 @@
         });
 
         // api call
-        nfRegistryApi.deleteUserGroup(123).subscribe(function (response) {
-            expect(response.message).toEqual('Http failure response for ../nifi-registry-api/tenants/user-groups/123: 401 DELETE user groups mock error');
+        nfRegistryApi.deleteUserGroup(123, 0).subscribe(function (response) {
+            expect(response.message).toEqual('Http failure response for ../nifi-registry-api/tenants/user-groups/123?version=0: 401 DELETE user groups mock error');
             var dialogServiceCall = nfRegistryApi.dialogService.openConfirm.calls.first();
             expect(dialogServiceCall.args[0].title).toBe('Error');
-            expect(dialogServiceCall.args[0].message).toBe('Http failure response for ../nifi-registry-api/tenants/user-groups/123: 401 DELETE user groups mock error');
+            expect(dialogServiceCall.args[0].message).toBe('Http failure response for ../nifi-registry-api/tenants/user-groups/123?version=0: 401 DELETE user groups mock error');
             expect(dialogServiceCall.args[0].acceptButton).toBe('Ok');
             expect(dialogServiceCall.args[0].acceptButtonColor).toBe('fds-warn');
         });
 
         // the request it made
-        req = httpMock.expectOne('../nifi-registry-api/tenants/user-groups/123');
+        req = httpMock.expectOne('../nifi-registry-api/tenants/user-groups/123?version=0');
         expect(req.request.method).toEqual('DELETE');
 
         // Next, fulfill the request by transmitting a response.
-        req.flush('Http failure response for ../nifi-registry-api/tenants/user-groups/123: 401 DELETE user groups mock error', {status: 401, statusText: 'DELETE user groups mock error'});
+        req.flush('Http failure response for ../nifi-registry-api/tenants/user-groups/123?version=0: 401 DELETE user groups mock error', {status: 401, statusText: 'DELETE user groups mock error'});
 
         // Finally, assert that there are no outstanding requests.
         httpMock.verify();
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
index 7999978..c0c65d7 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
@@ -410,21 +410,27 @@
             }).afterClosed().subscribe(
                 function (accept) {
                     if (accept) {
-                        self.api.deleteDroplet(droplet.link.href).subscribe(function (response) {
-                            self.droplets = self.droplets.filter(function (d) {
-                                return (d.identifier !== droplet.identifier);
-                            });
-                            self.snackBarService.openCoaster({
-                                title: 'Success',
-                                message: 'All versions of this ' + droplet.type.toLowerCase() + ' have been deleted.',
-                                verticalPosition: 'bottom',
-                                horizontalPosition: 'right',
-                                icon: 'fa fa-check-circle-o',
-                                color: '#1EB475',
-                                duration: 3000
-                            });
-                            self.droplet = {};
-                            self.filterDroplets();
+                        var deleteUrl = droplet.link.href;
+                        if (droplet.type === 'Flow') {
+                            deleteUrl = deleteUrl + '?version=' + droplet.revision.version;
+                        }
+                        self.api.deleteDroplet(deleteUrl).subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.droplets = self.droplets.filter(function (d) {
+                                    return (d.identifier !== droplet.identifier);
+                                });
+                                self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'All versions of this ' + droplet.type.toLowerCase() + ' have been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.droplet = {};
+                                self.filterDroplets();
+                            }
                         });
                     }
                 }
@@ -544,22 +550,24 @@
             }).afterClosed().subscribe(
                 function (accept) {
                     if (accept) {
-                        self.api.deleteBucket(bucket.identifier).subscribe(function (response) {
-                            self.buckets = self.buckets.filter(function (b) {
-                                return b.identifier !== bucket.identifier;
-                            });
-                            self.snackBarService.openCoaster({
-                                title: 'Success',
-                                message: 'All versions of all items in this bucket, as well as the bucket, have been deleted.',
-                                verticalPosition: 'bottom',
-                                horizontalPosition: 'right',
-                                icon: 'fa fa-check-circle-o',
-                                color: '#1EB475',
-                                duration: 3000
-                            });
-                            self.bucket = {};
-                            self.filterBuckets();
-                            self.determineAllBucketsSelectedState();
+                        self.api.deleteBucket(bucket.identifier, bucket.revision.version).subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.buckets = self.buckets.filter(function (b) {
+                                    return b.identifier !== bucket.identifier;
+                                });
+                                self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'All versions of all items in this bucket, as well as the bucket, have been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.bucket = {};
+                                self.filterBuckets();
+                                self.determineAllBucketsSelectedState();
+                            }
                         });
                     }
                 }
@@ -773,20 +781,22 @@
                 if (accept) {
                     self.filteredBuckets.forEach(function (filteredBucket) {
                         if (filteredBucket.checked) {
-                            self.api.deleteBucket(filteredBucket.identifier).subscribe(function (response) {
-                                self.buckets = self.buckets.filter(function (bucket) {
-                                    return bucket.identifier !== filteredBucket.identifier;
-                                });
-                                self.snackBarService.openCoaster({
-                                    title: 'Success',
-                                    message: 'All versions of all items in ' + filteredBucket.name + ' have been deleted.',
-                                    verticalPosition: 'bottom',
-                                    horizontalPosition: 'right',
-                                    icon: 'fa fa-check-circle-o',
-                                    color: '#1EB475',
-                                    duration: 3000
-                                });
-                                self.filterBuckets();
+                            self.api.deleteBucket(filteredBucket.identifier, filteredBucket.revision.version).subscribe(function (response) {
+                                if (!response.status || response.status === 200) {
+                                    self.buckets = self.buckets.filter(function (bucket) {
+                                        return bucket.identifier !== filteredBucket.identifier;
+                                    });
+                                    self.snackBarService.openCoaster({
+                                        title: 'Success',
+                                        message: 'All versions of all items in ' + filteredBucket.name + ' have been deleted.',
+                                        verticalPosition: 'bottom',
+                                        horizontalPosition: 'right',
+                                        icon: 'fa fa-check-circle-o',
+                                        color: '#1EB475',
+                                        duration: 3000
+                                    });
+                                    self.filterBuckets();
+                                }
                             });
                         }
                     });
@@ -1018,21 +1028,23 @@
             }).afterClosed().subscribe(
                 function (accept) {
                     if (accept) {
-                        self.api.deleteUser(user.identifier).subscribe(function (response) {
-                            self.users = self.users.filter(function (u) {
-                                return u.identifier !== user.identifier;
-                            });
-                            self.snackBarService.openCoaster({
-                                title: 'Success',
-                                message: 'User: ' + user.identity + ' has been deleted.',
-                                verticalPosition: 'bottom',
-                                horizontalPosition: 'right',
-                                icon: 'fa fa-check-circle-o',
-                                color: '#1EB475',
-                                duration: 3000
-                            });
-                            self.filterUsersAndGroups();
-                            self.determineAllUsersAndGroupsSelectedState();
+                        self.api.deleteUser(user.identifier, user.revision.version).subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.users = self.users.filter(function (u) {
+                                    return u.identifier !== user.identifier;
+                                });
+                                self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'User: ' + user.identity + ' has been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.filterUsersAndGroups();
+                                self.determineAllUsersAndGroupsSelectedState();
+                            }
                         });
                     }
                 }
@@ -1065,21 +1077,23 @@
             }).afterClosed().subscribe(
                 function (accept) {
                     if (accept) {
-                        self.api.deleteUserGroup(group.identifier).subscribe(function (response) {
-                            self.groups = self.groups.filter(function (u) {
-                                return u.identifier !== group.identifier;
-                            });
-                            self.snackBarService.openCoaster({
-                                title: 'Success',
-                                message: 'Group: ' + group.identity + ' has been deleted.',
-                                verticalPosition: 'bottom',
-                                horizontalPosition: 'right',
-                                icon: 'fa fa-check-circle-o',
-                                color: '#1EB475',
-                                duration: 3000
-                            });
-                            self.filterUsersAndGroups();
-                            self.determineAllUsersAndGroupsSelectedState();
+                        self.api.deleteUserGroup(group.identifier, group.revision.version).subscribe(function (response) {
+                            if (!response.status || response.status === 200) {
+                                self.groups = self.groups.filter(function (u) {
+                                    return u.identifier !== group.identifier;
+                                });
+                                self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'Group: ' + group.identity + ' has been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.filterUsersAndGroups();
+                                self.determineAllUsersAndGroupsSelectedState();
+                            }
                         });
                     }
                 }
@@ -1110,39 +1124,43 @@
                 if (accept) {
                     self.filteredUserGroups.forEach(function (filteredUserGroup) {
                         if (filteredUserGroup.checked) {
-                            self.api.deleteUserGroup(filteredUserGroup.identifier).subscribe(function (response) {
-                                self.groups = self.groups.filter(function (u) {
-                                    return u.identifier !== filteredUserGroup.identifier;
-                                });
-                                self.snackBarService.openCoaster({
-                                    title: 'Success',
-                                    message: 'User group: ' + filteredUserGroup.identity + ' has been deleted.',
-                                    verticalPosition: 'bottom',
-                                    horizontalPosition: 'right',
-                                    icon: 'fa fa-check-circle-o',
-                                    color: '#1EB475',
-                                    duration: 3000
-                                });
-                                self.filterUsersAndGroups();
+                            self.api.deleteUserGroup(filteredUserGroup.identifier, filteredUserGroup.revision.version).subscribe(function (response) {
+                                if (!response.status || response.status === 200) {
+                                    self.groups = self.groups.filter(function (u) {
+                                        return u.identifier !== filteredUserGroup.identifier;
+                                    });
+                                    self.snackBarService.openCoaster({
+                                        title: 'Success',
+                                        message: 'User group: ' + filteredUserGroup.identity + ' has been deleted.',
+                                        verticalPosition: 'bottom',
+                                        horizontalPosition: 'right',
+                                        icon: 'fa fa-check-circle-o',
+                                        color: '#1EB475',
+                                        duration: 3000
+                                    });
+                                    self.filterUsersAndGroups();
+                                }
                             });
                         }
                     });
                     self.filteredUsers.forEach(function (filteredUser) {
                         if (filteredUser.checked) {
-                            self.api.deleteUser(filteredUser.identifier).subscribe(function (response) {
-                                self.users = self.users.filter(function (u) {
-                                    return u.identifier !== filteredUser.identifier;
-                                });
-                                self.snackBarService.openCoaster({
-                                    title: 'Success',
-                                    message: 'User: ' + filteredUser.identity + ' has been deleted.',
-                                    verticalPosition: 'bottom',
-                                    horizontalPosition: 'right',
-                                    icon: 'fa fa-check-circle-o',
-                                    color: '#1EB475',
-                                    duration: 3000
-                                });
-                                self.filterUsersAndGroups();
+                            self.api.deleteUser(filteredUser.identifier, filteredUser.revision.version).subscribe(function (response) {
+                                if (!response.status || response.status === 200) {
+                                    self.users = self.users.filter(function (u) {
+                                        return u.identifier !== filteredUser.identifier;
+                                    });
+                                    self.snackBarService.openCoaster({
+                                        title: 'Success',
+                                        message: 'User: ' + filteredUser.identity + ' has been deleted.',
+                                        verticalPosition: 'bottom',
+                                        horizontalPosition: 'right',
+                                        icon: 'fa fa-check-circle-o',
+                                        color: '#1EB475',
+                                        duration: 3000
+                                    });
+                                    self.filterUsersAndGroups();
+                                }
                             });
                         }
                     });
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js
index b70de9b..455963d 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js
@@ -795,10 +795,10 @@
         }).and.returnValue(of({identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', link: null}));
 
         // object to be updated by the test
-        const bucket = {identifier: '999'};
+        const bucket = {identifier: '999', revision: { version: 0}};
 
         // set up the bucket to be deleted
-        nfRegistryService.buckets = [bucket, {identifier: 1}];
+        nfRegistryService.buckets = [bucket, {identifier: 1, revision: { version: 0}}];
 
         // The function to test
         nfRegistryService.executeBucketAction({name: 'delete'}, bucket);
@@ -847,10 +847,10 @@
         }).and.returnValue(of({identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', link: null}));
 
         // object to be updated by the test
-        const user = {identifier: '999'};
+        const user = {identifier: '999', revision: {version: 0}};
 
         // set up the user to be deleted
-        nfRegistryService.users = [user, {identifier: 1}];
+        nfRegistryService.users = [user, {identifier: 1, revision: { version: 0}}];
 
         // The function to test
         nfRegistryService.executeUserAction({name: 'delete'}, user);
@@ -899,10 +899,10 @@
         }).and.returnValue(of({identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', link: null}));
 
         // object to be updated by the test
-        const group = {identifier: '999'};
+        const group = {identifier: '999', revision: {version: 0}};
 
         // set up the user to be deleted
-        nfRegistryService.groups = [group, {identifier: 1}];
+        nfRegistryService.groups = [group, {identifier: 1, revision: {version: 0}}];
 
         // The function to test
         nfRegistryService.executeGroupAction({name: 'delete'}, group);
@@ -1023,10 +1023,10 @@
         }).and.returnValue(of({identifier: 999, link: null}));
 
         // object to be updated by the test
-        const bucket = {identifier: 999, checked: true};
+        const bucket = {identifier: 999, checked: true, revision: { version: 0}};
 
         // set up the bucket to be deleted
-        nfRegistryService.buckets = [bucket, {identifier: 1}];
+        nfRegistryService.buckets = [bucket, {identifier: 1, revision: { version: 0}}];
         nfRegistryService.filteredBuckets = nfRegistryService.buckets;
 
         // The function to test
@@ -1065,13 +1065,13 @@
         }).and.returnValue(of({identifier: 99, link: null}));
 
         // object to be updated by the test
-        const group = {identifier: 999, checked: true};
-        const user = {identifier: 999, checked: true};
+        const group = {identifier: 999, checked: true, revision: { version: 0}};
+        const user = {identifier: 999, checked: true, revision: { version: 0}};
 
         // set up the group to be deleted
-        nfRegistryService.groups = [group, {identifier: 1}];
+        nfRegistryService.groups = [group, {identifier: 1, revision: { version: 0}}];
         nfRegistryService.filteredUserGroups = nfRegistryService.groups;
-        nfRegistryService.users = [user, {identifier: 12}];
+        nfRegistryService.users = [user, {identifier: 12, revision: { version: 0}}];
         nfRegistryService.filteredUsers = nfRegistryService.users;
 
         // The function to test