NIFIREG-358 Refactoring proxy authorization to be part of Authorizables

NIFIREG-358 Catching UntrustedProxyException when asking for authorized resources since it would be considered unauthorized

This closes #258.

Signed-off-by: Kevin Doran <kdoran@apache.org>
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 959e29e..f69ac3c 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
@@ -248,12 +248,8 @@
                             try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(authorizerClassLoader)) {
                                 authorizer.onConfigured(authorizerConfigurationContext);
                             }
-
-                            // wrap the integrity checked Authorizer with the FrameworkAuthorizer
-                            authorizer = createFrameworkAuthorizer(authorizer);
                         }
 
-
                     } catch (AuthorizerFactoryException e) {
                         throw e;
                     } catch (Exception e) {
@@ -427,14 +423,6 @@
         return instance;
     }
 
-    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)) {
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
deleted file mode 100644
index 08fb8f0..0000000
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkAuthorizer.java
+++ /dev/null
@@ -1,189 +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.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
deleted file mode 100644
index 478482e..0000000
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkManagedAuthorizer.java
+++ /dev/null
@@ -1,54 +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.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/StandardAuthorizableLookup.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
index 18c2a52..6f68ebe 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
@@ -17,15 +17,22 @@
 package org.apache.nifi.registry.security.authorization;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.exception.ResourceNotFoundException;
 import org.apache.nifi.registry.security.authorization.resource.Authorizable;
 import org.apache.nifi.registry.security.authorization.resource.InheritingAuthorizable;
+import org.apache.nifi.registry.security.authorization.resource.ProxyChainAuthorizable;
+import org.apache.nifi.registry.security.authorization.resource.PublicCheckingAuthorizable;
 import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
 import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.service.RegistryService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import java.util.Objects;
+
 @Component
 public class StandardAuthorizableLookup implements AuthorizableLookup {
 
@@ -103,14 +110,21 @@
         }
     };
 
+    private final RegistryService registryService;
+
+    @Autowired
+    public StandardAuthorizableLookup(final RegistryService registryService) {
+        this.registryService = Objects.requireNonNull(registryService);
+    }
+
     @Override
     public Authorizable getActuatorAuthorizable() {
-        return ACTUATOR_AUTHORIZABLE;
+        return new ProxyChainAuthorizable(ACTUATOR_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed);
     }
 
     @Override
     public Authorizable getSwaggerAuthorizable() {
-        return SWAGGER_AUTHORIZABLE;
+        return new ProxyChainAuthorizable(SWAGGER_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed);
     }
 
     @Override
@@ -120,34 +134,42 @@
 
     @Override
     public Authorizable getTenantsAuthorizable() {
-        return TENANTS_AUTHORIZABLE;
+        return new ProxyChainAuthorizable(TENANTS_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed);
     }
 
     @Override
     public Authorizable getPoliciesAuthorizable() {
-        return POLICIES_AUTHORIZABLE;
+        return new ProxyChainAuthorizable(POLICIES_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed);
     }
 
     @Override
     public Authorizable getBucketsAuthorizable() {
-        return BUCKETS_AUTHORIZABLE;
+        return new ProxyChainAuthorizable(BUCKETS_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed);
     }
 
     @Override
     public Authorizable getBucketAuthorizable(String bucketIdentifier) {
-        // Note - this returns a special Authorizable type that inherits permissions from the parent Authorizable
-        return new InheritingAuthorizable() {
+        // Note - this creates a special Authorizable type that inherits permissions from the parent Authorizable
+        final Authorizable inheritingAuthorizable = new InheritingAuthorizable() {
 
             @Override
             public Authorizable getParentAuthorizable() {
-                return getBucketsAuthorizable();
+                // Use the unwrapped buckets authorizable here so that we don't reauthorize the proxy chain
+                return BUCKETS_AUTHORIZABLE;
             }
 
             @Override
             public Resource getResource() {
                 return ResourceFactory.getBucketResource(bucketIdentifier, "Bucket with ID " + bucketIdentifier);
             }
+
         };
+
+        // Wrap the inheriting Authorizable with logic that first checks if public access is allowed, if not then delegates to the inheriting Authorizable
+        final Authorizable publicCheckingAuthorizable = new PublicCheckingAuthorizable(inheritingAuthorizable, this::isPublicAccessAllowed);
+
+        // Return ProxyChainAuthorizable -> public checking Authorizable -> inheriting Authorizable
+        return new ProxyChainAuthorizable(publicCheckingAuthorizable, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed);
     }
 
     @Override
@@ -217,4 +239,44 @@
         return authorizable;
     }
 
