NIFIREG-206 Support anonymous access to public resources
- Enabled anonymous user filter in all modes
- Added supportsLogin flag to CurrentUser
- Moved proxy authorization into FrameworkAuthorizer
- Added public read flag to bucket table and updated services
- Added logic for public buckets into framework authorizer and authorization service
- Added unit tests for FrameworkAuthorizer
- Updated front-end to display login link and allow anonymous user to reach login page
- Added close button to login form
- Added public checkbox to create bucket page
- Addressing review feedback
- Ensuring new checkbox is disabled when running unsecurely
- Removing unused code

This closes #197.
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java
index 1bd4d4e..af135ec 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java
@@ -24,6 +24,7 @@
 
     private String identity;
     private boolean anonymous;
+    private boolean loginSupported;
     private ResourcePermissions resourcePermissions;
 
     @ApiModelProperty(value = "The identity of the current user", readOnly = true)
@@ -44,6 +45,15 @@
         this.anonymous = anonymous;
     }
 
+    @ApiModelProperty(value = "Indicates if the NiFi instance supports logging in")
+    public boolean isLoginSupported() {
+        return loginSupported;
+    }
+
+    public void setLoginSupported(boolean loginSupported) {
+        this.loginSupported = loginSupported;
+    }
+
     @ApiModelProperty(value = "The access that the current user has to top level resources", readOnly = true)
     public ResourcePermissions getResourcePermissions() {
         return resourcePermissions;
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 6837a6d..9cd82e0 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
@@ -43,6 +43,8 @@
 
     private Boolean allowBundleRedeploy;
 
+    private Boolean allowPublicRead;
+
     private Permissions permissions;
 
     @ApiModelProperty(value = "An ID to uniquely identify this object.", readOnly = true)
@@ -90,6 +92,15 @@
         this.allowBundleRedeploy = allowBundleRedeploy;
     }
 
+    @ApiModelProperty("Indicates if this bucket allows read access to unauthenticated anonymous users")
+    public Boolean isAllowPublicRead() {
+        return allowPublicRead;
+    }
+
+    public void setAllowPublicRead(final Boolean allowPublicRead) {
+        this.allowPublicRead = allowPublicRead;
+    }
+
     @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-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 3f75439..63a4bd1 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
@@ -76,13 +76,14 @@
 
     @Override
     public BucketEntity createBucket(final BucketEntity b) {
-        final String sql = "INSERT INTO BUCKET (ID, NAME, DESCRIPTION, CREATED, ALLOW_EXTENSION_BUNDLE_REDEPLOY) VALUES (?, ?, ?, ?, ?)";
+        final String sql = "INSERT INTO BUCKET (ID, NAME, DESCRIPTION, CREATED, ALLOW_EXTENSION_BUNDLE_REDEPLOY, ALLOW_PUBLIC_READ) VALUES (?, ?, ?, ?, ?, ?)";
         jdbcTemplate.update(sql,
                 b.getId(),
                 b.getName(),
                 b.getDescription(),
                 b.getCreated(),
-                b.isAllowExtensionBundleRedeploy() ? 1 : 0);
+                b.isAllowExtensionBundleRedeploy() ? 1 : 0,
+                b.isAllowPublicRead() ? 1 : 0);
         return b;
     }
 
@@ -104,8 +105,20 @@
 
     @Override
     public BucketEntity updateBucket(final BucketEntity bucket) {
-        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());
+        final String sql = "UPDATE BUCKET SET " +
+                    "name = ?, " +
+                    "description = ?, " +
+                    "allow_extension_bundle_redeploy = ?, " +
+                    "allow_public_read = ? " +
+                "WHERE id = ?";
+
+        jdbcTemplate.update(sql,
+                bucket.getName(),
+                bucket.getDescription(),
+                bucket.isAllowExtensionBundleRedeploy() ? 1 : 0,
+                bucket.isAllowPublicRead() ? 1 : 0,
+                bucket.getId());
+
         return bucket;
     }
 
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 fe980d0..b6bf12b 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
@@ -31,6 +31,8 @@
 
     private boolean allowExtensionBundleRedeploy;
 
+    private boolean allowPublicRead;
+
 
     public String getId() {
         return id;
@@ -72,6 +74,14 @@
         this.allowExtensionBundleRedeploy = allowExtensionBundleRedeploy;
     }
 
+    public boolean isAllowPublicRead() {
+        return allowPublicRead;
+    }
+
+    public void setAllowPublicRead(boolean allowPublicRead) {
+        this.allowPublicRead = allowPublicRead;
+    }
+
     @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/mapper/BucketEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java
index 5ef5c60..5bda0a8 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
@@ -34,6 +34,7 @@
         b.setDescription(rs.getString("DESCRIPTION"));
         b.setCreated(rs.getTimestamp("CREATED"));
         b.setAllowExtensionBundleRedeploy(rs.getInt("ALLOW_EXTENSION_BUNDLE_REDEPLOY") == 0 ? false : true);
+        b.setAllowPublicRead(rs.getInt("ALLOW_PUBLIC_READ") == 0 ? false : true);
         return b;
     }
 
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
index 43862fe..916892b 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
@@ -31,6 +31,7 @@
 import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
 import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
 import org.apache.nifi.registry.security.util.XmlUtils;
+import org.apache.nifi.registry.service.RegistryService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.DisposableBean;
@@ -88,6 +89,7 @@
     private final NiFiRegistryProperties properties;
     private final ExtensionManager extensionManager;
     private final SensitivePropertyProvider sensitivePropertyProvider;
+    private final RegistryService registryService;
 
     private Authorizer authorizer;
     private final Map<String, UserGroupProvider> userGroupProviders = new HashMap<>();
@@ -98,11 +100,13 @@
     public AuthorizerFactory(
             final NiFiRegistryProperties properties,
             final ExtensionManager extensionManager,
-            @Nullable final SensitivePropertyProvider sensitivePropertyProvider) {
+            @Nullable final SensitivePropertyProvider sensitivePropertyProvider,
+            final RegistryService registryService) {
 
         this.properties = properties;
         this.extensionManager = extensionManager;
         this.sensitivePropertyProvider = sensitivePropertyProvider;
+        this.registryService = registryService;
 
         if (this.properties == null) {
             throw new IllegalStateException("NiFiRegistryProperties cannot be null");
@@ -111,6 +115,10 @@
         if (this.extensionManager == null) {
             throw new IllegalStateException("ExtensionManager cannot be null");
         }
+
+        if (this.registryService == null) {
+            throw new IllegalStateException("RegistryService cannot be null");
+        }
     }
 
     /***** UserGroupProviderLookup *****/
@@ -354,10 +362,22 @@
         // call post construction lifecycle event
         instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
 
-        return installIntegrityChecks(instance);
+        // wrap the instance Authorizer with checks to ensure integrity of data
+        final Authorizer integrityCheckAuthorizer = installIntegrityChecks(instance);
+
+        // wrap the integrity checked Authorizer with the FrameworkAuthorizer
+        return createFrameworkAuthorizer(integrityCheckAuthorizer);
     }
 
