NIFIREG-215 - Extension Bundle Improvements

- Adding content size field extension bundle version
- Adding optional filters to the get all bundles end-point
- Bumping minor versions of spring boot and spring security
- Adding /bundles/versions with filter params
- Added new client method to return an optional checksum for a given group, artifact, and version
- Adding support for SNAPSHOT versions
- Adding flag on buckets to indicate if redeploying extension bundle versions is allowed

This closes #149.

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/ExtensionBundleClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleClient.java
index 32eb3c9..b2724ae 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleClient.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.registry.client;
 
 import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
 
 import java.io.IOException;
 import java.util.List;
@@ -37,6 +38,17 @@
     List<ExtensionBundle> getAll() throws IOException, NiFiRegistryException;
 
     /**
+     * Retrieves all extension bundles matching the specified filters, located in buckets the current user is authorized for.
+     *
+     * @param filterParams the filter params
+     * @return the list of extension bundles
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NiFiRegistryException if an non I/O error occurs
+     */
+    List<ExtensionBundle> getAll(ExtensionBundleFilterParams filterParams) throws IOException, NiFiRegistryException;
+
+    /**
      * Retrieves the extension bundles located in the given bucket.
      *
      * @param bucketId the bucket id
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleVersionClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleVersionClient.java
index 8bad8c9..dd07df3 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleVersionClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleVersionClient.java
@@ -19,6 +19,7 @@
 import org.apache.nifi.registry.extension.ExtensionBundleType;
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 
 import java.io.File;
 import java.io.IOException;
@@ -88,6 +89,18 @@
     ExtensionBundleVersion create(String bucketId, ExtensionBundleType bundleType, File bundleFile, String sha256)
             throws IOException, NiFiRegistryException;
 
+    /**
+     * Retrieves all the extension bundle versions located in buckets the current user is authorized for, and
+     * matching any of the provided filter params.
+     *
+     * @param filterParams the filter params
+     * @return the list of bundle version metadata
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NiFiRegistryException if an non I/O error occurs
+     */
+    List<ExtensionBundleVersionMetadata> getBundleVersions(ExtensionBundleVersionFilterParams filterParams)
+            throws IOException, NiFiRegistryException;
 
     /**
      * Retrieves the metadata about the versions of the given bundle.
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java
index 6682921..bcb1b30 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java
@@ -25,6 +25,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Client for interacting with the extension repository.
@@ -123,4 +124,21 @@
     String getVersionSha256(String bucketName, String groupId, String artifactId, String version)
             throws IOException, NiFiRegistryException;
 
+    /**
+     * Gets the hex representation of the SHA-256 hash of the binary content for the given version.
+     *
+     * If the version is a SNAPSHOT version, there may be more than one instance of the SNAPSHOT version in different
+     * buckets. In this case the instance with the latest created timestamp will be used to obtain the checksum.
+     *
+     * @param groupId the group id
+     * @param artifactId the artifact id
+     * @param version the version
+     * @return the SHA-256 hex string
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NiFiRegistryException if an non I/O error occurs
+     */
+    Optional<String> getVersionSha256(String groupId, String artifactId, String version)
+            throws IOException, NiFiRegistryException;
+
 }
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleClient.java
index b70a653..9f748d5 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleClient.java
@@ -20,6 +20,7 @@
 import org.apache.nifi.registry.client.ExtensionBundleClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
 
 import javax.ws.rs.client.WebTarget;
 import java.io.IOException;
@@ -48,9 +49,23 @@
 
     @Override
     public List<ExtensionBundle> getAll() throws IOException, NiFiRegistryException {
+        return getAll(null);
+    }
+
+    @Override
+    public List<ExtensionBundle> getAll(final ExtensionBundleFilterParams filterParams) throws IOException, NiFiRegistryException {
         return executeAction("Error getting extension bundles", () -> {
             WebTarget target = extensionBundlesTarget;
 
+            if (filterParams != null) {
+                if (!StringUtils.isBlank(filterParams.getGroupId())) {
+                    target = target.queryParam("groupId", filterParams.getGroupId());
+                }
+                if (!StringUtils.isBlank(filterParams.getArtifactId())) {
+                    target = target.queryParam("artifactId", filterParams.getArtifactId());
+                }
+            }
+
             final ExtensionBundle[] bundles = getRequestBuilder(target).get(ExtensionBundle[].class);
             return  bundles == null ? Collections.emptyList() : Arrays.asList(bundles);
         });
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleVersionClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleVersionClient.java
index 6969317..0332412 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleVersionClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleVersionClient.java
@@ -22,6 +22,7 @@
 import org.apache.nifi.registry.extension.ExtensionBundleType;
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.glassfish.jersey.media.multipart.FormDataMultiPart;
 import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
 import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
@@ -149,6 +150,32 @@
     }
 
     @Override
+    public List<ExtensionBundleVersionMetadata> getBundleVersions(final ExtensionBundleVersionFilterParams filterParams)
+            throws IOException, NiFiRegistryException {
+
+        return executeAction("Error getting extension bundle versions", () -> {
+            WebTarget target = extensionBundlesTarget.path("/versions");
+
+            if (filterParams != null) {
+                if (!StringUtils.isBlank(filterParams.getGroupId())) {
+                    target = target.queryParam("groupId", filterParams.getGroupId());
+                }
+
+                if (!StringUtils.isBlank(filterParams.getArtifactId())) {
+                    target = target.queryParam("artifactId", filterParams.getArtifactId());
+                }
+
+                if (!StringUtils.isBlank(filterParams.getVersion())) {
+                    target = target.queryParam("version", filterParams.getVersion());
+                }
+            }
+
+            final ExtensionBundleVersionMetadata[] bundleVersions = getRequestBuilder(target).get(ExtensionBundleVersionMetadata[].class);
+            return  bundleVersions == null ? Collections.emptyList() : Arrays.asList(bundleVersions);
+        });
+    }
+
+    @Override
     public List<ExtensionBundleVersionMetadata> getBundleVersions(final String bundleId)
             throws IOException, NiFiRegistryException {
 
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
index 18a6979..dc67b97 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
@@ -25,6 +25,7 @@
 import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
 
+import javax.ws.rs.NotFoundException;
 import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.MediaType;
 import java.io.IOException;
@@ -33,6 +34,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 public class JerseyExtensionRepoClient extends AbstractJerseyClient implements ExtensionRepoClient {
 
@@ -179,6 +181,38 @@
         });
     }
 