+    /**
+     * 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;
+        }
+
+        if (action != RequestAction.READ) {
+            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();
+        } 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;
+        }
+    }
+
 }
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java
new file mode 100644
index 0000000..fbf1580
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+public class UntrustedProxyException extends RuntimeException {
+
+    public UntrustedProxyException(String message) {
+        super(message);
+    }
+
+    public UntrustedProxyException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
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 04cb469..c461965 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
@@ -27,9 +27,7 @@
 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 {
@@ -95,10 +93,6 @@
             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()
@@ -211,18 +205,10 @@
             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/security/authorization/resource/ProxyChainAuthorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java
new file mode 100644
index 0000000..aec8d76
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java
@@ -0,0 +1,145 @@
+/*
+ * 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.resource;
+
+import org.apache.nifi.registry.security.authorization.AuthorizationResult;
+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.UntrustedProxyException;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiFunction;
+
+/**
+ * Authorizable that wraps another Authorizable and applies logic for authorizing the proxy chain, unless the resource
+ * allows public access, which then skips authorizing the proxy chain.
+ */
+public class ProxyChainAuthorizable implements Authorizable {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ProxyChainAuthorizable.class);
+
+    private final Authorizable wrappedAuthorizable;
+    private final Authorizable proxyAuthorizable;
+    private final BiFunction<Resource,RequestAction,Boolean> publicResourceCheck;
+
+    public ProxyChainAuthorizable(final Authorizable wrappedAuthorizable,
+                                  final Authorizable proxyAuthorizable,
+                                  final BiFunction<Resource,RequestAction,Boolean> publicResourceCheck) {
+        this.wrappedAuthorizable = Objects.requireNonNull(wrappedAuthorizable);
+        this.proxyAuthorizable = Objects.requireNonNull(proxyAuthorizable);
+        this.publicResourceCheck = Objects.requireNonNull(publicResourceCheck);
+    }
+
+    @Override
+    public Authorizable getParentAuthorizable() {
+        if (wrappedAuthorizable.getParentAuthorizable() == null) {
+            return null;
+        } else {
+            final Authorizable parentAuthorizable = wrappedAuthorizable.getParentAuthorizable();
+            return new ProxyChainAuthorizable(parentAuthorizable, proxyAuthorizable, publicResourceCheck);
+        }
+    }
+
+    @Override
+    public Resource getResource() {
+        return wrappedAuthorizable.getResource();
+    }
+
+    @Override
+    public AuthorizationResult checkAuthorization(final Authorizer authorizer, final RequestAction action, final NiFiUser user,
+                                                  final Map<String, String> resourceContext) {
+        final Resource requestResource = wrappedAuthorizable.getRequestedResource();
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Requested resource is {}", new Object[]{requestResource.getIdentifier()});
+        }
+
+        // if public access is allowed then we want to skip proxy authorization so just return
+        final Boolean isPublicAccessAllowed = publicResourceCheck.apply(requestResource, action);
+        if (isPublicAccessAllowed) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Proxy chain will not be checked, public access is allowed for {} on {}",
+                        new Object[]{action.toString(), requestResource.getIdentifier()});
+            }
+            return AuthorizationResult.approved();
+        }
+
+        // otherwise public access is not allowed so check the proxy chain for the given action
+        NiFiUser proxyUser = user.getChain();
+        while (proxyUser  != null) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Checking proxy [{}] for {}", new Object[]{proxyUser, action});
+            }
+
+            // if the proxy is denied then break out of the loop and return a denied result
+            final AuthorizationResult proxyAuthorizationResult = proxyAuthorizable.checkAuthorization(authorizer, action, proxyUser);
+            if (proxyAuthorizationResult.getResult() == AuthorizationResult.Result.Denied) {
+                final String deniedMessage = String.format("Untrusted proxy [%s] for %s operation.", proxyUser.getIdentity(), action.toString());
+                return AuthorizationResult.denied(deniedMessage);
+            }
+
+            proxyUser = proxyUser.getChain();
+        }
+
+        // at this point the proxy chain was approved so continue to check the original Authorizable
+        return wrappedAuthorizable.checkAuthorization(authorizer, action, user, resourceContext);
+    }
+
+    @Override
+    public void authorize(final Authorizer authorizer, final RequestAction action, final NiFiUser user,
+                          final Map<String, String> resourceContext) throws AccessDeniedException {
+        final Resource requestResource = wrappedAuthorizable.getRequestedResource();
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Requested resource is {}", new Object[]{requestResource.getIdentifier()});
+        }
+
+        // if public access is allowed then we want to skip proxy authorization so just return
+        final Boolean isPublicAccessAllowed = publicResourceCheck.apply(requestResource, action);
+        if (isPublicAccessAllowed) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Proxy chain will not be authorized, public access is allowed for {} on {}",
+                        new Object[]{action.toString(), requestResource.getIdentifier()});
+            }
+            return;
+        }
+
+        // otherwise public access is not allowed so authorize proxy chain for the given action
+        NiFiUser proxyUser = user.getChain();
+        while (proxyUser  != null) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Authorizing proxy [{}] for {}", new Object[]{proxyUser, action});
+            }
+
+            try {
+                proxyAuthorizable.authorize(authorizer, action, proxyUser);
+            } catch (final AccessDeniedException e) {
+                final String actionString = action.toString();
+                throw new UntrustedProxyException(String.format("Untrusted proxy [%s] for %s operation.", proxyUser.getIdentity(), actionString));
+            }
+            proxyUser = proxyUser.getChain();
+        }
+
+        // at this point the proxy chain was authorized so continue to authorize the original Authorizable
+        wrappedAuthorizable.authorize(authorizer, action, user, resourceContext);
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java
new file mode 100644
index 0000000..7cacb59
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java
@@ -0,0 +1,107 @@
+/*
+ * 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.resource;
+
+import org.apache.nifi.registry.security.authorization.AuthorizationResult;
+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.user.NiFiUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiFunction;
+
+/**
+ * Authorizable that first checks if public access is allowed for the resource and action. If it is then it short-circuits
+ * and returns approved, otherwise it continues and delegates to the wrapped Authorizable.
+ */
+public class PublicCheckingAuthorizable implements Authorizable {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(PublicCheckingAuthorizable.class);
+
+    private final Authorizable wrappedAuthorizable;
+    private final BiFunction<Resource, RequestAction,Boolean> publicResourceCheck;
+
+    public PublicCheckingAuthorizable(final Authorizable wrappedAuthorizable,
+                                      final BiFunction<Resource,RequestAction,Boolean> publicResourceCheck) {
+        this.wrappedAuthorizable = Objects.requireNonNull(wrappedAuthorizable);
+        this.publicResourceCheck = Objects.requireNonNull(publicResourceCheck);
+    }
+
+    @Override
+    public Authorizable getParentAuthorizable() {
+        return wrappedAuthorizable.getParentAuthorizable();
+    }
+
+    @Override
+    public Resource getResource() {
+        return wrappedAuthorizable.getResource();
+    }
+
+    @Override
+    public AuthorizationResult checkAuthorization(final Authorizer authorizer, final RequestAction action, final NiFiUser user,
+                                                  final Map<String, String> resourceContext) {
+        final Resource resource = getResource();
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Requested resource is {}", new Object[]{resource.getIdentifier()});
+        }
+
+        // if public access is allowed then return approved
+        final Boolean isPublicAccessAllowed = publicResourceCheck.apply(resource, action);
+        if(isPublicAccessAllowed) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Public access is allowed for {}", new Object[]{resource.getIdentifier()});
+            }
+            return AuthorizationResult.approved();
+        }
+
+        // otherwise delegate to the original inheriting authorizable
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Delegating to inheriting authorizable for {}", new Object[]{resource.getIdentifier()});
+        }
+        return wrappedAuthorizable.checkAuthorization(authorizer, action, user, resourceContext);
+    }
+
+    @Override
+    public void authorize(final Authorizer authorizer, final RequestAction action, final NiFiUser user,
+                          final Map<String, String> resourceContext) throws AccessDeniedException {
+        final Resource resource = getResource();
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Requested resource is {}", new Object[]{resource.getIdentifier()});
+        }
+
+        // if public access is allowed then skip authorization and return
+        final Boolean isPublicAccessAllowed = publicResourceCheck.apply(resource, action);
+        if(isPublicAccessAllowed) {
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Public access is allowed for {}", new Object[]{resource.getIdentifier()});
+            }
+            return;
+        }
+
+        // otherwise delegate to the original authorizable
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Delegating to inheriting authorizable for {}", new Object[]{resource.getIdentifier()});
+        }
+
+        wrappedAuthorizable.authorize(authorizer, action, user, resourceContext);
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
index 12f39b6..503f27c 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
@@ -39,6 +39,7 @@
 import org.apache.nifi.registry.security.authorization.Group;
 import org.apache.nifi.registry.security.authorization.ManagedAuthorizer;
 import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.UntrustedProxyException;
 import org.apache.nifi.registry.security.authorization.UserAndGroups;
 import org.apache.nifi.registry.security.authorization.UserGroupProvider;
 import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