-        private void performMethodInjection(final Object instance, final Class authorizerClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+    private Authorizer createFrameworkAuthorizer(final Authorizer baseAuthorizer) {
+        if (baseAuthorizer instanceof ManagedAuthorizer) {
+            return new FrameworkManagedAuthorizer((ManagedAuthorizer) baseAuthorizer, registryService);
+        } else {
+            return new FrameworkAuthorizer(baseAuthorizer, registryService);
+        }
+    }
+
+    private void performMethodInjection(final Object instance, final Class authorizerClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
         for (final Method method : authorizerClass.getMethods()) {
             if (method.isAnnotationPresent(AuthorizerContext.class)) {
                 // make the method accessible
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkAuthorizer.java
new file mode 100644
index 0000000..08fb8f0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkAuthorizer.java
@@ -0,0 +1,189 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.service.RegistryService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Wraps an Authorizer and adds framework level logic for authorizing proxies, public resources, and anything else
+ * that needs to be done on top of the regular Authorizer.
+ */
+public class FrameworkAuthorizer implements Authorizer {
+
+    public static Logger LOGGER = LoggerFactory.getLogger(FrameworkAuthorizer.class);
+
+    private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getProxyResource();
+        }
+    };
+
+    private final Authorizer wrappedAuthorizer;
+    private final RegistryService registryService;
+
+    public FrameworkAuthorizer(final Authorizer wrappedAuthorizer, final RegistryService registryService) {
+        this.wrappedAuthorizer = Objects.requireNonNull(wrappedAuthorizer);
+        this.registryService = Objects.requireNonNull(registryService);
+    }
+
+    @Override
+    public void initialize(final AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException {
+        wrappedAuthorizer.initialize(initializationContext);
+    }
+
+    @Override
+    public void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        wrappedAuthorizer.onConfigured(configurationContext);
+    }
+
+    @Override
+    public AuthorizationResult authorize(final AuthorizationRequest request) throws AuthorizationAccessException {
+        final Resource resource = request.getResource();
+        final RequestAction requestAction = request.getAction();
+
+        /**
+         * If the request is for a resource that has been made public and action is READ, then it should automatically be authorized.
+         *
+         * This needs to be checked before the proxy authorizations b/c access to a public resource should always be allowed.
+         */
+
+        final boolean allowPublicAccess = isPublicAccessAllowed(resource, requestAction);
+        if (allowPublicAccess) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Authorizing access to public resource '{}'", new Object[]{resource.getIdentifier()});
+            }
+            return AuthorizationResult.approved();
+        }
+
+        /**
+         * Deny an anonymous user access to anything else, they should only have access to publicly readable resources checked above
+         */
+
+        if (request.isAnonymous()) {
+            return AuthorizationResult.denied("Anonymous access is not authorized");
+        }
+
+        /*
+        * If the request has a proxy chain, ensure each identity in the chain is an authorized proxy for the given action.
+        *
+        * The action comes from the original request. For example, if user1 is proxied by proxy1, and it is a WRITE request
+        * to /buckets/12345, then we need to determine if proxy1 is authorized to proxy WRITE requests.
+        */
+
+        final List<String> proxyChainIdentities = request.getProxyIdentities();
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Found {} proxy identities", new Object[]{proxyChainIdentities.size()});
+        }
+
+        for (final String proxyIdentity : proxyChainIdentities) {
+            final NiFiUser proxyNiFiUser = createProxyNiFiUser(proxyIdentity);
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Authorizing proxy [{}] for {}", new Object[]{proxyIdentity, requestAction});
+            }
+
+            try {
+                PROXY_AUTHORIZABLE.authorize(wrappedAuthorizer, requestAction, proxyNiFiUser);
+            } catch (final AccessDeniedException e) {
+                final String actionString = requestAction.toString();
+                return AuthorizationResult.denied(String.format("Untrusted proxy [%s] for %s operation.", proxyIdentity, actionString));
+            }
+        }
+
+        /**
+         * All other authorization decisions need to be delegated to the original wrapped Authorizer.
+         */
+
+        return wrappedAuthorizer.authorize(request);
+    }
+
+    /**
+     * Determines if the given Resource is considered public for the action being performed.
+     *
+     * @param resource a Resource being authorized
+     * @param action the action being performed
+     * @return true if the resource is public for the given action, false otherwise
+     */
+    private boolean isPublicAccessAllowed(final Resource resource, final RequestAction action) {
+        if (resource == null || action == null) {
+            return false;
+        }
+
+        final String resourceIdentifier = resource.getIdentifier();
+        if (resourceIdentifier == null || !resourceIdentifier.startsWith(ResourceType.Bucket.getValue() + "/")) {
+            return false;
+        }
+
+        final int lastSlashIndex = resourceIdentifier.lastIndexOf("/");
+        if (lastSlashIndex < 0 || lastSlashIndex >= resourceIdentifier.length() - 1) {
+            return false;
+        }
+
+        final String bucketId = resourceIdentifier.substring(lastSlashIndex + 1);
+        try {
+            final Bucket bucket = registryService.getBucket(bucketId);
+            return bucket.isAllowPublicRead() && action == RequestAction.READ;
+        } catch (ResourceNotFoundException rnfe) {
+            // if not found then we can't determine public access, so return false to delegate to regular authorizer
+            LOGGER.debug("Cannot determine public access, bucket not found with id [{}]", new Object[]{bucketId});
+            return false;
+        } catch (Exception e) {
+            LOGGER.error("Error checking public access to bucket with id [{}]", new Object[]{bucketId}, e);
+            return false;
+        }
+    }
+
+    /**
+     * Creates a NiFiUser for the given proxy identity.
+     *
+     * This is only intended to be used for authorizing the given proxy identity against the /proxy resource, so we
+     * don't need to populate the rest of the info on this user.
+     *
+     * @param proxyIdentity the proxy identity
+     * @return the NiFiUser
+     */
+    private NiFiUser createProxyNiFiUser(final String proxyIdentity) {
+        return new StandardNiFiUser.Builder().identity(proxyIdentity).build();
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+        wrappedAuthorizer.preDestruction();
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkManagedAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkManagedAuthorizer.java
new file mode 100644
index 0000000..478482e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkManagedAuthorizer.java
@@ -0,0 +1,54 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+import org.apache.nifi.registry.service.RegistryService;
+
+/**
+ * Similar to FrameworkAuthorizer, but specifically for wrapping a ManagedAuthorizer.
+ */
+public class FrameworkManagedAuthorizer extends FrameworkAuthorizer implements ManagedAuthorizer {
+
+    private final ManagedAuthorizer wrappedManagedAuthorizer;
+
+    public FrameworkManagedAuthorizer(final ManagedAuthorizer wrappedManagedAuthorizer, final RegistryService registryService) {
+        super(wrappedManagedAuthorizer, registryService);
+        this.wrappedManagedAuthorizer = wrappedManagedAuthorizer;
+    }
+
+    @Override
+    public String getFingerprint() throws AuthorizationAccessException {
+        return wrappedManagedAuthorizer.getFingerprint();
+    }
+
+    @Override
+    public void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException {
+        wrappedManagedAuthorizer.inheritFingerprint(fingerprint);
+    }
+
+    @Override
+    public void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+        wrappedManagedAuthorizer.checkInheritability(proposedFingerprint);
+    }
+
+    @Override
+    public AccessPolicyProvider getAccessPolicyProvider() {
+        return wrappedManagedAuthorizer.getAccessPolicyProvider();
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java
index d08467e..04cb469 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java
@@ -16,18 +16,20 @@
  */
 package org.apache.nifi.registry.security.authorization.resource;
 
-import org.apache.nifi.registry.security.authorization.AuthorizationResult.Result;
-import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
-import org.apache.nifi.registry.security.authorization.user.NiFiUser;
 import org.apache.nifi.registry.security.authorization.AuthorizationAuditor;
 import org.apache.nifi.registry.security.authorization.AuthorizationRequest;
 import org.apache.nifi.registry.security.authorization.AuthorizationResult;
+import org.apache.nifi.registry.security.authorization.AuthorizationResult.Result;
 import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.security.authorization.Resource;
 import org.apache.nifi.registry.security.authorization.UserContextKeys;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 public interface Authorizable {
@@ -93,6 +95,10 @@
             userContext = null;
         }
 
+        // Note: We don't include the proxy identities here since this is not a direct attempt to access the resource and
+        // we just want to determine if the end user is authorized. The proxy identities will be authorized when calling
+        // Authorizable.authorize() during a direct access attempt for a resource.
+
         final Resource resource = getResource();
         final Resource requestedResource = getRequestedResource();
         final AuthorizationRequest request = new AuthorizationRequest.Builder()
@@ -205,10 +211,18 @@
             userContext = null;
         }
 
+        final List<String> proxyChain = new ArrayList<>();
+        NiFiUser proxyUser = user.getChain();
+        while (proxyUser  != null) {
+            proxyChain.add(proxyUser.getIdentity());
+            proxyUser = proxyUser.getChain();
+        }
+
         final Resource resource = getResource();
         final Resource requestedResource = getRequestedResource();
         final AuthorizationRequest request = new AuthorizationRequest.Builder()
                 .identity(user.getIdentity())
+                .proxyIdentities(proxyChain)
                 .groups(user.getGroups())
                 .anonymous(user.isAnonymous())
                 .accessAttempt(true)
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 26057ae..8df44e8 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
@@ -160,6 +160,10 @@
             bucket.setAllowBundleRedeploy(false);
         }
 