+    @Override
+    public Optional<String> getVersionSha256(final String groupId, final String artifactId, final String version)
+            throws IOException, NiFiRegistryException {
+
+        if (StringUtils.isBlank(groupId)) {
+            throw new IllegalArgumentException("Group id cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(artifactId)) {
+            throw new IllegalArgumentException("Artifact id cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(version)) {
+            throw new IllegalArgumentException("Version cannot be null or blank");
+        }
+
+        return executeAction("Error retrieving version content for extension repo", () -> {
+            final WebTarget target = extensionRepoTarget
+                    .path("{groupId}/{artifactId}/{version}/sha256")
+                    .resolveTemplate("groupId", groupId)
+                    .resolveTemplate("artifactId", artifactId)
+                    .resolveTemplate("version", version);
+
+            try {
+                final String sha256 = getRequestBuilder(target).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class);
+                return Optional.of(sha256);
+            } catch (NotFoundException nfe) {
+                return Optional.empty();
+            }
+        });
+    }
+
     private void validate(String bucketName, String groupId, String artifactId, String version) {
         if (StringUtils.isBlank(bucketName)) {
             throw new IllegalArgumentException("Bucket name cannot be null or blank");
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 94402a9..a5f9f51 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
@@ -41,6 +41,8 @@
 
     private String description;
 
+    private Boolean allowExtensionBundleRedeploy;
+
     private Permissions permissions;
 
     @ApiModelProperty(value = "An ID to uniquely identify this object.", readOnly = true)
@@ -79,6 +81,15 @@
         this.description = description;
     }
 
+    @ApiModelProperty("Indicates if this bucket allows the same version of an extension bundle to be redeployed and thus overwrite the existing artifact. By default this is false.")
+    public Boolean isAllowExtensionBundleRedeploy() {
+        return allowExtensionBundleRedeploy;
+    }
+
+    public void setAllowExtensionBundleRedeploy(final Boolean allowExtensionBundleRedeploy) {
+        this.allowExtensionBundleRedeploy = allowExtensionBundleRedeploy;
+    }
+
     @ApiModelProperty(value = "The access that the current user has to this bucket.", readOnly = true)
     public Permissions getPermissions() {
         return permissions;
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java
index 35756f9..6902083 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java
@@ -56,6 +56,10 @@
     @NotNull
     private Boolean sha256Supplied;
 
+    @NotNull
+    @Min(0)
+    private long contentSize;
+
 
     @ApiModelProperty(value = "The id of this version of the extension bundle")
     public String getId() {
@@ -138,6 +142,15 @@
         this.sha256Supplied = sha256Supplied;
     }
 
+    @ApiModelProperty(value = "The size of the binary content for this version in bytes")
+    public long getContentSize() {
+        return contentSize;
+    }
+
+    public void setContentSize(long contentSize) {
+        this.contentSize = contentSize;
+    }
+
     @Override
     public int compareTo(final ExtensionBundleVersionMetadata o) {
         return o == null ? -1 : version.compareTo(o.version);
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/filter/ExtensionBundleFilterParams.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/filter/ExtensionBundleFilterParams.java
new file mode 100644
index 0000000..eb4490f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/filter/ExtensionBundleFilterParams.java
@@ -0,0 +1,70 @@
+/*
+ * 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.extension.filter;
+
+/**
+ * Filter parameters for retrieving extension bundles.
+ */
+public class ExtensionBundleFilterParams {
+
+    private static final ExtensionBundleFilterParams EMPTY_PARAMS = new Builder().build();
+
+    private final String groupId;
+    private final String artifactId;
+
+    private ExtensionBundleFilterParams(final Builder builder) {
+        this.groupId = builder.groupId;
+        this.artifactId = builder.artifactId;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    public static ExtensionBundleFilterParams of(final String groupId, final String artifactId) {
+        return new Builder().group(groupId).artifact(artifactId).build();
+    }
+
+    public static ExtensionBundleFilterParams empty() {
+        return EMPTY_PARAMS;
+    }
+
+    public static class Builder {
+
+        private String groupId;
+        private String artifactId;
+
+        public Builder group(final String groupId) {
+            this.groupId = groupId;
+            return this;
+        }
+
+        public Builder artifact(final String artifactId) {
+            this.artifactId = artifactId;
+            return this;
+        }
+
+        public ExtensionBundleFilterParams build() {
+            return new ExtensionBundleFilterParams(this);
+        }
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/filter/ExtensionBundleVersionFilterParams.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/filter/ExtensionBundleVersionFilterParams.java
new file mode 100644
index 0000000..1402989
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/filter/ExtensionBundleVersionFilterParams.java
@@ -0,0 +1,82 @@
+/*
+ * 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.extension.filter;
+
+/**
+ * Filter parameters for extension bundle versions.
+ */
+public class ExtensionBundleVersionFilterParams {
+
+    private static final ExtensionBundleVersionFilterParams EMPTY_PARAMS = new Builder().build();
+
+    private final String groupId;
+    private final String artifactId;
+    private final String version;
+
+    private ExtensionBundleVersionFilterParams(final Builder builder) {
+        this.groupId = builder.groupId;
+        this.artifactId = builder.artifactId;
+        this.version = builder.version;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public static ExtensionBundleVersionFilterParams of(final String groupId, final String artifactId, final String version) {
+        return new Builder().group(groupId).artifact(artifactId).version(version).build();
+    }
+
+    public static ExtensionBundleVersionFilterParams empty() {
+        return EMPTY_PARAMS;
+    }
+
+    public static class Builder {
+
+        private String groupId;
+        private String artifactId;
+        private String version;
+
+        public Builder group(final String groupId) {
+            this.groupId = groupId;
+            return this;
+        }
+
+        public Builder artifact(final String artifactId) {
+            this.artifactId = artifactId;
+            return this;
+        }
+
+        public Builder version(final String version) {
+            this.version = version;
+            return this;
+        }
+
+        public ExtensionBundleVersionFilterParams build() {
+            return new ExtensionBundleVersionFilterParams(this);
+        }
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
index de9cf69..d80485d 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.db;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.db.entity.BucketEntity;
 import org.apache.nifi.registry.db.entity.BucketItemEntity;
 import org.apache.nifi.registry.db.entity.BucketItemEntityType;
@@ -35,6 +36,8 @@
 import org.apache.nifi.registry.db.mapper.ExtensionEntityRowMapper;
 import org.apache.nifi.registry.db.mapper.FlowEntityRowMapper;
 import org.apache.nifi.registry.db.mapper.FlowSnapshotEntityRowMapper;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.service.MetadataService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.EmptyResultDataAccessException;
@@ -65,8 +68,13 @@
 
     @Override
     public BucketEntity createBucket(final BucketEntity b) {
-        final String sql = "INSERT INTO bucket (ID, NAME, DESCRIPTION, CREATED) VALUES (?, ?, ?, ?)";
-        jdbcTemplate.update(sql, b.getId(), b.getName(), b.getDescription(), b.getCreated());
+        final String sql = "INSERT INTO bucket (ID, NAME, DESCRIPTION, CREATED, ALLOW_EXTENSION_BUNDLE_REDEPLOY) VALUES (?, ?, ?, ?, ?)";
+        jdbcTemplate.update(sql,
+                b.getId(),
+                b.getName(),
+                b.getDescription(),
+                b.getCreated(),
+                b.isAllowExtensionBundleRedeploy() ? 1 : 0);
         return b;
     }
 
@@ -88,8 +96,8 @@
 
     @Override
     public BucketEntity updateBucket(final BucketEntity bucket) {
-        final String sql = "UPDATE bucket SET name = ?, description = ? WHERE id = ?";
-        jdbcTemplate.update(sql, bucket.getName(), bucket.getDescription(), bucket.getId());
+        final String sql = "UPDATE bucket SET name = ?, description = ?, allow_extension_bundle_redeploy = ? WHERE id = ?";
+        jdbcTemplate.update(sql, bucket.getName(), bucket.getDescription(), bucket.isAllowExtensionBundleRedeploy() ? 1 : 0, bucket.getId());
         return bucket;
     }
 
@@ -495,12 +503,14 @@
     }
 
     @Override
-    public List<ExtensionBundleEntity> getExtensionBundles(final Set<String> bucketIds) {
+    public List<ExtensionBundleEntity> getExtensionBundles(final Set<String> bucketIds, final ExtensionBundleFilterParams filterParams) {
         if (bucketIds == null || bucketIds.isEmpty()) {
             return Collections.emptyList();
         }
 
-        final String selectSql =
+        final List<Object> args = new ArrayList<>();
+
+        final StringBuilder sqlBuilder = new StringBuilder(
                 "SELECT " +
                         "item.id as ID, " +
                         "item.name as NAME, " +
@@ -519,19 +529,28 @@
                     "bucket b " +
                 "WHERE " +
                     "item.id = eb.id AND " +
-                    "b.id = item.bucket_id";
+                    "b.id = item.bucket_id");
 
-        final StringBuilder sqlBuilder = new StringBuilder(selectSql).append(" AND item.bucket_id IN (");
-        for (int i=0; i < bucketIds.size(); i++) {
-            if (i > 0) {
-                sqlBuilder.append(", ");
+        if (filterParams != null) {
+            final String groupId = filterParams.getGroupId();
+            if (!StringUtils.isBlank(groupId)) {
+                sqlBuilder.append(" AND eb.group_id LIKE ? ");
+                args.add(groupId);
             }
-            sqlBuilder.append("?");
+
+            final String artifactId = filterParams.getArtifactId();
+            if (!StringUtils.isBlank(artifactId)) {
+                sqlBuilder.append(" AND eb.artifact_id LIKE ? ");
+                args.add(artifactId);
+            }
         }
-        sqlBuilder.append(") ");
+
+        addBucketIdentifiersClause(sqlBuilder, "item.bucket_id", bucketIds);
         sqlBuilder.append("ORDER BY eb.group_id ASC, eb.artifact_id ASC");
 
-        final List<ExtensionBundleEntity> bundleEntities = jdbcTemplate.query(sqlBuilder.toString(), bucketIds.toArray(), new ExtensionBundleEntityWithBucketNameRowMapper());
+        args.addAll(bucketIds);
+
+        final List<ExtensionBundleEntity> bundleEntities = jdbcTemplate.query(sqlBuilder.toString(), args.toArray(), new ExtensionBundleEntityWithBucketNameRowMapper());
         return populateVersionCounts(bundleEntities);
     }
 
@@ -607,8 +626,9 @@
                     "CREATED_BY, " +
                     "DESCRIPTION, " +
                     "SHA_256_HEX, " +
-                    "SHA_256_SUPPLIED " +
-                ") VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
+                    "SHA_256_SUPPLIED," +
+                    "CONTENT_SIZE " +
+                ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
 
         jdbcTemplate.update(sql,
                 extensionBundleVersion.getId(),
@@ -618,26 +638,13 @@
                 extensionBundleVersion.getCreatedBy(),
                 extensionBundleVersion.getDescription(),
                 extensionBundleVersion.getSha256Hex(),
-                extensionBundleVersion.getSha256Supplied() ? 1 : 0);
+                extensionBundleVersion.getSha256Supplied() ? 1 : 0,
+                extensionBundleVersion.getContentSize());
 
         return extensionBundleVersion;
     }
 
-    @Override
-    public ExtensionBundleVersionEntity getExtensionBundleVersion(final String extensionBundleId, final String version) {
-        final String sql =
-                "SELECT * " +
-                "FROM extension_bundle_version " +
-                "WHERE extension_bundle_id = ? AND version = ?";
-
-        try {
-            return jdbcTemplate.queryForObject(sql, new ExtensionBundleVersionEntityRowMapper(), extensionBundleId, version);
-        } catch (EmptyResultDataAccessException e) {
-            return null;
-        }
-    }
-
-    private static final String BASE_EXTENSION_BUNDLE_SQL =
+    private static final String BASE_EXTENSION_BUNDLE_VERSION_SQL =
             "SELECT " +
                 "ebv.id AS ID," +
                 "ebv.extension_bundle_id AS EXTENSION_BUNDLE_ID, " +
@@ -646,13 +653,26 @@
                 "ebv.created_by AS CREATED_BY, " +
                 "ebv.description AS DESCRIPTION, " +
                 "ebv.sha_256_hex AS SHA_256_HEX, " +
-                "ebv.sha_256_supplied AS SHA_256_SUPPLIED " +
+                "ebv.sha_256_supplied AS SHA_256_SUPPLIED ," +
+                "ebv.content_size AS CONTENT_SIZE, " +
+                "eb.bucket_id AS BUCKET_ID " +
             "FROM extension_bundle eb, extension_bundle_version ebv " +
             "WHERE eb.id = ebv.extension_bundle_id ";
 
     @Override
+    public ExtensionBundleVersionEntity getExtensionBundleVersion(final String extensionBundleId, final String version) {
+        final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL +
+                " AND ebv.extension_bundle_id = ? AND ebv.version = ?";
+        try {
+            return jdbcTemplate.queryForObject(sql, new ExtensionBundleVersionEntityRowMapper(), extensionBundleId, version);
+        } catch (EmptyResultDataAccessException e) {
+            return null;
+        }
+    }
+
+    @Override
     public ExtensionBundleVersionEntity getExtensionBundleVersion(final String bucketId, final String groupId, final String artifactId, final String version) {
-        final String sql = BASE_EXTENSION_BUNDLE_SQL +
+        final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL +
                     "AND eb.bucket_id = ? " +
                     "AND eb.group_id = ? " +
                     "AND eb.artifact_id = ? " +
@@ -666,14 +686,63 @@
     }
 
     @Override
+    public List<ExtensionBundleVersionEntity> getExtensionBundleVersions(final Set<String> bucketIdentifiers, final ExtensionBundleVersionFilterParams filterParams) {
+        if (bucketIdentifiers == null || bucketIdentifiers.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        final List<Object> args = new ArrayList<>();
+        final StringBuilder sqlBuilder = new StringBuilder(BASE_EXTENSION_BUNDLE_VERSION_SQL);
+
+        if (filterParams != null) {
+            final String groupId = filterParams.getGroupId();
+            if (!StringUtils.isBlank(groupId)) {
+                sqlBuilder.append(" AND eb.group_id LIKE ? ");
+                args.add(groupId);
+            }
+
+            final String artifactId = filterParams.getArtifactId();
+            if (!StringUtils.isBlank(artifactId)) {
+                sqlBuilder.append(" AND eb.artifact_id LIKE ? ");
+                args.add(artifactId);
+            }
+
+            final String version = filterParams.getVersion();
+            if (!StringUtils.isBlank(version)) {
+                sqlBuilder.append(" AND ebv.version LIKE ? ");
+                args.add(version);
+            }
+        }
+
+        addBucketIdentifiersClause(sqlBuilder, "eb.bucket_id", bucketIdentifiers);
+        args.addAll(bucketIdentifiers);
+
+        final List<ExtensionBundleVersionEntity> bundleVersionEntities = jdbcTemplate.query(
+                sqlBuilder.toString(), args.toArray(), new ExtensionBundleVersionEntityRowMapper());
+
+        return bundleVersionEntities;
+    }
+
+    private void addBucketIdentifiersClause(StringBuilder sqlBuilder, String bucketField, Set<String> bucketIdentifiers) {
+        sqlBuilder.append(" AND ").append(bucketField).append(" IN (");
+        for (int i = 0; i < bucketIdentifiers.size(); i++) {
+            if (i > 0) {
+                sqlBuilder.append(", ");
+            }
+            sqlBuilder.append("?");
+        }
+        sqlBuilder.append(") ");
+    }
+
+    @Override
     public List<ExtensionBundleVersionEntity> getExtensionBundleVersions(final String extensionBundleId) {
-        final String sql = "SELECT * FROM extension_bundle_version WHERE extension_bundle_id = ?";
+        final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL + " AND ebv.extension_bundle_id = ?";
         return jdbcTemplate.query(sql, new Object[]{extensionBundleId}, new ExtensionBundleVersionEntityRowMapper());
     }
 
     @Override
     public List<ExtensionBundleVersionEntity> getExtensionBundleVersions(final String bucketId, final String groupId, final String artifactId) {
-        final String sql = BASE_EXTENSION_BUNDLE_SQL +
+        final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL +
                     "AND eb.bucket_id = ? " +
                     "AND eb.group_id = ? " +
                     "AND eb.artifact_id = ? ";
@@ -684,7 +753,7 @@
 
     @Override
     public List<ExtensionBundleVersionEntity> getExtensionBundleVersionsGlobal(final String groupId, final String artifactId, final String version) {
-        final String sql = BASE_EXTENSION_BUNDLE_SQL +
+        final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL +
                 "AND eb.group_id = ? " +
                 "AND eb.artifact_id = ? " +
                 "AND ebv.version = ?";
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java
index 71d5a92..fe980d0 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java
@@ -29,6 +29,8 @@
 
     private Date created;
 
+    private boolean allowExtensionBundleRedeploy;
+
 
     public String getId() {
         return id;
@@ -62,6 +64,14 @@
         this.description = description;
     }
 
+    public boolean isAllowExtensionBundleRedeploy() {
+        return allowExtensionBundleRedeploy;
+    }
+
+    public void setAllowExtensionBundleRedeploy(final boolean allowExtensionBundleRedeploy) {
+        this.allowExtensionBundleRedeploy = allowExtensionBundleRedeploy;
+    }
+
     @Override
     public int hashCode() {
         return Objects.hashCode(this.id);
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java
index 08fc2c2..0bc5a59 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java
@@ -26,6 +26,9 @@
     // Foreign key to the extension bundle this version goes with
     private String extensionBundleId;
 
+    // The bucket id where the bundle is located
+    private String bucketId;
+
     // The version of this bundle
     private String version;
 
@@ -40,6 +43,8 @@
     // Indicates whether the SHA-256 was supplied by the client, which means it matched the server's calculation, or was not supplied by the client
     private boolean sha256Supplied;
 
+    // The size of binary content in bytes
+    private long contentSize;
 
     public String getId() {
         return id;
@@ -57,6 +62,14 @@
         this.extensionBundleId = extensionBundleId;
     }
 
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
     public String getVersion() {
         return version;
     }
@@ -104,4 +117,13 @@
     public void setSha256Supplied(boolean sha256Supplied) {
         this.sha256Supplied = sha256Supplied;
     }
+
+    public long getContentSize() {
+        return contentSize;
+    }
+
+    public void setContentSize(long contentSize) {
+        this.contentSize = contentSize;
+    }
+
 }
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java
index 6c5bc2e..5ef5c60 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java
@@ -33,6 +33,7 @@
         b.setName(rs.getString("NAME"));
         b.setDescription(rs.getString("DESCRIPTION"));
         b.setCreated(rs.getTimestamp("CREATED"));
+        b.setAllowExtensionBundleRedeploy(rs.getInt("ALLOW_EXTENSION_BUNDLE_REDEPLOY") == 0 ? false : true);
         return b;
     }
 
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionEntityRowMapper.java
index 60ca48f..4dd47b6 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionEntityRowMapper.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionEntityRowMapper.java
@@ -29,9 +29,11 @@
         final ExtensionBundleVersionEntity entity = new ExtensionBundleVersionEntity();
         entity.setId(rs.getString("ID"));
         entity.setExtensionBundleId(rs.getString("EXTENSION_BUNDLE_ID"));
+        entity.setBucketId(rs.getString("BUCKET_ID"));
         entity.setVersion(rs.getString("VERSION"));
         entity.setSha256Hex(rs.getString("SHA_256_HEX"));
         entity.setSha256Supplied(rs.getInt("SHA_256_SUPPLIED") == 1);
+        entity.setContentSize(rs.getLong("CONTENT_SIZE"));
 
         entity.setCreated(rs.getTimestamp("CREATED"));
         entity.setCreatedBy(rs.getString("CREATED_BY"));
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemExtensionBundlePersistenceProvider.java
index 1d8c9cc..1b29ba9 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemExtensionBundlePersistenceProvider.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemExtensionBundlePersistenceProvider.java
@@ -76,7 +76,7 @@
     }
 
     @Override
-    public synchronized void saveBundleVersion(final ExtensionBundleContext context, final InputStream contentStream)
+    public synchronized void saveBundleVersion(final ExtensionBundleContext context, final InputStream contentStream, boolean overwrite)
             throws ExtensionBundlePersistenceException {
 
         final File bundleVersionDir = getBundleVersionDirectory(bundleStorageDir, context.getBucketName(),
@@ -91,7 +91,7 @@
         final File bundleFile = getBundleFile(bundleVersionDir, context.getBundleArtifactId(),
                 context.getBundleVersion(), context.getBundleType());
 
-        if (bundleFile.exists()) {
+        if (bundleFile.exists() && !overwrite) {
             throw new ExtensionBundlePersistenceException("Unable to save because an extension bundle already exists at "
                     + bundleFile.getAbsolutePath());
         }
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
index 6974cb9..6be1929 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
@@ -53,6 +53,7 @@
         bucketEntity.setName(bucket.getName());
         bucketEntity.setDescription(bucket.getDescription());
         bucketEntity.setCreated(new Date(bucket.getCreatedTimestamp()));
+        bucketEntity.setAllowExtensionBundleRedeploy(bucket.isAllowExtensionBundleRedeploy());
         return bucketEntity;
     }
 
@@ -62,6 +63,7 @@
         bucket.setName(bucketEntity.getName());
         bucket.setDescription(bucketEntity.getDescription());
         bucket.setCreatedTimestamp(bucketEntity.getCreated().getTime());
+        bucket.setAllowExtensionBundleRedeploy(bucketEntity.isAllowExtensionBundleRedeploy());
         return bucket;
     }
 
@@ -224,30 +226,29 @@
         final ExtensionBundleVersionEntity entity = new ExtensionBundleVersionEntity();
         entity.setId(bundleVersionMetadata.getId());
         entity.setExtensionBundleId(bundleVersionMetadata.getExtensionBundleId());
+        entity.setBucketId(bundleVersionMetadata.getBucketId());
         entity.setVersion(bundleVersionMetadata.getVersion());
         entity.setCreated(new Date(bundleVersionMetadata.getTimestamp()));
         entity.setCreatedBy(bundleVersionMetadata.getAuthor());
         entity.setDescription(bundleVersionMetadata.getDescription());
         entity.setSha256Hex(bundleVersionMetadata.getSha256());
         entity.setSha256Supplied(bundleVersionMetadata.getSha256Supplied());
+        entity.setContentSize(bundleVersionMetadata.getContentSize());
         return entity;
     }
 
-    public static ExtensionBundleVersionMetadata map(final BucketEntity bucketEntity, final ExtensionBundleVersionEntity bundleVersionEntity) {
+    public static ExtensionBundleVersionMetadata map(final ExtensionBundleVersionEntity bundleVersionEntity) {
         final ExtensionBundleVersionMetadata bundleVersionMetadata = new ExtensionBundleVersionMetadata();
         bundleVersionMetadata.setId(bundleVersionEntity.getId());
         bundleVersionMetadata.setExtensionBundleId(bundleVersionEntity.getExtensionBundleId());
+        bundleVersionMetadata.setBucketId(bundleVersionEntity.getBucketId());
         bundleVersionMetadata.setVersion(bundleVersionEntity.getVersion());
         bundleVersionMetadata.setTimestamp(bundleVersionEntity.getCreated().getTime());
         bundleVersionMetadata.setAuthor(bundleVersionEntity.getCreatedBy());
         bundleVersionMetadata.setDescription(bundleVersionEntity.getDescription());
         bundleVersionMetadata.setSha256(bundleVersionEntity.getSha256Hex());
         bundleVersionMetadata.setSha256Supplied(bundleVersionEntity.getSha256Supplied());
-
-        if (bucketEntity != null) {
-            bundleVersionMetadata.setBucketId(bucketEntity.getId());
-        }
-
+        bundleVersionMetadata.setContentSize(bundleVersionEntity.getContentSize());
         return bundleVersionMetadata;
     }
 
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
index 1dc90d4..260618d 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
@@ -25,6 +25,8 @@
 import org.apache.nifi.registry.db.entity.ExtensionEntityCategory;
 import org.apache.nifi.registry.db.entity.FlowEntity;
 import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 
 import java.util.List;
 import java.util.Set;
@@ -246,9 +248,10 @@
      * Retrieves all extension bundles in the buckets with the given bucket ids.
      *
      * @param bucketIds the bucket ids
+     * @param filterParams the optional filter params
      * @return the list of all extension bundles in the given buckets
      */
-    List<ExtensionBundleEntity> getExtensionBundles(Set<String> bucketIds);
+    List<ExtensionBundleEntity> getExtensionBundles(Set<String> bucketIds, ExtensionBundleFilterParams filterParams);
 
     /**
      * Retrieves the extension bundles for the given bucket.
@@ -312,6 +315,15 @@
     ExtensionBundleVersionEntity getExtensionBundleVersion(String bucketId, String groupId, String artifactId, String version);
 
     /**
+     * Retrieves the extension bundle versions in the given buckets, matching the optional filter parameters.
+     *
+     * @param bucketIdentifiers the bucket identifiers
+     * @param filterParams the optional filter params
+     * @return the extension bundle versions
+     */
+    List<ExtensionBundleVersionEntity> getExtensionBundleVersions(Set<String> bucketIdentifiers, ExtensionBundleVersionFilterParams filterParams);
+
+    /**
      * Retrieves the extension bundle versions for the given extension bundle id.
      *
      * @param extensionBundleId the extension bundle id
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 091803f..cdfaf79 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
@@ -33,6 +33,8 @@
 import org.apache.nifi.registry.extension.ExtensionBundleType;
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
@@ -141,6 +143,10 @@
         bucket.setIdentifier(UUID.randomUUID().toString());
         bucket.setCreatedTimestamp(System.currentTimeMillis());
 
+        if (bucket.isAllowExtensionBundleRedeploy() == null) {
+            bucket.setAllowExtensionBundleRedeploy(false);
+        }
+
         validate(bucket, "Cannot create Bucket");
 
         writeLock.lock();
@@ -259,6 +265,10 @@
                 existingBucketById.setDescription(bucket.getDescription());
             }
 
+            if (bucket.isAllowExtensionBundleRedeploy() != null) {
+                existingBucketById.setAllowExtensionBundleRedeploy(bucket.isAllowExtensionBundleRedeploy());
+            }
+
             // perform the actual update
             final BucketEntity updatedBucket = metadataService.updateBucket(existingBucketById);
             return DataModelMapper.map(updatedBucket);
@@ -1027,10 +1037,10 @@
         }
     }
 
-    public List<ExtensionBundle> getExtensionBundles(Set<String> bucketIdentifiers) {
+    public List<ExtensionBundle> getExtensionBundles(final Set<String> bucketIdentifiers, final ExtensionBundleFilterParams filterParams) {
         readLock.lock();
         try {
-            return extensionService.getExtensionBundles(bucketIdentifiers);
+            return extensionService.getExtensionBundles(bucketIdentifiers, filterParams);
         } finally {
             readLock.unlock();
         }
@@ -1063,6 +1073,17 @@
         }
     }
 
+    public SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(final Set<String> bucketIdentifiers,
+                                                                                final ExtensionBundleVersionFilterParams filterParams) {
+        readLock.lock();
+        try {
+            return extensionService.getExtensionBundleVersions(bucketIdentifiers, filterParams);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+
     public SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(final String extensionBundleIdentifier) {
         readLock.lock();
         try {
@@ -1072,7 +1093,7 @@
         }
     }
 
-    public ExtensionBundleVersion getExtensionBundleVersion(ExtensionBundleVersionCoordinate versionCoordinate) {
+    public ExtensionBundleVersion getExtensionBundleVersion(final ExtensionBundleVersionCoordinate versionCoordinate) {
         readLock.lock();
         try {
             return extensionService.getExtensionBundleVersion(versionCoordinate);
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java
index 3ab8a07..8ffd4d3 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java
@@ -21,6 +21,8 @@
 import org.apache.nifi.registry.extension.ExtensionBundleType;
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
@@ -35,6 +37,8 @@
 
 public interface ExtensionService {
 
+    // ----- Extension Bundles -----
+
     /**
      * Creates a version of an extension bundle.
      *
@@ -57,9 +61,10 @@
      * Retrieves the extension bundles in the given buckets.
      *
      * @param bucketIdentifiers the bucket identifiers
+     * @param filterParams the optional filter params
      * @return the bundles in the given buckets
      */
-    List<ExtensionBundle> getExtensionBundles(Set<String> bucketIdentifiers);
+    List<ExtensionBundle> getExtensionBundles(Set<String> bucketIdentifiers, ExtensionBundleFilterParams filterParams);
 
     /**
      * Retrieves the extension bundles in the given bucket.
@@ -85,6 +90,18 @@
      */
     ExtensionBundle deleteExtensionBundle(ExtensionBundle extensionBundle);
 
+    // ----- Extension Bundle Versions -----
+
+    /**
+     * Retrieves the extension bundle versions in the given buckets.
+     *
+     * @param bucketIdentifiers the bucket identifiers
+     * @param filterParams the optional filter params
+     * @return the set of extension bundle versions
+     */
+    SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(
+            Set<String> bucketIdentifiers, ExtensionBundleVersionFilterParams filterParams);
+
     /**
      * Retrieves the versions of the given extension bundle.
      *
@@ -119,12 +136,39 @@
 
     // ----- Extension Repo Methods -----
 
+    /**
+     * Retrieves the extension repo buckets for the given bucket ids.
+     *
+     * @param bucketIds the bucket ids
+     * @return the set of buckets
+     */
     SortedSet<ExtensionRepoBucket> getExtensionRepoBuckets(Set<String> bucketIds);
 
+    /**
+     * Retrieves the extension repo groups for the given bucket.
+     *
+     * @param bucket the bucket
+     * @return the groups for the bucket
+     */
     SortedSet<ExtensionRepoGroup> getExtensionRepoGroups(Bucket bucket);
 
+    /**
+     * Retrieves the extension repo artifacts for the given bucket and group.
+     *
+     * @param bucket the bucket
+     * @param groupId the group id
+     * @return the artifacts for the bucket and group
+     */
     SortedSet<ExtensionRepoArtifact> getExtensionRepoArtifacts(Bucket bucket, String groupId);
 
+    /**
+     * Retrieves the extension repo version summaries for the given bucket, group, and artifact.
+     *
+     * @param bucket the bucket
+     * @param groupId the group id
+     * @param artifactId the artifact id
+     * @return the version summaries for the bucket, group, and artifact
+     */
     SortedSet<ExtensionRepoVersionSummary> getExtensionRepoVersions(Bucket bucket, String groupId, String artifactId);
 
 }
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
index e737b75..ac69140 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
@@ -38,6 +38,8 @@
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionDependency;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
@@ -66,6 +68,7 @@
 import java.security.DigestInputStream;
 import java.security.MessageDigest;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -80,6 +83,8 @@
 
     private static final Logger LOGGER = LoggerFactory.getLogger(StandardExtensionService.class);
 
+    static final String SNAPSHOT_VERSION_SUFFIX = "SNAPSHOT";
+
     private final MetadataService metadataService;
     private final Map<ExtensionBundleType, BundleExtractor> extractors;
     private final ExtensionBundlePersistenceProvider bundlePersistenceProvider;
@@ -173,14 +178,18 @@
             final String groupId = bundleCoordinate.getGroupId();
             final String artifactId = bundleCoordinate.getArtifactId();
             final String version = bundleCoordinate.getVersion();
+
+            final boolean isSnapshotVersion = version.endsWith(SNAPSHOT_VERSION_SUFFIX);
+            final boolean overwriteBundleVersion = isSnapshotVersion || existingBucket.isAllowExtensionBundleRedeploy();
+
             LOGGER.debug("Extracted bundle details - '{}' - '{}' - '{}'", new Object[]{groupId, artifactId, version});
 
-            // a bundle with the same group, artifact, and version can exist in multiple buckets, but only if it contains the same binary content,
+            // a bundle with the same group, artifact, and version can exist in multiple buckets, but only if it contains the same binary content, or if its a snapshot version
             // we can determine that by comparing the SHA-256 digest of the incoming bundle against existing bundles with the same group, artifact, version
             final List<ExtensionBundleVersionEntity> allExistingVersions = metadataService.getExtensionBundleVersionsGlobal(groupId, artifactId, version);
             for (final ExtensionBundleVersionEntity existingVersionEntity : allExistingVersions) {
-                if (!existingVersionEntity.getSha256Hex().equals(sha256Hex)) {
-                    throw new IllegalStateException("Found existing extension bundle with same group, artifact, and version, but different SHA-256 check-sum");
+                if (!existingVersionEntity.getSha256Hex().equals(sha256Hex) && !isSnapshotVersion) {
+                    throw new IllegalStateException("Found existing extension bundle with same group, artifact, and version, but different SHA-256 checksums");
                 }
             }
 
@@ -188,11 +197,17 @@
             final long currentTime = System.currentTimeMillis();
             final ExtensionBundleEntity extensionBundle = getOrCreateExtensionBundle(bucketIdentifier, groupId, artifactId, bundleType, currentTime);
 
-            // ensure there isn't already a version of the bundle with the same version
+            // check if the version of incoming bundle already exists in the bucket
+            // if it exists and it is a snapshot version or the bucket allows redeploying, then first delete the row in the extension_bundle_version table so we can create a new one
+            // otherwise we throw an exception because we don't allow the same version in the same bucket
             final ExtensionBundleVersionEntity existingVersion = metadataService.getExtensionBundleVersion(bucketIdentifier, groupId, artifactId, version);
             if (existingVersion != null) {
-                LOGGER.warn("The specified version [{}] already exists for extension bundle [{}].", new Object[]{version, extensionBundle.getId()});
-                throw new IllegalStateException("The specified version already exists for the given extension bundle");
+                if (overwriteBundleVersion) {
+                    metadataService.deleteExtensionBundleVersion(existingVersion);
+                } else {
+                    LOGGER.warn("The specified version [{}] already exists for extension bundle [{}].", new Object[]{version, extensionBundle.getId()});
+                    throw new IllegalStateException("The specified version already exists for the given extension bundle");
+                }
             }
 
             // create the version metadata instance and validate it has all the required fields
@@ -206,6 +221,7 @@
             versionMetadata.setAuthor(userIdentity);
             versionMetadata.setSha256(sha256Hex);
             versionMetadata.setSha256Supplied(sha256Supplied);
+            versionMetadata.setContentSize(extensionWorkingFile.length());
 
             validate(versionMetadata, "Cannot create extension bundle version");
 
@@ -248,9 +264,8 @@
 
             try (final InputStream in = new FileInputStream(extensionWorkingFile);
                  final InputStream bufIn = new BufferedInputStream(in)) {
-                bundlePersistenceProvider.saveBundleVersion(context, bufIn);
-                LOGGER.debug("Bundle saved to persistence provider - '{}' - '{}' - '{}'",
-                        new Object[]{groupId, artifactId, version});
+                bundlePersistenceProvider.saveBundleVersion(context, bufIn, overwriteBundleVersion);
+                LOGGER.debug("Bundle saved to persistence provider - '{}' - '{}' - '{}'", new Object[]{groupId, artifactId, version});
             }
 
             // get the updated extension bundle so it contains the correct version count
@@ -315,12 +330,13 @@
     }
 
     @Override
-    public List<ExtensionBundle> getExtensionBundles(Set<String> bucketIdentifiers) {
+    public List<ExtensionBundle> getExtensionBundles(final Set<String> bucketIdentifiers, final ExtensionBundleFilterParams filterParams) {
         if (bucketIdentifiers == null) {
             throw new IllegalArgumentException("Bucket identifiers cannot be null");
         }
 
-        final List<ExtensionBundleEntity> bundleEntities = metadataService.getExtensionBundles(bucketIdentifiers);
+        final List<ExtensionBundleEntity> bundleEntities = metadataService.getExtensionBundles(bucketIdentifiers,
+                filterParams == null ? ExtensionBundleFilterParams.empty() : filterParams);
         return bundleEntities.stream().map(b -> DataModelMapper.map(null, b)).collect(Collectors.toList());
     }
 
@@ -377,6 +393,26 @@
     }
 
     @Override
+    public SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(final Set<String> bucketIdentifiers,
+                                                                                final ExtensionBundleVersionFilterParams filterParams) {
+        if (bucketIdentifiers == null) {
+            throw new IllegalArgumentException("Bucket identifiers cannot be null");
+        }
+
+        final SortedSet<ExtensionBundleVersionMetadata> sortedVersions = new TreeSet<>(
+                Comparator.comparing(ExtensionBundleVersionMetadata::getExtensionBundleId)
+                        .thenComparing(ExtensionBundleVersionMetadata::getVersion)
+        );
+
+        final List<ExtensionBundleVersionEntity> bundleVersionEntities = metadataService.getExtensionBundleVersions(bucketIdentifiers,
+                filterParams == null ? ExtensionBundleVersionFilterParams.empty() : filterParams);
+        if (bundleVersionEntities != null) {
+            bundleVersionEntities.forEach(bv -> sortedVersions.add(DataModelMapper.map(bv)));
+        }
+        return sortedVersions;
+    }
+
+    @Override
     public SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(final String extensionBundleIdentifier) {
         if (StringUtils.isBlank(extensionBundleIdentifier)) {
             throw new IllegalArgumentException("Extension bundle identifier cannot be null or blank");
@@ -392,15 +428,13 @@
         return getExtensionBundleVersionsSet(existingBundle);
     }
 
-    private SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersionsSet(ExtensionBundleEntity existingBundle) {
+    private SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersionsSet(final ExtensionBundleEntity existingBundle) {
         final SortedSet<ExtensionBundleVersionMetadata> sortedVersions = new TreeSet<>(Collections.reverseOrder());
 
         final List<ExtensionBundleVersionEntity> existingVersions = metadataService.getExtensionBundleVersions(existingBundle.getId());
         if (existingVersions != null) {
-            final BucketEntity existingBucket = metadataService.getBucketById(existingBundle.getBucketId());
-            existingVersions.stream().forEach(s -> sortedVersions.add(DataModelMapper.map(existingBucket, s)));
+            existingVersions.stream().forEach(s -> sortedVersions.add(DataModelMapper.map(s)));
         }
-
         return sortedVersions;
     }
 
@@ -451,7 +485,7 @@
 
         // create the full ExtensionBundleVersion instance to return
         final ExtensionBundleVersion extensionBundleVersion = new ExtensionBundleVersion();
-        extensionBundleVersion.setVersionMetadata(DataModelMapper.map(existingBucket, existingVersion));
+        extensionBundleVersion.setVersionMetadata(DataModelMapper.map(existingVersion));
         extensionBundleVersion.setExtensionBundle(DataModelMapper.map(existingBucket, existingBundle));
         extensionBundleVersion.setBucket(DataModelMapper.map(existingBucket));
         extensionBundleVersion.setDependencies(dependencies);
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V3__AddExtensions.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V3__AddExtensions.sql
index da66cd1..3cfb55a 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V3__AddExtensions.sql
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V3__AddExtensions.sql
@@ -34,6 +34,7 @@
     DESCRIPTION TEXT,
     SHA_256_HEX VARCHAR(512) NOT NULL,
     SHA_256_SUPPLIED INT NOT NULL,
+    CONTENT_SIZE BIGINT NOT NULL,
     CONSTRAINT PK__EXTENSION_BUNDLE_VERSION_ID PRIMARY KEY (ID),
     CONSTRAINT FK__EXTENSION_BUNDLE_VERSION_EXTENSION_BUNDLE_ID FOREIGN KEY (EXTENSION_BUNDLE_ID) REFERENCES EXTENSION_BUNDLE(ID) ON DELETE CASCADE,
     CONSTRAINT UNIQUE__EXTENSION_BUNDLE_VERSION_EXTENSION_BUNDLE_ID_VERSION UNIQUE (EXTENSION_BUNDLE_ID, VERSION)
@@ -68,4 +69,6 @@
     TAG VARCHAR(200) NOT NULL,
     CONSTRAINT PK__EXTENSION_TAG_EXTENSION_ID_AND_TAG PRIMARY KEY (EXTENSION_ID, TAG),
     CONSTRAINT FK__EXTENSION_TAG_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE
-);
\ No newline at end of file
+);
+
+ALTER TABLE BUCKET ADD ALLOW_EXTENSION_BUNDLE_REDEPLOY INT NOT NULL DEFAULT (0);
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
index a2bacd4..66be490 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
@@ -27,6 +27,8 @@
 import org.apache.nifi.registry.db.entity.ExtensionEntityCategory;
 import org.apache.nifi.registry.db.entity.FlowEntity;
 import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.service.MetadataService;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -68,6 +70,7 @@
         assertEquals(b.getName(), createdBucket.getName());
         assertEquals(b.getDescription(), createdBucket.getDescription());
         assertEquals(b.getCreated(), createdBucket.getCreated());
+        assertFalse(b.isAllowExtensionBundleRedeploy());
     }
 
     @Test
@@ -95,12 +98,14 @@
     public void testUpdateBucket() {
         final BucketEntity bucket = metadataService.getBucketById("1");
         assertNotNull(bucket);
+        assertFalse(bucket.isAllowExtensionBundleRedeploy());
 
         final String updatedName = bucket.getName() + " UPDATED";
         final String updatedDesc = bucket.getDescription() + "DESC";
 
         bucket.setName(updatedName);
         bucket.setDescription(updatedDesc);
+        bucket.setAllowExtensionBundleRedeploy(true);
 
         metadataService.updateBucket(bucket);
 
@@ -108,6 +113,7 @@
         assertNotNull(updatedName);
         assertEquals(updatedName, updatedBucket.getName());
         assertEquals(updatedDesc, updatedBucket.getDescription());
+        assertTrue(updatedBucket.isAllowExtensionBundleRedeploy());
     }
 
     @Test
@@ -443,13 +449,13 @@
     }
 
     @Test
-    public void testGetExtensionBundles() {
+    public void testGetExtensionBundlesWithEmptyFilterParams() {
         final Set<String> bucketIds = new HashSet<>();
         bucketIds.add("1");
         bucketIds.add("2");
         bucketIds.add("3");
 
-        final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundles(bucketIds);
+        final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundles(bucketIds, ExtensionBundleFilterParams.empty());
         assertNotNull(bundles);
         assertEquals(3, bundles.size());
 
@@ -460,6 +466,44 @@
     }
 
     @Test
+    public void testGetExtensionBundlesWithFilterParams() {
+        final Set<String> bucketIds = new HashSet<>();
+        bucketIds.add("1");
+        bucketIds.add("2");
+        bucketIds.add("3");
+
+        final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundles(bucketIds,
+                ExtensionBundleFilterParams.empty());
+        assertNotNull(bundles);
+        assertEquals(3, bundles.size());
+
+        final List<ExtensionBundleEntity> bundles2 = metadataService.getExtensionBundles(bucketIds,
+                ExtensionBundleFilterParams.of("org.apache.nifi", null));
+        assertNotNull(bundles2);
+        assertEquals(2, bundles2.size());
+
+        final List<ExtensionBundleEntity> bundles3 = metadataService.getExtensionBundles(bucketIds,
+                ExtensionBundleFilterParams.of("org.apache.%", null));
+        assertNotNull(bundles3);
+        assertEquals(2, bundles3.size());
+
+        final List<ExtensionBundleEntity> bundles4 = metadataService.getExtensionBundles(bucketIds,
+                ExtensionBundleFilterParams.of("org.apache.nifi", "nifi-example-processors-nar"));
+        assertNotNull(bundles4);
+        assertEquals(1, bundles4.size());
+
+        final List<ExtensionBundleEntity> bundles5 = metadataService.getExtensionBundles(bucketIds,
+                ExtensionBundleFilterParams.of("org.apache.nifi", "nifi-example-processors-%"));
+        assertNotNull(bundles5);
+        assertEquals(1, bundles5.size());
+
+        final List<ExtensionBundleEntity> bundles6 = metadataService.getExtensionBundles(bucketIds,
+                ExtensionBundleFilterParams.of(null, "nifi-example-processors-%"));
+        assertNotNull(bundles6);
+        assertEquals(1, bundles6.size());
+    }
+
+    @Test
     public void testGetExtensionBundlesByBucket() {
         final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundlesByBucket("3");
         assertNotNull(bundles);
@@ -474,7 +518,7 @@
     public void testGetExtensionBundlesByBucketAndGroup() {
         final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundlesByBucketAndGroup("3", "org.apache.nifi");
         assertNotNull(bundles);
-        assertEquals(3, bundles.size());
+        assertEquals(2, bundles.size());
 
         final List<ExtensionBundleEntity> bundles2 = metadataService.getExtensionBundlesByBucketAndGroup("3", "does-not-exist");
         assertNotNull(bundles2);
@@ -547,6 +591,7 @@
         bundleVersion.setDescription("This is v1.1.0");
         bundleVersion.setSha256Hex("123456789");
         bundleVersion.setSha256Supplied(false);
+        bundleVersion.setContentSize(2048);
 
         metadataService.createExtensionBundleVersion(bundleVersion);
 
@@ -557,6 +602,61 @@
     }
 
     @Test
+    public void testGetExtensionBundleVersionsWithEmptyBucketIdsAndEmptyFilterParams() {
+        final List<ExtensionBundleVersionEntity> versionEntities = metadataService.getExtensionBundleVersions(
+                Collections.emptySet(), ExtensionBundleVersionFilterParams.empty());
+        assertEquals(0, versionEntities.size());
+    }
+
+    @Test
+    public void testGetExtensionBundleVersionsWithEmptyFilterParams() {
+        final Set<String> bucketIds = new HashSet<>();
+        bucketIds.add("1");
+        bucketIds.add("2");
+        bucketIds.add("3");
+
+        final List<ExtensionBundleVersionEntity> versionEntities = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.empty());
+        assertEquals(3, versionEntities.size());
+    }
+
+    @Test
+    public void testGetExtensionBundleVersionsWithFilterParams() {
+        final Set<String> bucketIds = new HashSet<>();
+        bucketIds.add("1");
+        bucketIds.add("2");
+        bucketIds.add("3");
+
+        final List<ExtensionBundleVersionEntity> versionEntities = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.of("org.apache.nifi", null, null));
+        assertEquals(2, versionEntities.size());
+
+        final List<ExtensionBundleVersionEntity> versionEntities2 = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.of("org.apache.%", null, null));
+        assertEquals(2, versionEntities2.size());
+
+        final List<ExtensionBundleVersionEntity> versionEntities3 = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", null));
+        assertEquals(1, versionEntities3.size());
+
+        final List<ExtensionBundleVersionEntity> versionEntities4 = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-%", null));
+        assertEquals(1, versionEntities4.size());
+
+        final List<ExtensionBundleVersionEntity> versionEntities5 = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", "1.0.0"));
+        assertEquals(1, versionEntities5.size());
+
+        final List<ExtensionBundleVersionEntity> versionEntities6 = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", "1.0.%"));
+        assertEquals(1, versionEntities6.size());
+
+        final List<ExtensionBundleVersionEntity> versionEntities7 = metadataService.getExtensionBundleVersions(
+                bucketIds, ExtensionBundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", "NOT-FOUND"));
+        assertEquals(0, versionEntities7.size());
+    }
+
+    @Test
     public void testGetExtensionBundleVersionByBundleIdAndVersion() {
         final ExtensionBundleVersionEntity bundleVersion = metadataService.getExtensionBundleVersion("eb1", "1.0.0");
         assertNotNull(bundleVersion);
@@ -567,6 +667,7 @@
         assertEquals("user1", bundleVersion.getCreatedBy());
         assertEquals("First version of eb1", bundleVersion.getDescription());
         assertTrue(bundleVersion.getSha256Supplied());
+        assertEquals(1024, bundleVersion.getContentSize());
     }
 
     @Test
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockExtensionBundlePersistenceProvider.java
index ba7f12f..42c5c49 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockExtensionBundlePersistenceProvider.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockExtensionBundlePersistenceProvider.java
@@ -29,7 +29,7 @@
     private Map<String,String> properties;
 
     @Override
-    public void saveBundleVersion(ExtensionBundleContext context, InputStream contentStream)
+    public void saveBundleVersion(ExtensionBundleContext context, InputStream contentStream, boolean overwrite)
             throws ExtensionBundlePersistenceException {
 
     }
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemExtensionBundlePersistenceProvider.java
index 5a611bb..ba7b3ab 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemExtensionBundlePersistenceProvider.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemExtensionBundlePersistenceProvider.java
@@ -254,7 +254,7 @@
         final ExtensionBundleContext context = getExtensionBundleContext(bucketName, groupId, artifactId, version, bundleType);
 
         try (final InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
-            persistenceProvider.saveBundleVersion(context, in);
+            persistenceProvider.saveBundleVersion(context, in, false);
         }
     }
 
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql b/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
index b8e0d3a..80b58cb 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
@@ -112,7 +112,8 @@
   created_by,
   description,
   sha_256_hex,
-  sha_256_supplied
+  sha_256_supplied,
+  content_size
 ) values (
   'eb1-v1',
   'eb1',
@@ -121,7 +122,8 @@
   'user1',
   'First version of eb1',
   '123456789',
-  '1'
+  '1',
+  1024
 );
 
 insert into extension_bundle_version_dependency (
@@ -167,7 +169,7 @@
   'eb2',
   '3',
   'NIFI_NAR',
-  'org.apache.nifi',
+  'com.foo',
   'nifi-example-services-nar'
 );
 
@@ -179,7 +181,8 @@
   created_by,
   description,
   sha_256_hex,
-  sha_256_supplied
+  sha_256_supplied,
+  content_size
 ) values (
   'eb2-v1',
   'eb2',
@@ -188,7 +191,8 @@
   'user1',
   'First version of eb2',
   '123456789',
-  '1'
+  '1',
+  1024
 );
 
 insert into extension_bundle_version_dependency (
@@ -246,7 +250,8 @@
   created_by,
   description,
   sha_256_hex,
-  sha_256_supplied
+  sha_256_supplied,
+  content_size
 ) values (
   'eb3-v1',
   'eb3',
@@ -255,7 +260,8 @@
   'user1',
   'First version of eb3',
   '123456789',
-  '1'
+  '1',
+  1024
 );
 
 -- test data for extensions
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceProvider.java
index 9ef2646..6116d97 100644
--- a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceProvider.java
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceProvider.java
@@ -31,9 +31,11 @@
      *
      * @param context the context about the bundle version being persisted
      * @param contentStream the stream of binary content to persist
+     * @param overwrite if true the persistence provider should overwrite any content that may already exist for the given bundle version,
+     *                  if false the persistence provider should throw an ExtensionBundlePersistenceException if content already exists
      * @throws ExtensionBundlePersistenceException if an error occurs storing the content
      */
-    void saveBundleVersion(ExtensionBundleContext context, InputStream contentStream) throws ExtensionBundlePersistenceException;
+    void saveBundleVersion(ExtensionBundleContext context, InputStream contentStream, boolean overwrite) throws ExtensionBundlePersistenceException;
 
     /**
      * Writes the binary content of the bundle specified by the bucket-group-artifact-version to the provided OutputStream.
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepositoryResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepositoryResource.java
index 46be907..02820db 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepositoryResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepositoryResource.java
@@ -24,10 +24,13 @@
 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.bucket.Bucket;
-import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
@@ -97,7 +100,7 @@
         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();
+            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
         }
 
         final SortedSet<ExtensionRepoBucket> repoBuckets = registryService.getExtensionRepoBuckets(authorizedBucketIds);
@@ -370,5 +373,66 @@
         return Response.ok(sha256Hex, MediaType.TEXT_PLAIN).build();
     }
 
+    @GET
+    @Path("{groupId}/{artifactId}/{version}/sha256")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.TEXT_PLAIN)
+    @ApiOperation(
+            value = "Gets the hex representation of the SHA-256 digest for the binary content of the version of the extension bundle. Since the " +
+                    "same group-artifact-version can exist in multiple buckets, this will return the checksum of the first one returned. This will be " +
+                    "consistent since the checksum must be the same when existing in multiple buckets.",
+            response = String.class
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getExtensionBundleVersionSha256(
+            @PathParam("groupId")
+            @ApiParam("The group identifier")
+                final String groupId,
+            @PathParam("artifactId")
+            @ApiParam("The artifact identifier")
+                final String artifactId,
+            @PathParam("version")
+            @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)) {
+            throw new IllegalArgumentException("Group id cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(artifactId)) {
+            throw new IllegalArgumentException("Artifact id cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(version)) {
+            throw new IllegalArgumentException("Version cannot be null or blank");
+        }
+
+        final ExtensionBundleVersionFilterParams filterParams = ExtensionBundleVersionFilterParams.of(groupId, artifactId, version);
+
+        final SortedSet<ExtensionBundleVersionMetadata> bundleVersions = registryService.getExtensionBundleVersions(authorizedBucketIds, filterParams);
+        if (bundleVersions.isEmpty()) {
+            throw new ResourceNotFoundException("An extension bundle version does not exist with the specific group, artifact, and version");
+        } else {
+            ExtensionBundleVersionMetadata latestVersionMetadata = null;
+            for (ExtensionBundleVersionMetadata versionMetadata : bundleVersions) {
+                if (latestVersionMetadata == null || versionMetadata.getTimestamp() > latestVersionMetadata.getTimestamp()) {
+                    latestVersionMetadata = versionMetadata;
+                }
+            }
+            return Response.ok(latestVersionMetadata.getSha256(), MediaType.TEXT_PLAIN).build();
+        }
+    }
 }
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 62c4c08..d8c1e7a 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
@@ -25,12 +25,13 @@
 import io.swagger.annotations.Extension;
 import io.swagger.annotations.ExtensionProperty;
 import org.apache.commons.lang3.StringUtils;
-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.extension.ExtensionBundle;
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.service.AuthorizationService;
 import org.apache.nifi.registry.service.RegistryService;
@@ -46,6 +47,7 @@
 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 javax.ws.rs.core.StreamingOutput;
@@ -81,6 +83,8 @@
         this.permissionsService = permissionsService;
     }
 
+    // ---------- Extension Bundles ----------
+
     @GET
     @Path("bundles")
     @Consumes(MediaType.WILDCARD)
@@ -93,15 +97,25 @@
             responseContainer = "List"
     )
     @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
-    public Response getExtensionBundles() {
+    public Response getExtensionBundles(
+            @QueryParam("groupId")
+            @ApiParam("Optional groupId to filter results. The value may be an exact match, or a wildcard, " +
+                    "such as 'com.%' to select all bundles where the groupId starts with 'com.'.")
+                final String groupId,
+            @QueryParam("artifactId")
+            @ApiParam("Optional artifactId to filter results. The value may be an exact match, or a wildcard, " +
+                    "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<BucketItem>()).build();
+            return Response.status(Response.Status.OK).entity(new ArrayList<>()).build();
         }
 
-        List<ExtensionBundle> bundles = registryService.getExtensionBundles(authorizedBucketIds);
+        final ExtensionBundleFilterParams filterParams = ExtensionBundleFilterParams.of(groupId, artifactId);
+
+        List<ExtensionBundle> bundles = registryService.getExtensionBundles(authorizedBucketIds, filterParams);
         if (bundles == null) {
             bundles = Collections.emptyList();
         }
@@ -180,6 +194,48 @@
         return Response.status(Response.Status.OK).entity(deletedExtensionBundle).build();
     }
 
+    // ---------- Extension Bundle Versions ----------
+
+    @GET
+    @Path("bundles/versions")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Get extension bundles versions across all authorized buckets",
+            notes = "The returned items will include only items from buckets for which the user is authorized. " +
+                    "If the user is not authorized to any buckets, an empty list will be returned.",
+            response = ExtensionBundleVersionMetadata.class,
+            responseContainer = "List"
+    )
+    @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
+    public Response getExtensionBundleVersions(
+            @QueryParam("groupId")
+            @ApiParam("Optional groupId to filter results. The value may be an exact match, or a wildcard, " +
+                    "such as 'com.%' to select all bundle versions where the groupId starts with 'com.'.")
+                final String groupId,
+            @QueryParam("artifactId")
+            @ApiParam("Optional artifactId to filter results. The value may be an exact match, or a wildcard, " +
+                    "such as 'nifi-%' to select all bundle versions where the artifactId starts with 'nifi-'.")
+                final String artifactId,
+            @QueryParam("version")
+            @ApiParam("Optional version to filter results. The value maye be an exact match, or a wildcard, " +
+                    "such as '1.0.%' to select all bundle versions where the version starts with '1.0.'.")
+                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 ExtensionBundleVersionFilterParams filterParams = ExtensionBundleVersionFilterParams.of(groupId, artifactId, version);
+        final SortedSet<ExtensionBundleVersionMetadata> bundleVersions = registryService.getExtensionBundleVersions(authorizedBucketIds, filterParams);
+        linkService.populateLinks(bundleVersions);
+
+        return Response.status(Response.Status.OK).entity(bundleVersions).build();
+    }
+
     @GET
     @Path("bundles/{bundleId}/versions")
     @Consumes(MediaType.WILDCARD)
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 2fb2f4a..d5c0cf5 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
@@ -41,6 +41,8 @@
 import org.apache.nifi.registry.extension.ExtensionBundleVersion;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionDependency;
 import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleFilterParams;
+import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
 import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
@@ -76,6 +78,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -161,6 +164,7 @@
         for (final Bucket bucket : createdBuckets) {
             final Bucket retrievedBucket = bucketClient.get(bucket.getIdentifier());
             Assert.assertNotNull(retrievedBucket);
+            Assert.assertFalse(retrievedBucket.isAllowExtensionBundleRedeploy());
             LOGGER.info("Retrieved bucket " + retrievedBucket.getIdentifier());
         }
 
@@ -328,6 +332,7 @@
         Assert.assertEquals(0, allBundles.size());
 
         final Bucket bundlesBucket = createdBuckets.get(1);
+        final Bucket bundlesBucket2 = createdBuckets.get(2);
         final ExtensionBundleVersionClient bundleVersionClient = client.getExtensionBundleVersionClient();
 
         // create version 1.0.0 of nifi-test-nar
@@ -358,6 +363,7 @@
         Assert.assertEquals(bundlesBucket.getIdentifier(), testNarV1Metadata.getBucketId());
         Assert.assertTrue(testNarV1Metadata.getTimestamp() > 0);
         Assert.assertFalse(testNarV1Metadata.getSha256Supplied());
+        Assert.assertTrue(testNarV1Metadata.getContentSize() > 1);
 
         final Set<ExtensionBundleVersionDependency> dependencies = createdTestNarV1.getDependencies();
         Assert.assertNotNull(dependencies);
@@ -395,10 +401,52 @@
         final ExtensionBundle fooNarV1Bundle = createdFooNarV1.getExtensionBundle();
         LOGGER.info("Created bundle with id {}", new Object[]{fooNarV1Bundle.getIdentifier()});
 
+        // verify that bucket 1 currently does not allow redeploying non-snapshot artifacts
+        Assert.assertFalse(bundlesBucket.isAllowExtensionBundleRedeploy());
+
+        // try to re-deploy version 1.0.0 of nifi-foo-nar, should fail
+        try {
+            createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNar, null);
+            Assert.fail("Should have thrown exception when re-deploying foo nar");
+        } catch (Exception e) {
+            // Should throw exception
+        }
+
+        // now update bucket 1 to allow redeploy
+        bundlesBucket.setAllowExtensionBundleRedeploy(true);
+        final Bucket updatedBundlesBucket = bucketClient.update(bundlesBucket);
+        Assert.assertTrue(updatedBundlesBucket.isAllowExtensionBundleRedeploy());
+
+        // try to re-deploy version 1.0.0 of nifi-foo-nar again, this time should work
+        Assert.assertNotNull(createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNar, null));
+
         // verify there are 2 bundles now
         final List<ExtensionBundle> allBundlesAfterCreate = bundleClient.getAll();
         Assert.assertEquals(2, allBundlesAfterCreate.size());
 
+        // create version 2.0.0-SNAPSHOT (build 1 content) of nifi-foor-nar in the first bucket
+        final String fooNarV2SnapshotB1 = "src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar";
+        final ExtensionBundleVersion createdFooNarV2SnapshotB1 = createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNarV2SnapshotB1, null);
+        Assert.assertFalse(createdFooNarV2SnapshotB1.getVersionMetadata().getSha256Supplied());
+
+        // create version 2.0.0-SNAPSHOT (build 2 content) of nifi-foor-nar in the second bucket
+        // proves that snapshots can have different checksums across buckets, non-snapshots can't
+        final String fooNarV2SnapshotB2 = "src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar";
+        final ExtensionBundleVersion createdFooNarV2SnapshotB2 = createExtensionBundleVersionWithFile(bundlesBucket2, bundleVersionClient, fooNarV2SnapshotB2, null);
+        Assert.assertFalse(createdFooNarV2SnapshotB2.getVersionMetadata().getSha256Supplied());
+
+        // create version 2.0.0-SNAPSHOT (build 2 content) of nifi-foor-nar in the second bucket
+        // proves that we can overwrite a snapshot in a given bucket
+        final String fooNarV2SnapshotB3 = "src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar";
+        final ExtensionBundleVersion createdFooNarV2SnapshotB3 = createExtensionBundleVersionWithFile(bundlesBucket2, bundleVersionClient, fooNarV2SnapshotB3, null);
+        Assert.assertFalse(createdFooNarV2SnapshotB3.getVersionMetadata().getSha256Supplied());
+
+        // verify retrieving nifi-foo-nar 2.0.0-SNAPSHOT from second bucket returns the build 3 content
+        final ExtensionBundleVersion retrievedFooNarV2SnapshotB3 = bundleVersionClient.getBundleVersion(
+                createdFooNarV2SnapshotB3.getVersionMetadata().getExtensionBundleId(),
+                createdFooNarV2SnapshotB3.getVersionMetadata().getVersion());
+        Assert.assertEquals(calculateSha256Hex(fooNarV2SnapshotB3), retrievedFooNarV2SnapshotB3.getVersionMetadata().getSha256());
+
         // verify getting bundles by bucket
         Assert.assertEquals(2, bundleClient.getByBucket(bundlesBucket.getIdentifier()).size());
         Assert.assertEquals(0, bundleClient.getByBucket(flowsBucket.getIdentifier()).size());
@@ -455,6 +503,23 @@
             // should catch exception
         }
 
+        // Verify getting bundles with filter params
+        Assert.assertEquals(3, bundleClient.getAll(ExtensionBundleFilterParams.empty()).size());
+
+        final List<ExtensionBundle> filteredBundles = bundleClient.getAll(ExtensionBundleFilterParams.of("org.apache.nifi", "nifi-test-nar"));
+        Assert.assertEquals(1, filteredBundles.size());
+
+        // Verify getting bundle versions with filter params
+        Assert.assertEquals(4, bundleVersionClient.getBundleVersions(ExtensionBundleVersionFilterParams.empty()).size());
+
+        final List<ExtensionBundleVersionMetadata> filteredVersions = bundleVersionClient.getBundleVersions(
+                ExtensionBundleVersionFilterParams.of("org.apache.nifi", "nifi-foo-nar", "1.0.0"));
+        Assert.assertEquals(1, filteredVersions.size());
+
+        final List<ExtensionBundleVersionMetadata> filteredVersions2 = bundleVersionClient.getBundleVersions(
+                ExtensionBundleVersionFilterParams.of("org.apache.nifi", null, null));
+        Assert.assertEquals(4, filteredVersions2.size());
+
         // ---------------------- TEST EXTENSION REPO ----------------------//
 
         final ExtensionRepoClient extensionRepoClient = client.getExtensionRepoClient();
@@ -499,10 +564,26 @@
         // verify the client methods for content input stream and content sha256
         try (final InputStream repoVersionInputStream = extensionRepoClient.getVersionContent(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString)) {
             final String sha256Hex = DigestUtils.sha256Hex(repoVersionInputStream);
+
             final String repoSha256Hex = extensionRepoClient.getVersionSha256(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString);
             Assert.assertEquals(sha256Hex, repoSha256Hex);
+
+            final Optional<String> repoSha256HexOptional = extensionRepoClient.getVersionSha256(repoGroupId, repoArtifactId, repoVersionString);
+            Assert.assertTrue(repoSha256HexOptional.isPresent());
+            Assert.assertEquals(sha256Hex, repoSha256HexOptional.get());
         }
 
+        final Optional<String> repoSha256HexDoesNotExist = extensionRepoClient.getVersionSha256(repoGroupId, repoArtifactId, "DOES-NOT-EXIST");
+        Assert.assertFalse(repoSha256HexDoesNotExist.isPresent());
+
+        // since we uploaded two snapshot versions, make sure when we retrieve the sha that it's for the second snapshot that replaced the first
+        final Optional<String> fooNarV2SnapshotLatestSha = extensionRepoClient.getVersionSha256(
+                createdFooNarV2SnapshotB3.getExtensionBundle().getGroupId(),
+                createdFooNarV2SnapshotB3.getExtensionBundle().getArtifactId(),
+                createdFooNarV2SnapshotB2.getVersionMetadata().getVersion());
+        Assert.assertTrue(fooNarV2SnapshotLatestSha.isPresent());
+        Assert.assertEquals(calculateSha256Hex(fooNarV2SnapshotB3), fooNarV2SnapshotLatestSha.get());
+
         // ---------------------- TEST ITEMS -------------------------- //
 
         final ItemsClient itemsClient = client.getItemsClient();
@@ -514,7 +595,7 @@
 
         // get all items
         final List<BucketItem> allItems = itemsClient.getAll();
-        Assert.assertEquals(4, allItems.size());
+        Assert.assertEquals(5, allItems.size());
         allItems.stream().forEach(i -> {
             Assert.assertNotNull(i.getBucketName());
             Assert.assertNotNull(i.getLink());
@@ -527,11 +608,11 @@
                 .collect(Collectors.toList());
         Assert.assertEquals(2, flowItems.size());
 
-        // verify 2 bundle items
+        // verify 3 bundle items
         final List<BucketItem> extensionBundleItems = allItems.stream()
                 .filter(i -> i.getType() == BucketItemType.Extension_Bundle)
                 .collect(Collectors.toList());
-        Assert.assertEquals(2, extensionBundleItems.size());
+        Assert.assertEquals(3, extensionBundleItems.size());
 
         // get items for bucket
         final List<BucketItem> bucketItems = itemsClient.getByBucket(flowsBucket.getIdentifier());
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar
new file mode 100644
index 0000000..9a0743a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar
new file mode 100644
index 0000000..d23eb98
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar
new file mode 100644
index 0000000..f114b12
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar
Binary files differ
diff --git a/pom.xml b/pom.xml
index 323c703..25fa752 100644
--- a/pom.xml
+++ b/pom.xml
@@ -94,8 +94,8 @@
         <jax.rs.api.version>2.1</jax.rs.api.version>
         <jersey.version>2.27</jersey.version>
         <jackson.version>2.9.7</jackson.version>
-        <spring.boot.version>2.1.0.RELEASE</spring.boot.version>
-        <spring.security.version>5.1.1.RELEASE</spring.security.version>
+        <spring.boot.version>2.1.1.RELEASE</spring.boot.version>
+        <spring.security.version>5.1.2.RELEASE</spring.security.version>
         <flyway.version>5.2.1</flyway.version>
         <flyway.tests.version>5.1.0</flyway.tests.version>
         <swagger.ui.version>3.12.0</swagger.ui.version>