@@ -487,7 +488,7 @@
                                         .getAuthorizableByResource(resource.getIdentifier())
                                         .authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser());
                                 return true;
-                            } catch (AccessDeniedException e) {
+                            } catch (AccessDeniedException | UntrustedProxyException e) {
                                 return false;
                             }
                         })
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 33b9f40..8035bd8 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
@@ -41,8 +41,7 @@
     def setup() {
         accessPolicyProvider.getUserGroupProvider() >> userGroupProvider
         def standardAuthorizer = new StandardManagedAuthorizer(accessPolicyProvider, userGroupProvider)
-        def frameworkAuthorizer = new FrameworkManagedAuthorizer(standardAuthorizer, registryService)
-        authorizationService = new AuthorizationService(authorizableLookup, frameworkAuthorizer, registryService)
+        authorizationService = new AuthorizationService(authorizableLookup, standardAuthorizer, registryService)
     }
 
     // ----- User tests -------------------------------------------------------
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
deleted file mode 100644
index 2cc03f8..0000000
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestFrameworkAuthorizer.java
+++ /dev/null
@@ -1,278 +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.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-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java
new file mode 100644
index 0000000..2804ac7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java
@@ -0,0 +1,404 @@
+/*
+ * 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.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.StandardNiFiUser;
+import org.apache.nifi.registry.service.RegistryService;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+
+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 TestStandardAuthorizableLookup {
+
+    private static final NiFiUser USER_NO_PROXY_CHAIN = new StandardNiFiUser.Builder()
+            .identity("user1")
+            .build();
+
+    private static final NiFiUser USER_WITH_PROXY_CHAIN = new StandardNiFiUser.Builder()
+            .identity("user1")
+            .chain(new StandardNiFiUser.Builder().identity("CN=localhost, OU=NIFI").build())
+            .build();
+
+    private Authorizer authorizer;
+    private RegistryService registryService;
+    private AuthorizableLookup authorizableLookup;
+
+    private Bucket bucketPublic;
+    private Bucket bucketNotPublic;
+
+    @Before
+    public void setup() {
+        authorizer = mock(Authorizer.class);
+        registryService = mock(RegistryService.class);
+        authorizableLookup = new StandardAuthorizableLookup(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 check method for Bucket Authorizable
+
+    @Test
+    public void testCheckReadPublicBucketWithNoProxyChain() {
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, RequestAction.READ, USER_NO_PROXY_CHAIN);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Approved, result.getResult());
+
+        // Should never call authorizer because resource is public
+        verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testCheckReadPublicBucketWithProxyChain() {
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, RequestAction.READ, USER_WITH_PROXY_CHAIN);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Approved, result.getResult());
+
+        // Should never call authorizer because resource is public
+        verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testCheckWritePublicBucketWithUnauthorizedUserAndNoProxyChain() {
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to the specific bucket
+        final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest(
+                bucketPublic.getIdentifier(), action, USER_NO_PROXY_CHAIN);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // second request will go to parent of /buckets
+        final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, USER_NO_PROXY_CHAIN);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // should reach authorizer and return denied
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, USER_NO_PROXY_CHAIN);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Denied, result.getResult());
+
+        // Should call authorizer twice for specific bucket and top-level /buckets
+        verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testCheckWritePublicBucketWithUnauthorizedProxyChain() {
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to authorize the proxy
+        final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, USER_WITH_PROXY_CHAIN.getChain());
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // the authorization of the proxy chain should return denied
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, USER_WITH_PROXY_CHAIN);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Denied, result.getResult());
+
+        // Should never call authorizer once for /proxy and then return denied
+        verify(authorizer, times(1)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testCheckWritePublicBucketWithUnauthorizedUserAndAuthorizedProxyChain() {
+        final NiFiUser user = USER_WITH_PROXY_CHAIN;
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to authorize the proxy
+        final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain());
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.approved());
+
+        // second request will be to the specific bucket
+        final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest(
+                bucketPublic.getIdentifier(), action, user);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // third request will go to parent of /buckets
+        final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, user);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // the authorization of the proxy chain should return denied
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, user);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Denied, result.getResult());
+
+        // Should call authorizer three time for /proxy, /bucket/{id}, and /buckets
+        verify(authorizer, times(3)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testCheckWritePublicBucketWithAuthorizedUserAndAuthorizedProxyChain() {
+        final NiFiUser user = USER_WITH_PROXY_CHAIN;
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to authorize the proxy
+        final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain());
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.approved());
+
+        // second request will be to the specific bucket
+        final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest(
+                bucketPublic.getIdentifier(), action, user);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.approved());
+
+        // the authorization should all return approved
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, user);
+        assertNotNull(result);
+        assertEquals(AuthorizationResult.Result.Approved, result.getResult());
+
+        // Should call authorizer two times for /proxy and /bucket/{id}
+        verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class));
+    }
+
+    // Test authorize method for Bucket Authorizable
+
+    @Test
+    public void testAuthorizeReadPublicBucketWithNoProxyChain() {
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        bucketAuthorizable.authorize(authorizer, RequestAction.READ, USER_NO_PROXY_CHAIN);
+
+        // Should never call authorizer because resource is public
+        verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testAuthorizeReadPublicBucketWithProxyChain() {
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        bucketAuthorizable.authorize(authorizer, RequestAction.READ, USER_WITH_PROXY_CHAIN);
+
+        // Should never call authorizer because resource is public
+        verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class));
+    }
+
+    @Test
+    public void testAuthorizeWritePublicBucketWithUnauthorizedUserAndNoProxyChain() {
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to the specific bucket
+        final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest(
+                bucketPublic.getIdentifier(), action, USER_NO_PROXY_CHAIN);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // second request will go to parent of /buckets
+        final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, USER_NO_PROXY_CHAIN);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // should reach authorizer and throw access denied
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        try {
+            bucketAuthorizable.authorize(authorizer, action, USER_NO_PROXY_CHAIN);
+            Assert.fail("Should have thrown exception");
+        } catch (AccessDeniedException e) {
+            // Should never call authorizer twice for specific bucket and top-level /buckets
+            verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class));
+        }
+    }
+
+    @Test
+    public void testAuthorizeWritePublicBucketWithUnauthorizedProxyChain() {
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to authorize the proxy
+        final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, USER_WITH_PROXY_CHAIN.getChain());
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // the authorization of the proxy chain should throw UntrustedProxyException
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        try {
+            bucketAuthorizable.authorize(authorizer, action, USER_WITH_PROXY_CHAIN);
+            Assert.fail("Should have thrown exception");
+        } catch (UntrustedProxyException e) {
+            // Should call authorizer once for /proxy and then throw exception
+            verify(authorizer, times(1)).authorize(any(AuthorizationRequest.class));
+        }
+    }
+
+    @Test
+    public void testAuthorizeWritePublicBucketWithUnauthorizedUserAndAuthorizedProxyChain() {
+        final NiFiUser user = USER_WITH_PROXY_CHAIN;
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to authorize the proxy
+        final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain());
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.approved());
+
+        // second request will be to the specific bucket
+        final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest(
+                bucketPublic.getIdentifier(), action, user);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // third request will go to parent of /buckets
+        final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, user);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.denied());
+
+        // the authorization of the proxy chain should throw UntrustedProxyException
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        try {
+            bucketAuthorizable.authorize(authorizer, action, user);
+            Assert.fail("Should have thrown exception");
+        } catch (AccessDeniedException e) {
+            // Should call authorizer three times for /proxy, /bucket/{id}, and /buckets
+            verify(authorizer, times(3)).authorize(any(AuthorizationRequest.class));
+        }
+    }
+
+    @Test
+    public void testAuthorizeWritePublicBucketWithAuthorizedUserAndAuthorizedProxyChain() {
+        final NiFiUser user = USER_WITH_PROXY_CHAIN;
+        final RequestAction action = RequestAction.WRITE;
+
+        // first request will be to authorize the proxy
+        final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain());
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.approved());
+
+        // second request will be to the specific bucket
+        final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest(
+                bucketPublic.getIdentifier(), action, user);
+
+        when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest))))
+                .thenReturn(AuthorizationResult.approved());
+
+        // the authorization should all return approved so no exception
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier());
+        bucketAuthorizable.authorize(authorizer, action, user);
+
+        // Should call authorizer two times for /proxy and /bucket/{id}
+        verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class));
+    }
+
+    private AuthorizationRequest getBucketAuthorizationRequest(final String bucketIdentifier, final RequestAction action, final NiFiUser user) {
+        return new AuthorizationRequest.Builder()
+                .resource(ResourceFactory.getBucketResource(bucketIdentifier, bucketIdentifier))
+                .action(action)
+                .identity(user.getIdentity())
+                .accessAttempt(true)
+                .anonymous(false)
+                .build();
+    }
+
+    private AuthorizationRequest getBucketsAuthorizationRequest(final RequestAction action, final NiFiUser user) {
+        return new AuthorizationRequest.Builder()
+                .resource(ResourceFactory.getBucketsResource())
+                .action(action)
+                .identity(user.getIdentity())
+                .accessAttempt(true)
+                .anonymous(false)
+                .build();
+    }
+
+    private AuthorizationRequest getProxyAuthorizationRequest(final RequestAction action, final NiFiUser user) {
+        return new AuthorizationRequest.Builder()
+                .resource(ResourceFactory.getProxyResource())
+                .action(action)
+                .identity(user.getIdentity())
+                .accessAttempt(true)
+                .anonymous(false)
+                .build();
+    }
+
+    /**
+     * ArugmentMatcher for AuthorizationRequest.
+     */
+    private static class AuthorizationRequestMatcher implements ArgumentMatcher<AuthorizationRequest> {
+
+        private final AuthorizationRequest expectedAuthorizationRequest;
+
+        public AuthorizationRequestMatcher(final AuthorizationRequest expectedAuthorizationRequest) {
+            this.expectedAuthorizationRequest = expectedAuthorizationRequest;
+        }
+
+        @Override
+        public boolean matches(final AuthorizationRequest authorizationRequest) {
+            if (authorizationRequest == null) {
+                return false;
+            }
+
+            final String requestResourceId = authorizationRequest.getResource().getIdentifier();
+            final String expectedResourceId = expectedAuthorizationRequest.getResource().getIdentifier();
+
+            final String requestAction = authorizationRequest.getAction().toString();
+            final String expectedAction = expectedAuthorizationRequest.getAction().toString();
+
+            final String requestUserIdentity = authorizationRequest.getIdentity();
+            final String expectedUserIdentity = authorizationRequest.getIdentity();
+
+            return requestResourceId.equals(expectedResourceId)
+                    && requestAction.equals(expectedAction)
+                    && requestUserIdentity.equals(expectedUserIdentity);
+        }
+    }
+}
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 56b7b45..3e832fa 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
@@ -109,6 +109,8 @@
      * 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