+        if (bucket.isAllowPublicRead() == null) {
+            bucket.setAllowPublicRead(false);
+        }
+
         validate(bucket, "Cannot create Bucket");
 
         writeLock.lock();
@@ -282,6 +286,10 @@
                 existingBucketById.setAllowExtensionBundleRedeploy(bucket.isAllowBundleRedeploy());
             }
 
+            if (bucket.isAllowPublicRead() != null) {
+                existingBucketById.setAllowPublicRead(bucket.isAllowPublicRead());
+            }
+
             // perform the actual update
             final BucketEntity updatedBucket = metadataService.updateBucket(existingBucketById);
             return BucketMappings.map(updatedBucket);
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/BucketMappings.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/BucketMappings.java
index 22020a6..542cc1b 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/BucketMappings.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/BucketMappings.java
@@ -33,6 +33,7 @@
         bucketEntity.setDescription(bucket.getDescription());
         bucketEntity.setCreated(new Date(bucket.getCreatedTimestamp()));
         bucketEntity.setAllowExtensionBundleRedeploy(bucket.isAllowBundleRedeploy());
+        bucketEntity.setAllowPublicRead(bucket.isAllowPublicRead());
         return bucketEntity;
     }
 
@@ -43,6 +44,7 @@
         bucket.setDescription(bucketEntity.getDescription());
         bucket.setCreatedTimestamp(bucketEntity.getCreated().getTime());
         bucket.setAllowBundleRedeploy(bucketEntity.isAllowExtensionBundleRedeploy());
+        bucket.setAllowPublicRead(bucketEntity.isAllowPublicRead());
         return bucket;
     }
 
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V5__AddBucketPublicFlags.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V5__AddBucketPublicFlags.sql
new file mode 100644
index 0000000..ef7478b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V5__AddBucketPublicFlags.sql
@@ -0,0 +1,16 @@
+-- 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.
+
+ALTER TABLE BUCKET ADD ALLOW_PUBLIC_READ INT NOT NULL DEFAULT (0);
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V5__AddBucketPublicFlags.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V5__AddBucketPublicFlags.sql
new file mode 100644
index 0000000..9ba1b80
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V5__AddBucketPublicFlags.sql
@@ -0,0 +1,16 @@
+-- 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.
+
+ALTER TABLE BUCKET ADD ALLOW_PUBLIC_READ INT NOT NULL DEFAULT 0;
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
index 932d483..34dfe6b 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
+++ b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
@@ -18,15 +18,15 @@
 
 import org.apache.nifi.registry.extension.ExtensionManager
 import org.apache.nifi.registry.properties.NiFiRegistryProperties
-import org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider
-import org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider
 import org.apache.nifi.registry.security.authorization.resource.ResourceFactory
+import org.apache.nifi.registry.service.RegistryService
 import spock.lang.Specification
 
 class AuthorizerFactorySpec extends Specification {
 
     def mockProperties = Mock(NiFiRegistryProperties)
     def mockExtensionManager = Mock(ExtensionManager)
+    def mockRegistryService = Mock(RegistryService)
 
     AuthorizerFactory authorizerFactory
 
@@ -35,7 +35,7 @@
         mockExtensionManager.getExtensionClassLoader(_) >> this.getClass().getClassLoader()
         mockProperties.getPropertyKeys() >> new HashSet<String>() // Called by IdentityMappingUtil.getIdentityMappings()
 
-        authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null)
+        authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null, mockRegistryService)
     }
 
     // runs after every feature method
@@ -88,7 +88,7 @@
 
         when: "a bad configuration is provided and getAuthorizer() is called"
         setMockPropsAuthorizersConfig(authorizersConfigFile, selectedAuthorizer)
-        authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null)
+        authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null, mockRegistryService)
         authorizerFactory.getAuthorizer()
 
         then: "expect an exception"
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
index 4646c2f..f47e7ce 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
+++ b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
@@ -20,13 +20,8 @@
 import org.apache.nifi.registry.authorization.User
 import org.apache.nifi.registry.authorization.UserGroup
 import org.apache.nifi.registry.bucket.Bucket
+import org.apache.nifi.registry.security.authorization.*
 import org.apache.nifi.registry.security.authorization.AccessPolicy as AuthAccessPolicy
-import org.apache.nifi.registry.security.authorization.AuthorizableLookup
-import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider
-import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider
-import org.apache.nifi.registry.security.authorization.Group
-import org.apache.nifi.registry.security.authorization.RequestAction
-import org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer
 import org.apache.nifi.registry.security.authorization.User as AuthUser
 import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException
 import org.apache.nifi.registry.security.authorization.resource.Authorizable
@@ -44,8 +39,9 @@
 
     def setup() {
         accessPolicyProvider.getUserGroupProvider() >> userGroupProvider
-        def authorizer = new StandardManagedAuthorizer(accessPolicyProvider, userGroupProvider)
-        authorizationService = new AuthorizationService(authorizableLookup, authorizer, registryService)
+        def standardAuthorizer = new StandardManagedAuthorizer(accessPolicyProvider, userGroupProvider)
+        def frameworkAuthorizer = new FrameworkManagedAuthorizer(standardAuthorizer, registryService)
+        authorizationService = new AuthorizationService(authorizableLookup, frameworkAuthorizer, registryService)
     }
 
     // ----- User tests -------------------------------------------------------
@@ -566,21 +562,31 @@
                 "b1": [
                         "name": "Bucket #1",
                         "description": "An initial bucket for testing",
-                        "createdTimestamp": 1
+                        "createdTimestamp": 1,
+                        "allowPublicRead" : false
                 ],
                 "b2": [
                         "name": "Bucket #2",
                         "description": "A second bucket for testing",
-                        "createdTimestamp": 2
+                        "createdTimestamp": 2,
+                        "allowPublicRead" : true
                 ],
+                "b3": [
+                        "name": "Bucket #3",
+                        "description": "A third bucket for testing",
+                        "createdTimestamp": 3,
+                        "allowPublicRead" : false
+                ]
         ]
         def mapBucket = {
             String id -> new Bucket([
                     identifier: id,
                     name: buckets[id]["name"] as String,
-                    description: buckets[id]["description"] as String]) }
+                    description: buckets[id]["description"] as String,
+                    allowPublicRead: buckets[id]["allowPublicRead"]
+            ]) }
 
-        registryService.getBuckets() >> {[ mapBucket("b1"), mapBucket("b2") ]}
+        registryService.getBuckets() >> {[ mapBucket("b1"), mapBucket("b2"), mapBucket("b3") ]}
 
         def authorized = Mock(Authorizable)
         authorized.authorize(_, _, _) >> { return }
@@ -590,7 +596,8 @@
         authorizableLookup.getAuthorizableByResource("/actuator")   >> denied
         authorizableLookup.getAuthorizableByResource("/buckets")    >> authorized
         authorizableLookup.getAuthorizableByResource("/buckets/b1") >> authorized
-        authorizableLookup.getAuthorizableByResource("/buckets/b2") >> denied
+        authorizableLookup.getAuthorizableByResource("/buckets/b2") >> authorized
+        authorizableLookup.getAuthorizableByResource("/buckets/b3") >> denied
         authorizableLookup.getAuthorizableByResource("/policies")   >> authorized
         authorizableLookup.getAuthorizableByResource("/proxy")      >> denied
         authorizableLookup.getAuthorizableByResource("/swagger")    >> denied
@@ -602,12 +609,13 @@
 
         then:
         resources != null
-        resources.size() == 4
+        resources.size() == 5
         def sortedResources = resources.sort{it.identifier}
         sortedResources[0].identifier == "/buckets"
         sortedResources[1].identifier == "/buckets/b1"
-        sortedResources[2].identifier == "/policies"
-        sortedResources[3].identifier == "/tenants"
+        sortedResources[2].identifier == "/buckets/b2"
+        sortedResources[3].identifier == "/policies"
+        sortedResources[4].identifier == "/tenants"
 
 
         when:
@@ -615,11 +623,11 @@
 
         then:
         filteredResources != null
-        filteredResources.size() == 2
+        filteredResources.size() == 3
         def sortedFilteredResources = filteredResources.sort{it.identifier}
         sortedFilteredResources[0].identifier == "/buckets"
         sortedFilteredResources[1].identifier == "/buckets/b1"
-
+        sortedFilteredResources[2].identifier == "/buckets/b2"
     }
 
 }
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 027bd3a..8c54152 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
@@ -105,6 +105,7 @@
         final BucketEntity bucket = metadataService.getBucketById("1");
         assertNotNull(bucket);
         assertFalse(bucket.isAllowExtensionBundleRedeploy());
+        assertFalse(bucket.isAllowPublicRead());
 
         final String updatedName = bucket.getName() + " UPDATED";
         final String updatedDesc = bucket.getDescription() + "DESC";
@@ -112,6 +113,7 @@
         bucket.setName(updatedName);
         bucket.setDescription(updatedDesc);
         bucket.setAllowExtensionBundleRedeploy(true);
+        bucket.setAllowPublicRead(true);
 
         metadataService.updateBucket(bucket);
 
@@ -120,6 +122,7 @@
         assertEquals(updatedName, updatedBucket.getName());
         assertEquals(updatedDesc, updatedBucket.getDescription());
         assertTrue(updatedBucket.isAllowExtensionBundleRedeploy());