+     *
+     * @deprecated no longer populated
      */
     public List<String> getProxyIdentities() {
         return proxyIdentities;
@@ -210,6 +212,9 @@
             return this;
         }
 
+        /**
+         * @deprecated no longer populated by the framework
+         */
         public Builder proxyIdentities(final List<String> proxyIdentities) {
             this.proxyIdentities = proxyIdentities;
             return this;
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java
new file mode 100644
index 0000000..453dbd5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.UntrustedProxyException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps an UntrustedProxyException to a FORBIDDEN response.
+ */
+@Component
+@Provider
+public class UntrustedProxyExceptionMapper implements ExceptionMapper<UntrustedProxyException> {
+
+    private static Logger LOGGER = LoggerFactory.getLogger(UntrustedProxyException.class);
+
+    @Override
+    public Response toResponse(final UntrustedProxyException exception) {
+        LOGGER.info("{}. Returning {} response.", exception, Response.Status.FORBIDDEN);
+        LOGGER.debug(StringUtils.EMPTY, exception);
+
+        return Response.status(Response.Status.FORBIDDEN)
+                .entity(exception.getMessage())
+                .type("text/plain")
+                .build();
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy b/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
index e27dfbe..806f73c 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
@@ -20,6 +20,7 @@
 import org.apache.nifi.registry.security.authorization.resource.Authorizable
 import org.apache.nifi.registry.security.authorization.resource.ResourceType
 import org.apache.nifi.registry.service.AuthorizationService
+import org.apache.nifi.registry.service.RegistryService
 import org.apache.nifi.registry.web.security.authorization.HttpMethodAuthorizationRules
 import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter
 import org.apache.nifi.registry.web.security.authorization.StandardHttpMethodAuthorizationRules
@@ -34,7 +35,8 @@
 
 class ResourceAuthorizationFilterSpec extends Specification {
 
-    AuthorizableLookup authorizableLookup = new StandardAuthorizableLookup()
+    RegistryService registryService = Mock(RegistryService)
+    AuthorizableLookup authorizableLookup = new StandardAuthorizableLookup(registryService)
     AuthorizationService mockAuthorizationService = Mock(AuthorizationService)
     FilterChain mockFilterChain = Mock(FilterChain)
     ResourceAuthorizationFilter.Builder resourceAuthorizationFilterBuilder