+        assertTrue(updatedBucket.isAllowPublicRead());
     }
 
     @Test
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestFrameworkAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestFrameworkAuthorizer.java
new file mode 100644
index 0000000..2cc03f8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestFrameworkAuthorizer.java
@@ -0,0 +1,278 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
+import org.apache.nifi.registry.service.RegistryService;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+
+import java.util.Arrays;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TestFrameworkAuthorizer {
+
+    private Authorizer frameworkAuthorizer;
+    private Authorizer wrappedAuthorizer;
+    private RegistryService registryService;
+
+    private Bucket bucketPublic;
+    private Bucket bucketNotPublic;
+
+    @Before
+    public void setup() {
+        wrappedAuthorizer = mock(Authorizer.class);
+        registryService = mock(RegistryService.class);
+        frameworkAuthorizer = new FrameworkAuthorizer(wrappedAuthorizer, registryService);
+
+        bucketPublic = new Bucket();
+        bucketPublic.setIdentifier(UUID.randomUUID().toString());
+        bucketPublic.setName("Public Bucket");
+        bucketPublic.setAllowPublicRead(true);
+
+        bucketNotPublic = new Bucket();
+        bucketNotPublic.setIdentifier(UUID.randomUUID().toString());
+        bucketNotPublic.setName("Non Public Bucket");
+        bucketNotPublic.setAllowPublicRead(false);
+
+        when(registryService.getBucket(bucketPublic.getIdentifier())).thenReturn(bucketPublic);
+        when(registryService.getBucket(bucketNotPublic.getIdentifier())).thenReturn(bucketNotPublic);
+    }
+
+    @Test
+    public void testReadPublicBucketWhenAnonymous() {
+        final Resource resource = ResourceFactory.getBucketResource(bucketPublic.getIdentifier(), bucketPublic.getName());
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(resource)
+                .requestedResource(resource)
+                .action(RequestAction.READ)
+                .accessAttempt(true)
+                .identity("anonymous")
+                .anonymous(true)
+                .build();
+
+        final AuthorizationResult result = frameworkAuthorizer.authorize(request);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Approved, result.getResult());
+
+        // should never make it to wrapped authorizer
+        verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testReadNonPublicBucketWhenAnonymous() {
+        final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName());
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(resource)
+                .requestedResource(resource)
+                .action(RequestAction.READ)
+                .accessAttempt(true)
+                .identity("anonymous")
+                .anonymous(true)
+                .build();
+
+        final AuthorizationResult result = frameworkAuthorizer.authorize(request);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Denied, result.getResult());
+
+        // should be denied before making it to the wrapped authorizer since the user is anonymous
+        verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testWritePublicBucketWhenAnonymous() {
+        final Resource resource = ResourceFactory.getBucketResource(bucketPublic.getIdentifier(), bucketPublic.getName());
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(resource)
+                .requestedResource(resource)
+                .action(RequestAction.WRITE)
+                .accessAttempt(true)
+                .identity("anonymous")
+                .anonymous(true)
+                .build();
+
+        final AuthorizationResult result = frameworkAuthorizer.authorize(request);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Denied, result.getResult());
+
+        // should be denied before making it to wrapped authorizer since request is anonymous
+        verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testReadPublicBucketWhenNotAnonymous() {
+        final Resource resource = ResourceFactory.getBucketResource(bucketPublic.getIdentifier(), bucketPublic.getName());
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(resource)
+                .requestedResource(resource)
+                .action(RequestAction.READ)
+                .accessAttempt(true)
+                .identity("user1")
+                .anonymous(false)
+                .proxyIdentities(Arrays.asList("proxy1", "proxy2"))
+                .build();
+
+        final AuthorizationResult result = frameworkAuthorizer.authorize(request);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Approved, result.getResult());
+
+        // should never make it to wrapped authorizer
+        verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testReadNonPublicBucketWhenNotAnonymousAndAuthorizedProxies() {
+        final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName());
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(resource)
+                .requestedResource(resource)
+                .action(RequestAction.READ)
+                .accessAttempt(true)
+                .identity("user1")
+                .anonymous(false)
+                .proxyIdentities(Arrays.asList("proxy1", "proxy2"))
+                .build();
+
+        // since the bucket is not public it will fall through to the wrapped authorizer
+        when(wrappedAuthorizer.authorize(any(AuthorizationRequest.class)))
+                .thenReturn(AuthorizationResult.approved());
+
+        final AuthorizationResult result = frameworkAuthorizer.authorize(request);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Approved, result.getResult());
+
+        // should make 3 calls to the wrapped authorizer to authorize user1, proxy1, proxy2
+        verify(wrappedAuthorizer, times(3)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testReadNonPublicBucketWhenNotAnonymousAndUnauthorizedProxy() {
+        final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName());
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(resource)
+                .requestedResource(resource)
+                .action(RequestAction.READ)
+                .accessAttempt(true)
+                .identity("user1")
+                .anonymous(false)
+                .proxyIdentities(Arrays.asList("proxy1", "proxy2"))
+                .build();
+
+        // since the bucket is not public and the user is not anonymous, it will continue to proxy authorization
+
+        // simulate the first proxy being authorized for READ actions
+        final AuthorizationRequestMatcher proxy1Matcher = new AuthorizationRequestMatcher(
+                "proxy1", ResourceFactory.getProxyResource(), request.getAction());
+        when(wrappedAuthorizer.authorize(argThat(proxy1Matcher))).thenReturn(AuthorizationResult.approved());
+
+        // simulate the second proxy being unauthorized for READ actions
+        final AuthorizationRequestMatcher proxy2Matcher = new AuthorizationRequestMatcher(
+                "proxy2", ResourceFactory.getProxyResource(), request.getAction());
+        when(wrappedAuthorizer.authorize(argThat(proxy2Matcher))).thenReturn(AuthorizationResult.denied("denied"));
+
+        final AuthorizationResult result = frameworkAuthorizer.authorize(request);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Denied, result.getResult());
+
+        // should make 2 calls to the wrapped authorizer for the two proxies
+        verify(wrappedAuthorizer, times(2)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testReadNonPublicBucketWhenNotAnonymousAndUnauthorizedEndUser() {
+        final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName());
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(resource)
+                .requestedResource(resource)
+                .action(RequestAction.READ)
+                .accessAttempt(true)
+                .identity("user1")
+                .anonymous(false)
+                .proxyIdentities(Arrays.asList("proxy1", "proxy2"))
+                .build();
+
+        // since the bucket is not public and the user is not anonymous, it will continue to proxy authorization
+
+        // simulate the first proxy being authorized for READ actions
+        final AuthorizationRequestMatcher proxy1Matcher = new AuthorizationRequestMatcher(
+                "proxy1", ResourceFactory.getProxyResource(), request.getAction());
+        when(wrappedAuthorizer.authorize(argThat(proxy1Matcher))).thenReturn(AuthorizationResult.approved());
+
+        // simulate the second proxy being authorized for READ actions
+        final AuthorizationRequestMatcher proxy2Matcher = new AuthorizationRequestMatcher(
+                "proxy2", ResourceFactory.getProxyResource(), request.getAction());
+        when(wrappedAuthorizer.authorize(argThat(proxy2Matcher))).thenReturn(AuthorizationResult.approved());
+
+        // simulate the end user being unauthorized for READ actions
+        final AuthorizationRequestMatcher user1Matcher = new AuthorizationRequestMatcher(
+                "user1", resource, request.getAction());
+        when(wrappedAuthorizer.authorize(argThat(user1Matcher))).thenReturn(AuthorizationResult.denied("denied"));
+
+        final AuthorizationResult result = frameworkAuthorizer.authorize(request);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Denied, result.getResult());
+
+        // should make 3 calls to the wrapped authorizer for the two proxies and end user
+        verify(wrappedAuthorizer, times(3)).authorize(any(AuthorizationRequest.class));
+    }
+
+
+    /**
+     * Matcher for matching Authorization requests.
+     */
+    private static class AuthorizationRequestMatcher implements ArgumentMatcher<AuthorizationRequest> {
+
+        private final String identity;
+        private final Resource resource;
+        private final RequestAction action;
+
+        public AuthorizationRequestMatcher(final String identity, final Resource resource, final RequestAction action) {
+            this.identity = identity;
+            this.resource = resource;
+            this.action = action;
+        }
+
+        @Override
+        public boolean matches(final AuthorizationRequest request) {
+            if (request == null) {
+                return false;
+            }
+
+            return identity.equals(request.getIdentity())
+                    && resource.getIdentifier().equals(request.getResource().getIdentifier())
+                    && action == request.getAction();
+        }
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java
index 1eb99f9..56b7b45 100644
--- a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java
@@ -18,6 +18,7 @@
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -33,6 +34,7 @@
     private final Resource resource;
     private final Resource requestedResource;
     private final String identity;
+    private final List<String> proxyIdentities;
     private final Set<String> groups;
     private final RequestAction action;
     private final boolean isAccessAttempt;
@@ -49,6 +51,7 @@
 
         this.resource = builder.resource;
         this.identity = builder.identity;
+        this.proxyIdentities = builder.proxyIdentities == null ? Collections.emptyList() : Collections.unmodifiableList(builder.proxyIdentities);
         this.groups = builder.groups == null ? null : Collections.unmodifiableSet(builder.groups);
         this.action = builder.action;
         this.isAccessAttempt = builder.isAccessAttempt;
@@ -103,6 +106,15 @@
     }
 
     /**
+     * The identities in the proxy chain for the request. Will be empty if the request was not proxied.
+     *
+     * @return The identities in the proxy chain
+     */
+    public List<String> getProxyIdentities() {
+        return proxyIdentities;
+    }
+
+    /**
      * The groups the user making this request belongs to. May be null if this NiFi is not configured to load user
      * groups or empty if the user has no groups
      *
@@ -174,6 +186,7 @@
         private Resource resource;
         private Resource requestedResource;
         private String identity;
+        private List<String> proxyIdentities;
         private Set<String> groups;
         private Boolean isAnonymous;
         private Boolean isAccessAttempt;
@@ -197,6 +210,11 @@
             return this;
         }
 
+        public Builder proxyIdentities(final List<String> proxyIdentities) {
+            this.proxyIdentities = proxyIdentities;
+            return this;
+        }
+
         public Builder groups(final Set<String> groups) {
             this.groups = groups;
             return this;
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
index 2cf3116..30374f0 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
@@ -124,6 +124,7 @@
         }
 
         final CurrentUser currentUser = authorizationService.getCurrentUser();
+        currentUser.setLoginSupported(httpServletRequest.isSecure() && identityProvider != null);
 
         return generateOkResponse(currentUser).build();
     }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
index a2a6ea9..20a6e0d 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
@@ -22,7 +22,6 @@
 import org.apache.nifi.registry.web.security.authentication.AnonymousIdentityFilter;
 import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider;
 import org.apache.nifi.registry.web.security.authentication.IdentityFilter;
-import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException;
 import org.apache.nifi.registry.web.security.authentication.jwt.JwtIdentityProvider;
 import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityAuthenticationProvider;
 import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider;
@@ -110,13 +109,9 @@
         // otp
         // todo, if needed one-time password auth filter goes here
 
-        if (properties.getSslPort() == null) {
-            // If we are running an unsecured NiFi Registry server, add an
-            // anonymous authentication filter that will populate the
-            // authenticated, anonymous user if no other user identity
-            // is detected earlier in the Spring filter chain.
-            http.anonymous().authenticationFilter(anonymousAuthenticationFilter);
-        }
+        // add an anonymous authentication filter that will populate the authenticated,
+        // anonymous user if no other user identity is detected earlier in the Spring filter chain
+        http.anonymous().authenticationFilter(anonymousAuthenticationFilter);
 
         // After Spring Security filter chain is complete (so authentication is done),
         // but before the Jersey application endpoints get the request,
@@ -182,20 +177,9 @@
                                  AuthenticationException authenticationException)
                     throws IOException, ServletException {
 
-                final int status;
-
-                // See X509IdentityAuthenticationProvider.buildAuthenticatedToken(...)
-                if (authenticationException instanceof UntrustedProxyException) {
-                    // return a 403 response
-                    status = HttpServletResponse.SC_FORBIDDEN;
-                    logger.info("Identity in proxy chain not trusted to act as a proxy: {} Returning 403 response.", authenticationException.toString());
-
-                } else {
-                    // return a 401 response
-                    status = HttpServletResponse.SC_UNAUTHORIZED;
-                    logger.info("Client could not be authenticated due to: {} Returning 401 response.", authenticationException.toString());
-                }
-
+                // return a 401 response
+                final int status = HttpServletResponse.SC_UNAUTHORIZED;
+                logger.info("Client could not be authenticated due to: {} Returning 401 response.", authenticationException.toString());
                 logger.debug("", authenticationException);
 
                 if (!response.isCommitted()) {
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java
deleted file mode 100644
index 82570a3..0000000
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.registry.web.security.authentication.exception;
-
-import org.springframework.security.core.AuthenticationException;
-
-public class UntrustedProxyException extends AuthenticationException {
-
-    public UntrustedProxyException(String msg) {
-        super(msg);
-    }
-
-    public UntrustedProxyException(String msg, Throwable t) {
-        super(msg, t);
-    }
-
-}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java
index 9d724ac..6571177 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java
@@ -19,25 +19,16 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
 import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
-import org.apache.nifi.registry.web.security.authentication.AuthenticationRequestToken;
 import org.apache.nifi.registry.security.authentication.AuthenticationResponse;
 import org.apache.nifi.registry.security.authentication.IdentityProvider;
-import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider;
-import org.apache.nifi.registry.web.security.authentication.AuthenticationSuccessToken;
 import org.apache.nifi.registry.security.authorization.Authorizer;
-import org.apache.nifi.registry.security.authorization.RequestAction;
-import org.apache.nifi.registry.security.authorization.Resource;
-import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
-import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
 import org.apache.nifi.registry.security.authorization.user.NiFiUser;
 import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails;
 import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser;
 import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
-import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpMethod;
+import org.apache.nifi.registry.web.security.authentication.AuthenticationRequestToken;
+import org.apache.nifi.registry.web.security.authentication.AuthenticationSuccessToken;
+import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider;
 
 import java.util.List;
 import java.util.ListIterator;
@@ -45,20 +36,6 @@
 
 public class X509IdentityAuthenticationProvider extends IdentityAuthenticationProvider {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(X509IdentityAuthenticationProvider.class);
-
-    private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() {
-        @Override
-        public Authorizable getParentAuthorizable() {
-            return null;
-        }
-
-        @Override
-        public Resource getResource() {
-            return ResourceFactory.getProxyResource();
-        }
-    };
-
     public X509IdentityAuthenticationProvider(NiFiRegistryProperties properties, Authorizer authorizer, IdentityProvider identityProvider) {
         super(properties, authorizer, identityProvider);
     }
@@ -86,10 +63,6 @@
         final List<String> proxyChain = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(proxiedEntitiesChain);
         proxyChain.add(response.getIdentity());
 
-        final String httpMethodStr = x509RequestDetails.getHttpMethod().toUpperCase();
-        final HttpMethod httpMethod = HttpMethod.resolve(httpMethodStr);
-        LOGGER.debug("HTTP method is {}", new Object[]{httpMethod});
-
         // add the chain as appropriate to each proxy
         NiFiUser proxy = null;
         for (final ListIterator<String> chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) {
@@ -108,51 +81,13 @@
             // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities
             String clientAddress = (proxy == null) ? requestToken.getClientAddress() : null;
             proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous);
-
-            if (chainIter.hasPrevious()) {
-                switch (httpMethod) {
-                    case POST:
-                    case PUT:
-                    case PATCH:
-                        authorizeWrite(proxy);
-                        break;
-                    case DELETE:
-                        authorizeDelete(proxy);
-                        break;
-                    default:
-                        authorizeRead(proxy);
-                        break;
-                }
-            }
         }
 
+        // Defer authorization of proxy until later in FrameworkAuthorizer
+
         return new AuthenticationSuccessToken(new NiFiUserDetails(proxy));
     }
 
-    private void authorizeRead(final NiFiUser proxy) {
-        try {
-            PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.READ, proxy);
-        } catch (final AccessDeniedException e) {
-            throw new UntrustedProxyException(String.format("Untrusted proxy for read operation [%s].", proxy.getIdentity()));
-        }
-    }
-
-    private void authorizeWrite(final NiFiUser proxy) {
-        try {
-            PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy);
-        } catch (final AccessDeniedException e) {
-            throw new UntrustedProxyException(String.format("Untrusted proxy for write operation [%s].", proxy.getIdentity()));
-        }
-    }
-
-    private void authorizeDelete(final NiFiUser proxy) {
-        try {
-            PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.DELETE, proxy);
-        } catch (final AccessDeniedException e) {
-            throw new UntrustedProxyException(String.format("Untrusted proxy for delete operation [%s].", proxy.getIdentity()));
-        }
-    }
-
     /**
      * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values.
      *
@@ -170,6 +105,4 @@
         }
     }
 
-
-
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
index 11d7b33..c6172a1 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -32,6 +32,7 @@
 import org.apache.nifi.registry.security.authorization.AuthorizerFactory;
 import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider;
 import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
+import org.apache.nifi.registry.service.RegistryService;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -94,9 +95,9 @@
         @Primary
         @Bean
         @DependsOn({"directoryServer"}) // Can't load LdapUserGroupProvider until the embedded LDAP server, which creates the "directoryServer" bean, is running
-        public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties properties, ExtensionManager extensionManager) throws Exception {
+        public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties properties, ExtensionManager extensionManager, RegistryService registryService) throws Exception {
             if (authorizerFactory == null) {
-                authorizerFactory = new AuthorizerFactory(properties, extensionManager, sensitivePropertyProvider());
+                authorizerFactory = new AuthorizerFactory(properties, extensionManager, sensitivePropertyProvider(), registryService);
             }
             return authorizerFactory.getAuthorizer();
         }
@@ -231,7 +232,18 @@
     @Test
     public void testGetCurrentUserFailsForAnonymous() throws Exception {
 
-        // Given: the client is connected to an unsecured NiFi Registry
+        // Given: the client is connected to an secured NiFi Registry
+        final String expectedJson = "{" +
+                "\"anonymous\":true," +
+                "\"identity\":\"anonymous\"," +
+                "\"loginSupported\":true," +
+                "\"resourcePermissions\":{" +
+                "\"anyTopLevelResource\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," +
+                "\"buckets\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," +
+                "\"policies\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," +
+                "\"proxy\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," +
+                "\"tenants\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}}" +
+                "}";
 
         // When: the /access endpoint is queried with no credentials
         final Response response = client
@@ -240,8 +252,10 @@
                 .get(Response.class);
 
         // Then: the server returns a 200 OK with the expected current user
-        assertEquals(401, response.getStatus());
+        assertEquals(200, response.getStatus());
 
+        final String actualJson = response.readEntity(String.class);
+        JSONAssert.assertEquals(expectedJson, actualJson, false);
     }
 
     @Test
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.html
index 32ee01a..19f7663 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.html
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.html
@@ -28,6 +28,10 @@
                 <input #newBucketInput matInput floatPlaceholder="always" placeholder="Bucket Name">
             </mat-input-container>
         </div>
+        <mat-checkbox #newBucketPublicReadCheckbox [disabled]="protocol === 'http:'" >
+            Make publicly visible<i matTooltip="Allows read access to items in this bucket by unauthenticated users. Overrides any specific policies granting read access."
+                                    class="pad-left-sm fa fa-question-circle-o help-icon"></i>
+        </mat-checkbox>
         <mat-checkbox [(ngModel)]="keepDialogOpen">
             Keep this dialog open after creating bucket
         </mat-checkbox>
@@ -38,8 +42,8 @@
                 i18n="Cancel creation of new bucket|A button for cancelling the creation of a new bucket in the registry.@@nf-admin-workflow-cancel-create-bucket-button">
             Cancel
         </button>
-        <button [disabled]="newBucketInput.value.length === 0" class="push-left-sm" data-automation-id="create-new-bucket-button" (click)="createBucket(newBucketInput)" color="fds-primary" mat-raised-button
-                i18n="Create new bucket button|A button for creating a new bucket in the registry.@@nf-admin-workflow-create-bucket-button">
+        <button [disabled]="newBucketInput.value.length === 0" class="push-left-sm" data-automation-id="create-new-bucket-button" (click)="createBucket(newBucketInput,newBucketPublicReadCheckbox)"
+                color="fds-primary" mat-raised-button i18n="Create new bucket button|A button for creating a new bucket in the registry.@@nf-admin-workflow-create-bucket-button">
             Create
         </button>
     </div>
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.js
index abaaa59..f529285 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.js
@@ -38,6 +38,7 @@
     this.dialogRef = matDialogRef;
     // local state
     this.keepDialogOpen = false;
+    this.protocol = location.protocol;
 }
 
 NfRegistryCreateBucket.prototype = {
@@ -46,11 +47,12 @@
     /**
      * Create a new bucket.
      *
-     * @param newBucketInput     The newBucketInput element.
+     * @param newBucketInput                The newBucketInput element.
+     * @param newBucketPublicReadCheckbox   The newBucketPublicReadCheckbox element.
      */
-    createBucket: function (newBucketInput) {
+    createBucket: function (newBucketInput, newBucketPublicReadCheckbox) {
         var self = this;
-        this.nfRegistryApi.createBucket(newBucketInput.value).subscribe(function (bucket) {
+        this.nfRegistryApi.createBucket(newBucketInput.value, newBucketPublicReadCheckbox.checked).subscribe(function (bucket) {
             if (!bucket.error) {
                 self.nfRegistryService.buckets.push(bucket);
                 self.nfRegistryService.filterBuckets();
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.spec.js
index 3cb59d7..5b14455 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.spec.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.spec.js
@@ -46,7 +46,7 @@
 
     it('should create a new bucket and close the dialog', function () {
         // The function to test
-        comp.createBucket({value: 'NewBucket'});
+        comp.createBucket({value: 'NewBucket'}, {checked: false});
 
         //assertions
         expect(comp).toBeDefined();
@@ -61,7 +61,7 @@
         comp.keepDialogOpen = true;
 
         // The function to test
-        comp.createBucket({value: 'NewBucket'});
+        comp.createBucket({value: 'NewBucket'}, {checked: false});
 
         //assertions
         expect(comp).toBeDefined();
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.html
index 9d3cdbe..126aaa7 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.html
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.html
@@ -42,6 +42,15 @@
                 Save
             </button>
         </div>
+        <div class="pad-left-md pad-right-md pad-bottom-sm">
+            <mat-checkbox #bundlePublicReadCheckbox
+                          [disabled]="!nfRegistryService.currentUser.resourcePermissions.buckets.canWrite || protocol === 'http:'"
+                          [(ngModel)]="allowPublicRead"
+                          (change)="toggleBucketPublicRead($event)">
+                Make publicly visible<i matTooltip="Allows read access to items in this bucket by unauthenticated users. Overrides any specific policies granting read access."
+                                    class="pad-left-sm fa fa-question-circle-o help-icon"></i>
+            </mat-checkbox>
+        </div>
         <div class="pad-top-md pad-left-md pad-right-md">
             <div class="pad-bottom-sm">
                 <span class="md-card-title">Bundle Settings</span>
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js
index 1d6a71b..367129c 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js
@@ -63,6 +63,7 @@
     this.userPermsSearchTerms = [];
     this.bucketname = '';
     this.allowBundleRedeploy = false;
+    this.allowPublicRead = false;
     this.bucketPolicies = [];
     this.userPerms = {};
     this.groupPerms = {};
@@ -80,6 +81,7 @@
     this.dialogService = fdsDialogService;
     this.snackBarService = fdsSnackBarService;
     this.dataTableService = tdDataTableService;
+    this.protocol = location.protocol;
 }
 
 NfRegistryManageBucket.prototype = {
@@ -104,6 +106,7 @@
                     self.nfRegistryService.bucket = bucket;
                     self.bucketname = bucket.name;
                     self.allowBundleRedeploy = bucket.allowBundleRedeploy;
+                    self.allowPublicRead = bucket.allowPublicRead;
                     if (!self.nfRegistryService.currentUser.anonymous) {
                         if (!response[1].status || response[1].status === 200) {
                             var policies = response[1];
@@ -163,6 +166,7 @@
                     self.nfRegistryService.bucket = response;
                     self.bucketname = response.name;
                     self.allowBundleRedeploy = response.allowBundleRedeploy;
+                    self.allowPublicRead = response.allowPublicRead;
 
                     if (dialogResult) {
                         if (dialogResult.userOrGroup.type === 'user') {
@@ -201,6 +205,7 @@
                     self.nfRegistryService.bucket = response;
                     self.bucketname = response.name;
                     self.allowBundleRedeploy = response.allowBundleRedeploy;
+                    self.allowPublicRead = response.allowPublicRead;
 
                     if (dialogResult) {
                         if (dialogResult.userOrGroup.type === 'user') {
@@ -424,6 +429,8 @@
             } else if (response.status === 409) {
                 self.bucketname = self.nfRegistryService.bucket.name;
                 self.allowBundleRedeploy = self.nfRegistryService.bucket.allowBundleRedeploy;
+                self.allowPublicRead = self.nfRegistryService.bucket.allowPublicRead;
+
                 self.dialogService.openConfirm({
                     title: 'Error',
                     message: 'This bucket already exists. Please enter a different identity/bucket name.',
@@ -433,6 +440,8 @@
             } else if (response.status === 400) {
                 self.bucketname = self.nfRegistryService.bucket.name;
                 self.allowBundleRedeploy = self.nfRegistryService.bucket.allowBundleRedeploy;
+                self.allowPublicRead = self.nfRegistryService.bucket.allowPublicRead;
+
                 self.dialogService.openConfirm({
                     title: 'Error',
                     message: response.error,
@@ -478,6 +487,40 @@
     },
 
     /**
+     * Update allowPublicRead flag.
+     *
+     * @param the checkbox change event
+     */
+    toggleBucketPublicRead: function (event) {
+        var self = this;
+        this.nfRegistryApi.updateBucket({
+            'identifier': this.nfRegistryService.bucket.identifier,
+            'allowPublicRead': event.checked,
+        }).subscribe(function (response) {
+            if (!response.status || response.status === 200) {
+                self.nfRegistryService.bucket = response;
+                self.snackBarService.openCoaster({
+                    title: 'Success',
+                    message: 'Bucket settings have been updated.',
+                    verticalPosition: 'bottom',
+                    horizontalPosition: 'right',
+                    icon: 'fa fa-check-circle-o',
+                    color: '#1EB475',
+                    duration: 3000
+                });
+            } else if (response.status === 400) {
+                self.allowPublicRead = !event.checked;
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: response.error,
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                });
+            }
+        });
+    },
+
+    /**
      * Determine if bucket policies can be edited.
      * @returns {boolean}
      */
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html
index 343b319..986e7b3 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html
@@ -18,6 +18,9 @@
 <div id="nifi-registry-user-login-dialog">
     <div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
         <span class="md-card-title">Log In</span>
+        <button mat-icon-button (click)="cancel()">
+            <mat-icon color="primary">close</mat-icon>
+        </button>
     </div>
     <div fxLayout="column" fxLayoutAlign="space-between start" class="pad-bottom-md">
         <div class="pad-bottom-md fill-available-width">
@@ -33,13 +36,13 @@
     </div>
     <div fxLayout="row">
         <span fxFlex></span>
-        <button (click)="usernameInput.value='';passwordInput.value=''" color="fds-regular" mat-raised-button
-                i18n="Clear log in form|A button for clearing the login form.@@nf-clear-user-login-button">
-            Clear
+        <button (click)="cancel()" color="fds-regular" mat-raised-button
+                i18n="Cancel log in|A button for cancelling the login form.@@nf-cancel-user-login-button">
+            Cancel
         </button>
         <button [disabled]="usernameInput.value.length === 0 || passwordInput.value.length === 0" class="push-left-sm" (click)="login(usernameInput, passwordInput)" color="fds-primary" mat-raised-button
                 i18n="Log in|A button for attempting to authenticate with the registry.@@nf-user-login-button">
             Log In
         </button>
     </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.js
index 6412e65..d4da84c 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.js
@@ -20,6 +20,7 @@
 import NfRegistryApi from 'services/nf-registry.api';
 import { MatDialogRef } from '@angular/material';
 import { NfRegistryLoginAuthGuard } from 'services/nf-registry.auth-guard.service';
+import { Router } from '@angular/router';
 
 /**
  * NfRegistryUserLogin constructor.
@@ -28,13 +29,15 @@
  * @param nfRegistryService                 The nf-registry.service module.
  * @param matDialogRef                      The angular material dialog ref.
  * @param nfRegistryLoginAuthGuard          The login auth guard.
+ * @param router                    The angular router module.
  * @constructor
  */
-function NfRegistryUserLogin(nfRegistryApi, nfRegistryService, matDialogRef, nfRegistryLoginAuthGuard) {
+function NfRegistryUserLogin(nfRegistryApi, nfRegistryService, matDialogRef, nfRegistryLoginAuthGuard, router) {
     this.nfRegistryService = nfRegistryService;
     this.nfRegistryApi = nfRegistryApi;
     this.dialogRef = matDialogRef;
     this.nfRegistryLoginAuthGuard = nfRegistryLoginAuthGuard;
+    this.router = router;
 }
 
 NfRegistryUserLogin.prototype = {
@@ -56,6 +59,12 @@
                 self.nfRegistryLoginAuthGuard.checkLogin(self.nfRegistryService.redirectUrl);
             }
         });
+    },
+
+    cancel: function () {
+        var self = this;
+        self.dialogRef.close();
+        self.router.navigateByUrl('/nifi-registry');
     }
 };
 
@@ -69,7 +78,8 @@
     NfRegistryApi,
     NfRegistryService,
     MatDialogRef,
-    NfRegistryLoginAuthGuard
+    NfRegistryLoginAuthGuard,
+    Router
 ];
 
 export default NfRegistryUserLogin;
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html
index ac4e569..9d1e849 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html
@@ -74,6 +74,7 @@
             <div *ngIf="nfRegistryService.currentUser.identity && nfRegistryService.perspective !== 'login' && nfRegistryService.perspective !== 'not-found'" fxLayout="column" fxLayoutAlign="space-around end" class="push-right-sm">
                 <div id="current-user" matTooltip="{{nfRegistryService.currentUser.identity}}">{{nfRegistryService.currentUser.identity}}</div>
                 <a id="logout-link-container" *ngIf="nfRegistryService.currentUser.canLogout" class="link" (click)="logout()">logout</a>
+                <a id="logout-link-container" *ngIf="!nfRegistryService.currentUser.canLogout && nfRegistryService.currentUser.anonymous && nfRegistryService.currentUser.loginSupported" class="link" (click)="login()">login</a>
             </div>
             <div id="nifi-registry-documentation" *ngIf="nfRegistryService.perspective !== 'login'" class="pad-right-sm">
                 <a matTooltip="Help" href="{{nfRegistryService.documentation.link}}" target="_blank"><i class="fa fa-question-circle help-icon" aria-hidden="true"></i></a>
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js
index bea77d0..24702cf 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js
@@ -75,6 +75,13 @@
         delete this.nfRegistryService.currentUser.anonymous;
         this.nfStorage.removeItem('jwt');
         this.router.navigateByUrl('/nifi-registry/login');
+    },
+
+    /**
+     * Navigate to login route.
+     */
+    login: function () {
+        this.router.navigateByUrl('/nifi-registry/login');
     }
 };
 
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
index 28c79c1..2d2da8e 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
@@ -155,9 +155,9 @@
      * @param {string} name  The name of the bucket.
      * @returns {*}
      */
-    createBucket: function (name) {
+    createBucket: function (name, allowPublicRead) {
         var self = this;
-        return this.http.post('/nifi-registry-api/buckets', {'name': name}, headers)
+        return this.http.post('/nifi-registry-api/buckets', {'name': name, 'allowPublicRead': allowPublicRead}, headers)
             .map(function (response) {
                 return response;
             })
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js
index 2036eae..adf88ff 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js
@@ -578,10 +578,13 @@
         nfRegistryApi.updateBucket({
             'identifier': '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc',
             'name': 'Bucket #1',
-            'allowBundleRedeploy': false
+            'allowBundleRedeploy': true,
+            'allowPublicRead': true
         }).subscribe(function (response) {
             expect(response[0].identifier).toEqual('2f7f9e54-dc09-4ceb-aa58-9fe581319cdc');
             expect(response[0].name).toEqual('Bucket #1');
+            expect(response[0].allowBundleRedeploy).toEqual(true);
+            expect(response[0].allowPublicRead).toEqual(true);
         });
         // the request it made
         req = httpMock.expectOne('/nifi-registry-api/buckets/2f7f9e54-dc09-4ceb-aa58-9fe581319cdc');
@@ -590,7 +593,9 @@
         // Next, fulfill the request by transmitting a response.
         req.flush([{
             'identifier': '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc',
-            'name': 'Bucket #1'
+            'name': 'Bucket #1',
+            'allowBundleRedeploy': true,
+            'allowPublicRead': true
         }]);
 
         // Finally, assert that there are no outstanding requests.
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.auth-guard.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.auth-guard.service.js
index cc94c60..f003e80 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.auth-guard.service.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.auth-guard.service.js
@@ -96,8 +96,8 @@
                             });
                             self.router.navigateByUrl('/nifi-registry/explorer');
                         }
-                    } else {
-                        // registry security not configured, redirect to workflow perspective
+                    } else if (location.protocol === 'http:') {
+                        // user is anonymous and we are NOT secure, redirect to workflow perspective
                         self.dialogService.openConfirm({
                             title: 'Not Applicable',
                             message: 'User administration is not configured for this registry.',
@@ -105,6 +105,15 @@
                             acceptButtonColor: 'fds-warn'
                         });
                         self.router.navigateByUrl('/nifi-registry/administration/workflow');
+                    } else {
+                        // user is anonymous and we are secure so don't allow the url, navigate to the main page
+                        self.dialogService.openConfirm({
+                            title: 'Access denied',
+                            message: 'Please contact your system administrator.',
+                            acceptButton: 'Ok',
+                            acceptButtonColor: 'fds-warn'
+                        });
+                        self.router.navigateByUrl('/nifi-registry/explorer');
                     }
                 }
             });
@@ -155,7 +164,7 @@
 
     checkLogin: function (url) {
         var self = this;
-        if (this.nfRegistryService.currentUser.resourcePermissions.buckets.canRead || this.nfRegistryService.currentUser.anonymous) { return true; }
+        if (this.nfRegistryService.currentUser.resourcePermissions.buckets.canRead) { return true; }
 
         // Store the attempted URL for redirecting
         this.nfRegistryService.redirectUrl = url;
@@ -197,9 +206,18 @@
                             });
                             self.router.navigateByUrl('/nifi-registry/administration/users');
                         }
-                    } else {
-                        // registry security not configured, allow access to workflow perspective
+                    } else if (location.protocol === 'http:') {
+                        // user is anonymous and we are NOT secure so allow the url
                         self.router.navigateByUrl(url);
+                    } else {
+                        // user is anonymous and we are secure so don't allow the url, navigate to the main page
+                        self.dialogService.openConfirm({
+                            title: 'Access denied',
+                            message: 'Please contact your system administrator.',
+                            acceptButton: 'Ok',
+                            acceptButtonColor: 'fds-warn'
+                        });
+                        self.router.navigateByUrl('/nifi-registry/explorer');
                     }
                 }
             });
@@ -260,7 +278,7 @@
                     }
                     self.nfRegistryService.currentUser.canActivateResourcesAuthGuard = true;
                     self.router.navigateByUrl(self.nfRegistryService.redirectUrl);
-                } else if (self.nfRegistryService.currentUser.anonymous) {
+                } else if (self.nfRegistryService.currentUser.anonymous && !self.nfRegistryService.currentUser.loginSupported) {
                     self.router.navigateByUrl('/nifi-registry');
                 } else {
                     self.nfRegistryService.currentUser.anonymous = true;
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
index 060c8dd..33c41cc 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
@@ -206,6 +206,7 @@
     this.breadCrumbState = 'out';
     this.explorerViewType = '';
     this.currentUser = {
+        loginSupported: false,
         resourcePermissions: {
             anyTopLevelResource: {
                 canRead: false,