NIFIREG-134 Enable SpringBoot Actuator endpoints

- Configures Jersey as a filter (previously was a servlet) that
  forwards requests to /actuator/* so they can be handled by Actuator
- Adds a ResourceAuthorizationFilter that performs authorization in
  the filter chain, and configures it to gate access to /actuator/*
- Adds test cases for ResourceAuthorizationFilter

This closes #97.

Signed-off-by: Bryan Bende <bbende@apache.org>
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java
index 2ba7227..1842199 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java
@@ -21,6 +21,13 @@
 public interface AuthorizableLookup {
 
     /**
+     * Get the authorizable for /actuator.
+     *
+     * @return authorizable
+     */
+    Authorizable getActuatorAuthorizable();
+
+    /**
      * Get the authorizable for /proxy.
      *
      * @return authorizable
@@ -59,6 +66,8 @@
 
     /**
      * Get the authorizable of the specified resource.
+     * If the resource is authorized by its base/top-level
+     * resource type, the authorizable for the base type will be returned.
      *
      * @param resource resource
      * @return authorizable
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizeAccess.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizeAccess.java
deleted file mode 100644
index 94dc3be..0000000
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizeAccess.java
+++ /dev/null
@@ -1,21 +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;
-
-public interface AuthorizeAccess {
-    void authorize(AuthorizableLookup lookup);
-}
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
index 00d318a..50b7185 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
@@ -22,11 +22,15 @@
 import org.apache.nifi.registry.security.authorization.resource.InheritingAuthorizable;
 import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
 import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 
 @Component
 public class StandardAuthorizableLookup implements AuthorizableLookup {
 
+    private static final Logger logger = LoggerFactory.getLogger(StandardAuthorizableLookup.class);
+
     private static final Authorizable TENANTS_AUTHORIZABLE = new Authorizable() {
         @Override
         public Authorizable getParentAuthorizable() {
@@ -75,6 +79,23 @@
         }
     };
 
+    private static final Authorizable ACTUATOR_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getActuatorResource();
+        }
+    };
+
+    @Override
+    public Authorizable getActuatorAuthorizable() {
+        return ACTUATOR_AUTHORIZABLE;
+    }
+
     @Override
     public Authorizable getProxyAuthorizable() {
         return PROXY_AUTHORIZABLE;
@@ -114,12 +135,7 @@
 
     @Override
     public Authorizable getAuthorizableByResource(String resource) {
-        ResourceType resourceType = null;
-        for (ResourceType type : ResourceType.values()) {
-            if (resource.equals(type.getValue()) || resource.startsWith(type.getValue() + "/")) {
-                resourceType = type;
-            }
-        }
+        ResourceType resourceType = ResourceType.mapFullResourcePathToResourceType(resource);
 
         if (resourceType == null) {
             throw new ResourceNotFoundException("Unrecognized resource: " + resource);
@@ -129,21 +145,10 @@
     }
 
     private Authorizable getAuthorizableByResource(final ResourceType resourceType, final String resource) {
-        final String childResourceId = StringUtils.substringAfter(resource, resourceType.getValue());
-        if (childResourceId.startsWith("/")) {
-            return getAuthorizableByChildResource(resourceType, childResourceId.substring(1));
-        } else {
-            return getAuthorizableByResource(resourceType);
-        }
-    }
-
-    private Authorizable getAuthorizableByResource(final ResourceType resourceType) {
         Authorizable authorizable = null;
         switch (resourceType) {
 
-            case Bucket:
-                authorizable = getBucketsAuthorizable();
-                break;
+            /* Access to these resources are always authorized by the top-level resource */
             case Policy:
                 authorizable = getPoliciesAuthorizable();
                 break;
@@ -152,10 +157,24 @@
                 break;
             case Proxy:
                 authorizable = getProxyAuthorizable();
+                break;
+            case Actuator:
+                authorizable = getActuatorAuthorizable();
+                break;
+
+            /* Access to buckets can be authorized by the top-level /buckets resource or an individual /buckets/{id} resource */
+            case Bucket:
+                final String childResourceId = StringUtils.substringAfter(resource, resourceType.getValue());
+                if (childResourceId.startsWith("/")) {
+                    authorizable = getAuthorizableByChildResource(resourceType, childResourceId);
+                } else {
+                    authorizable = getBucketsAuthorizable();
+                }
         }
 
         if (authorizable == null) {
-            throw new IllegalArgumentException("An unexpected type of resource in this policy " + resourceType.getValue());
+            logger.debug("Could not determine the Authorizable for resource type='{}', path='{}', ", resourceType.getValue(), resource);
+            throw new IllegalArgumentException("This an unexpected type of authorizable resource: " + resourceType.getValue());
         }
 
         return authorizable;
@@ -165,8 +184,12 @@
         Authorizable authorizable;
         switch (baseResourceType) {
             case Bucket:
-                authorizable = getBucketAuthorizable(childResourceId);
-                break;
+                String[] childResourcePathParts = childResourceId.split("/");
+                if (childResourcePathParts.length >= 1) {
+                    final String bucketId = childResourcePathParts[1];
+                    authorizable = getBucketAuthorizable(bucketId);
+                    break;
+                }
             default:
                 throw new IllegalArgumentException("Unexpected lookup for child resource authorizable for base resource type " + baseResourceType.getValue());
         }
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java
index e4a03f3..83ceb50 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java
@@ -125,6 +125,9 @@
             new ResourceActionPair("/buckets", READ_CODE),
             new ResourceActionPair("/buckets", WRITE_CODE),
             new ResourceActionPair("/buckets", DELETE_CODE),
+            new ResourceActionPair("/actuator", READ_CODE),
+            new ResourceActionPair("/actuator", WRITE_CODE),
+            new ResourceActionPair("/actuator", DELETE_CODE),
             new ResourceActionPair("/proxy", WRITE_CODE)
     };
 
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java
index b81b873..845e719 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java
@@ -39,24 +39,6 @@
         }
     };
 
-
-    private final static Resource POLICY_RESOURCE = new Resource() {
-        @Override
-        public String getIdentifier() {
-            return ResourceType.Policy.getValue();
-        }
-
-        @Override
-        public String getName() {
-            return "Policies for ";
-        }
-
-        @Override
-        public String getSafeDescription() {
-            return "the policies for ";
-        }
-    };
-
     private final static Resource PROXY_RESOURCE = new Resource() {
         @Override
         public String getIdentifier() {
@@ -95,7 +77,7 @@
 
         @Override
         public String getIdentifier() {
-            return "/policies";
+            return ResourceType.Policy.getValue();
         }
 
         @Override
@@ -109,6 +91,32 @@
         }
     };
 
+    private final static Resource ACTUATOR_RESOURCE = new Resource() {
+        @Override
+        public String getIdentifier() {
+            return ResourceType.Actuator.getValue();
+        }
+
+        @Override
+        public String getName() {
+            return "Actuator";
+        }
+
+        @Override
+        public String getSafeDescription() {
+            return "actuator";
+        }
+    };
+
+    /**
+     * Gets the Resource for actuator system management endpoints.
+     *
+     * @return  The resource for actuator system management endpoints.
+     */
+    public static Resource getActuatorResource() {
+        return ACTUATOR_RESOURCE;
+    }
+
     /**
      * Gets the Resource for proxying a user request.
      *
@@ -152,33 +160,6 @@
     }
 
     /**
-     * Gets a Resource for accessing a resources's policies.
-     *
-     * @param resource      The resource being accessed
-     * @return              The resource
-     */
-    public static Resource getPolicyResource(final Resource resource) {
-        Objects.requireNonNull(resource, "The resource type must be specified.");
-
-        return new Resource() {
-            @Override
-            public String getIdentifier() {
-                return String.format("%s%s", POLICY_RESOURCE.getIdentifier(), resource.getIdentifier());
-            }
-
-            @Override
-            public String getName() {
-                return POLICY_RESOURCE.getName() + resource.getName();
-            }
-
-            @Override
-            public String getSafeDescription() {
-                return POLICY_RESOURCE.getSafeDescription() + resource.getSafeDescription();
-            }
-        };
-    }
-
-    /**
      * Get a Resource object for any object that has a base type and an identifier, ie:
      * /buckets/{uuid}
      *
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java
index 7274b56..00358ff 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java
@@ -20,7 +20,8 @@
     Bucket("/buckets"),
     Policy("/policies"),
     Proxy("/proxy"),
-    Tenant("/tenants");
+    Tenant("/tenants"),
+    Actuator("/actuator");
 
     final String value;
 
@@ -48,4 +49,38 @@
 
         return type;
     }
+
+    /**
+     * Map an arbitrary resource path to its base resource type. The base resource type is
+     * what the resource path starts with.
+     *
+     * The resourcePath arg is expected to be a string of the format:
+     *
+     * {ResourceTypeValue}/arbitrary/sub-resource/path
+     *
+     * For example:
+     *   /buckets -> ResourceType.Bucket
+     *   /buckets/bucketId -> ResourceType.Bucket
+     *   /policies/read/buckets -> ResourceType.Policy
+     *
+     * @param resourcePath the path component of a URI (not including the context path)
+     * @return the base resource type
+     */
+    public static ResourceType mapFullResourcePathToResourceType(final String resourcePath) {
+        if (resourcePath == null) {
+            throw new IllegalArgumentException("Resource path must not be null");
+        }
+
+        ResourceType type = null;
+
+        for (final ResourceType rt : values()) {
+            final String rtValue = rt.getValue();
+            if(resourcePath.equals(rtValue) || resourcePath.startsWith(rtValue + "/"))  {
+                type = rt;
+                break;
+            }
+        }
+
+        return type;
+    }
 }
\ No newline at end of file
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
index 219885c..12f6f26 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
@@ -29,7 +29,6 @@
 import org.apache.nifi.registry.security.authorization.AccessPolicyProvider;
 import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext;
 import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
-import org.apache.nifi.registry.security.authorization.AuthorizeAccess;
 import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
 import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
@@ -100,8 +99,16 @@
 
     // ---------------------- Authorization methods -------------------------------------
 
-    public void authorizeAccess(final AuthorizeAccess authorizeAccess) {
-        authorizeAccess.authorize(authorizableLookup);
+    public AuthorizableLookup getAuthorizableLookup() {
+        return authorizableLookup;
+    }
+
+    public Authorizer getAuthorizer() {
+        return authorizer;
+    }
+
+    public void authorize(Authorizable authorizable, RequestAction action) throws AccessDeniedException {
+        authorizable.authorize(authorizer, action, NiFiUserUtils.getNiFiUser());
     }
 
     // ---------------------- Permissions methods ---------------------------------------
@@ -520,6 +527,9 @@
         if (includeFilter == null || includeFilter.equals(ResourceType.Proxy)) {
             resources.add(ResourceFactory.getProxyResource());
         }
+        if (includeFilter == null || includeFilter.equals(ResourceType.Actuator)) {
+            resources.add(ResourceFactory.getActuatorResource());
+        }
         if (includeFilter == null || includeFilter.equals(ResourceType.Bucket)) {
             resources.add(ResourceFactory.getBucketsResource());
             // add all buckets
diff --git a/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy b/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
index ead72d4..8bde97a 100644
--- a/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
+++ b/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
@@ -536,14 +536,15 @@
 
         then:
         resources != null
-        resources.size() == 6
+        resources.size() == 7
         def sortedResources = resources.sort{it.identifier}
-        sortedResources[0].identifier == "/buckets"
-        sortedResources[1].identifier == "/buckets/b1"
-        sortedResources[2].identifier == "/buckets/b2"
-        sortedResources[3].identifier == "/policies"
-        sortedResources[4].identifier == "/proxy"
-        sortedResources[5].identifier == "/tenants"
+        sortedResources[0].identifier == "/actuator"
+        sortedResources[1].identifier == "/buckets"
+        sortedResources[2].identifier == "/buckets/b1"
+        sortedResources[3].identifier == "/buckets/b2"
+        sortedResources[4].identifier == "/policies"
+        sortedResources[5].identifier == "/proxy"
+        sortedResources[6].identifier == "/tenants"
 
     }
 
@@ -575,6 +576,7 @@
         def denied = Mock(Authorizable)
         denied.authorize(_, _, _) >> { throw new AccessDeniedException("") }
 
+        authorizableLookup.getAuthorizableByResource("/actuator")   >> denied
         authorizableLookup.getAuthorizableByResource("/buckets")    >> authorized
         authorizableLookup.getAuthorizableByResource("/buckets/b1") >> authorized
         authorizableLookup.getAuthorizableByResource("/buckets/b2") >> denied
diff --git a/nifi-registry-web-api/pom.xml b/nifi-registry-web-api/pom.xml
index 797f31e..e991836 100644
--- a/nifi-registry-web-api/pom.xml
+++ b/nifi-registry-web-api/pom.xml
@@ -240,5 +240,23 @@
             <version>3.2.1</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <version>1.0-groovy-2.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>2.4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib-nodep</artifactId>
+            <version>2.2.2</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
index 29c6480..256efb4 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
@@ -18,9 +18,11 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
 import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
 
+import java.util.Properties;
+
 /**
  * Main class for starting the NiFi Registry Web API as a Spring Boot application.
  *
@@ -31,12 +33,27 @@
  *
  * WebMvcAutoConfiguration is excluded because our web app is using Jersey in place of SpringMVC
  */
-@SpringBootApplication(exclude = WebMvcAutoConfiguration.class)
+@SpringBootApplication
 public class NiFiRegistryApiApplication extends SpringBootServletInitializer {
 
     public static final String NIFI_REGISTRY_PROPERTIES_ATTRIBUTE = "nifi-registry.properties";
     public static final String NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE = "nifi-registry.key";
 
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+        final Properties defaultProperties = new Properties();
+
+        // Enable Actuator Endpoints
+        defaultProperties.setProperty("management.endpoints.web.expose", "*");
+
+        // Run Jersey as a filter instead of a servlet so that requests can be forwarded to other handlers (e.g., actuator)
+        defaultProperties.setProperty("spring.jersey.type", "filter");
+
+        return application
+                .sources(NiFiRegistryApiApplication.class)
+                .properties(defaultProperties);
+    }
+
     public static void main(String[] args) {
         SpringApplication.run(NiFiRegistryApiApplication.class, args);
     }
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
index 878ec90..097fc47 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
@@ -26,6 +26,7 @@
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.server.ServerProperties;
 import org.glassfish.jersey.server.filter.HttpMethodOverrideFilter;
+import org.glassfish.jersey.servlet.ServletProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.context.annotation.Configuration;
@@ -67,6 +68,9 @@
         // if this value needs to be changed, kerberos authentication needs to move to filter chain
         // so it can directly set the HttpServletResponse instead of indirectly through a JAX-RS Response
         property(ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, true);
+
+        // configure jersey to ignore resource paths for actuator
+        property(ServletProperties.FILTER_STATIC_CONTENT_REGEX, "/actuator.*");
     }
 
 }
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
index 9950bb3..ac84b2f 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
@@ -32,7 +32,6 @@
 import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
 import org.apache.nifi.registry.service.AuthorizationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -69,11 +68,14 @@
 
     private static final Logger logger = LoggerFactory.getLogger(AccessPolicyResource.class);
 
+    private Authorizer authorizer;
+
     @Autowired
     public AccessPolicyResource(
             Authorizer authorizer,
             AuthorizationService authorizationService) {
-        super(authorizer, authorizationService);
+        super(authorizationService);
+        this.authorizer = authorizer;
     }
 
     /**
@@ -392,10 +394,8 @@
     }
 
     private void authorizeAccess(RequestAction actionType) {
-        authorizationService.authorizeAccess(lookup -> {
-            final Authorizable policiesAuthorizable = lookup.getPoliciesAuthorizable();
-            policiesAuthorizable.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser());
-        });
+        final Authorizable policiesAuthorizable = authorizableLookup.getPoliciesAuthorizable();
+        authorizationService.authorize(policiesAuthorizable, actionType);
     }
 
     private String generateAccessPolicyUri(final AccessPolicySummary accessPolicy) {
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
index d0be42c..8f983cf 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
@@ -18,11 +18,10 @@
 
 import org.apache.nifi.registry.authorization.Resource;
 import org.apache.nifi.registry.bucket.BucketItem;
-import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.security.authorization.resource.Authorizable;
 import org.apache.nifi.registry.security.authorization.resource.ResourceType;
-import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
 import org.apache.nifi.registry.service.AuthorizationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -36,20 +35,17 @@
     private static final Logger logger = LoggerFactory.getLogger(AuthorizableApplicationResource.class);
 
     protected final AuthorizationService authorizationService;
-    protected final Authorizer authorizer;
+    protected final AuthorizableLookup authorizableLookup;
 
     protected AuthorizableApplicationResource(
-            Authorizer authorizer,
             AuthorizationService authorizationService) {
-        this.authorizer = authorizer;
         this.authorizationService = authorizationService;
+        this.authorizableLookup = authorizationService.getAuthorizableLookup();
     }
 
     protected void authorizeBucketAccess(RequestAction actionType, String bucketIdentifier) {
-        authorizationService.authorizeAccess(lookup -> {
-            final Authorizable bucketAccessPolicy = lookup.getBucketAuthorizable(bucketIdentifier);
-            bucketAccessPolicy.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser());
-        });
+        final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketIdentifier);
+        authorizationService.authorize(bucketAuthorizable, actionType);
     }
 
     protected void authorizeBucketItemAccess(RequestAction actionType, BucketItem bucketItem) {
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
index 788f318..a53d6e2 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
@@ -31,7 +31,6 @@
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
-import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
 import org.apache.nifi.registry.service.AuthorizationService;
@@ -78,9 +77,8 @@
             final RegistryService registryService,
             final LinkService linkService,
             final PermissionsService permissionsService,
-            final AuthorizationService authorizationService,
-            final Authorizer authorizer) {
-        super(authorizer, authorizationService);
+            final AuthorizationService authorizationService) {
+        super(authorizationService);
         this.registryService = registryService;
         this.linkService = linkService;
         this.permissionsService =permissionsService;
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
index f94117d..5f75e80 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
@@ -28,11 +28,9 @@
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.field.Fields;
-import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
 import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
 import org.apache.nifi.registry.service.AuthorizationService;
 import org.apache.nifi.registry.service.RegistryService;
 import org.apache.nifi.registry.web.link.LinkService;
@@ -85,9 +83,8 @@
             final RegistryService registryService,
             final LinkService linkService,
             final PermissionsService permissionsService,
-            final AuthorizationService authorizationService,
-            final Authorizer authorizer) {
-        super(authorizer, authorizationService);
+            final AuthorizationService authorizationService) {
+        super(authorizationService);
         this.registryService = registryService;
         this.linkService = linkService;
         this.permissionsService = permissionsService;
@@ -278,10 +275,8 @@
     }
 
     private void authorizeAccess(RequestAction actionType) throws AccessDeniedException {
-        authorizationService.authorizeAccess(lookup -> {
-            final Authorizable bucketsAuthorizable = lookup.getBucketsAuthorizable();
-            bucketsAuthorizable.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser());
-        });
+        final Authorizable bucketsAuthorizable = authorizableLookup.getBucketsAuthorizable();
+        authorizationService.authorize(bucketsAuthorizable, actionType);
     }
 
 }
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
index 4ea059e..c9deb11 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
@@ -20,8 +20,6 @@
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.Authorization;
 import org.apache.nifi.registry.field.Fields;
-import org.apache.nifi.registry.security.authorization.Authorizer;
-import org.apache.nifi.registry.service.AuthorizationService;
 import org.apache.nifi.registry.service.RegistryService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
@@ -41,16 +39,12 @@
         description = "Gets metadata about flows.",
         authorizations = { @Authorization("Authorization") }
 )
-public class FlowResource extends AuthorizableApplicationResource {
+public class FlowResource extends ApplicationResource {
 
     private final RegistryService registryService;
 
     @Autowired
-    public FlowResource(
-            final RegistryService registryService,
-            final AuthorizationService authorizationService,
-            final Authorizer authorizer) {
-        super(authorizer, authorizationService);
+    public FlowResource(final RegistryService registryService) {
         this.registryService = registryService;
     }
 
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
index 645f34d..e7ae6be 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
@@ -26,7 +26,6 @@
 import io.swagger.annotations.ExtensionProperty;
 import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.field.Fields;
-import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.service.AuthorizationService;
 import org.apache.nifi.registry.service.RegistryService;
@@ -74,9 +73,8 @@
             final RegistryService registryService,
             final LinkService linkService,
             final PermissionsService permissionsService,
-            final AuthorizationService authorizationService,
-            final Authorizer authorizer) {
-        super(authorizer, authorizationService);
+            final AuthorizationService authorizationService) {
+        super(authorizationService);
         this.registryService = registryService;
         this.linkService = linkService;
         this.permissionsService = permissionsService;
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
index 2fc2e97..d00038d 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
@@ -32,7 +32,6 @@
 import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
 import org.apache.nifi.registry.security.authorization.RequestAction;
 import org.apache.nifi.registry.security.authorization.resource.Authorizable;
-import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
 import org.apache.nifi.registry.service.AuthorizationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,11 +67,12 @@
 
     private static final Logger logger = LoggerFactory.getLogger(TenantResource.class);
 
+    private Authorizer authorizer;
+
     @Autowired
-    public TenantResource(
-            Authorizer authorizer,
-            AuthorizationService authorizationService) {
-        super(authorizer, authorizationService);
+    public TenantResource(AuthorizationService authorizationService) {
+        super(authorizationService);
+        authorizer = authorizationService.getAuthorizer();
     }
 
 
@@ -563,10 +563,8 @@
     }
 
     private void authorizeAccess(RequestAction actionType) {
-        authorizationService.authorizeAccess(lookup -> {
-            final Authorizable tenantsAuthorizable = lookup.getTenantsAuthorizable();
-            tenantsAuthorizable.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser());
-        });
+        final Authorizable tenantsAuthorizable = authorizableLookup.getTenantsAuthorizable();
+        authorizationService.authorize(tenantsAuthorizable, actionType);
     }
 
     private String generateUserUri(final User user) {
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
index 9a5d18b..1d484c1 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
@@ -17,7 +17,8 @@
 package org.apache.nifi.registry.web.security;
 
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
-import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.service.AuthorizationService;
 import org.apache.nifi.registry.web.security.authentication.AnonymousIdentityFilter;
 import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider;
 import org.apache.nifi.registry.web.security.authentication.IdentityFilter;
@@ -25,6 +26,7 @@
 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;
+import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -38,6 +40,7 @@
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
 import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
 
 import javax.servlet.ServletException;
@@ -59,7 +62,7 @@
     private NiFiRegistryProperties properties;
 
     @Autowired
-    private Authorizer authorizer;
+    private AuthorizationService authorizationService;
 
     private AnonymousIdentityFilter anonymousAuthenticationFilter = new AnonymousIdentityFilter();
 
@@ -73,6 +76,8 @@
     private IdentityFilter jwtAuthenticationFilter;
     private IdentityAuthenticationProvider jwtAuthenticationProvider;
 
+    private ResourceAuthorizationFilter resourceAuthorizationFilter;
+
     public NiFiRegistrySecurityConfig() {
         super(true); // disable defaults
     }
@@ -112,6 +117,12 @@
             // 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,
+        // insert the ResourceAuthorizationFilter to do its authorization checks
+        http.addFilterAfter(resourceAuthorizationFilter(), FilterSecurityInterceptor.class);
+
     }
 
     @Override
@@ -130,7 +141,7 @@
 
     private IdentityAuthenticationProvider x509AuthenticationProvider() {
         if (x509AuthenticationProvider == null) {
-            x509AuthenticationProvider = new X509IdentityAuthenticationProvider(properties, authorizer, x509IdentityProvider);
+            x509AuthenticationProvider = new X509IdentityAuthenticationProvider(properties, authorizationService.getAuthorizer(), x509IdentityProvider);
         }
         return x509AuthenticationProvider;
     }
@@ -144,11 +155,21 @@
 
     private IdentityAuthenticationProvider jwtAuthenticationProvider() {
         if (jwtAuthenticationProvider == null) {
-            jwtAuthenticationProvider = new IdentityAuthenticationProvider(properties, authorizer, jwtIdentityProvider);
+            jwtAuthenticationProvider = new IdentityAuthenticationProvider(properties, authorizationService.getAuthorizer(), jwtIdentityProvider);
         }
         return jwtAuthenticationProvider;
     }
 
+    private ResourceAuthorizationFilter resourceAuthorizationFilter() {
+        if (resourceAuthorizationFilter == null) {
+            resourceAuthorizationFilter = ResourceAuthorizationFilter.builder()
+                    .setAuthorizationService(authorizationService)
+                    .addResourceType(ResourceType.Actuator)
+                    .build();
+        }
+        return resourceAuthorizationFilter;
+    }
+
     private AuthenticationEntryPoint http401AuthenticationEntryPoint() {
         // This gets used for both secured and unsecured configurations. It will be called by Spring Security if a request makes it through the filter chain without being authenticated.
         // For unsecured, this should never be reached because the custom AnonymousAuthenticationFilter should always populate a fully-authenticated anonymous user
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationFilter.java
deleted file mode 100644
index 8367eec..0000000
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationFilter.java
+++ /dev/null
@@ -1,148 +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;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
-import org.apache.nifi.registry.security.authentication.IdentityProvider;
-import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
-import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
-import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException;
-import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.AuthenticationServiceException;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
-import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-import org.springframework.security.web.util.matcher.RequestMatcher;
-
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.io.PrintWriter;
-
-/**
- * Note: This class is deprecated and is being considered for complete removal in favor of using {@link IdentityFilter}.
- *       It is remaining in place for the time being until the pattern of authentication implemented by {@link IdentityFilter}
- *       has been more thoroughly vetted in real use.
- */
-@Deprecated
-public class IdentityAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
-
-    private static final RequestMatcher requiresAuthenticationRequestMatcher = new RequestMatcher() {
-        @Override
-        public boolean matches(HttpServletRequest httpServletRequest) {
-            return NiFiUserUtils.getNiFiUser() == null;
-        }
-    };
-
-    private final IdentityProvider identityProvider;
-
-    public IdentityAuthenticationFilter(IdentityProvider identityProvider, AuthenticationManager authenticationManager, String defaultFilterProcessesUrl) {
-        super(defaultFilterProcessesUrl);
-        super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl)); // Authentication will only be initiated for the request url matching this pattern
-        setAuthenticationManager(authenticationManager);
-        this.identityProvider = identityProvider;
-    }
-
-    public IdentityAuthenticationFilter(IdentityProvider identityProvider, AuthenticationManager authenticationManager) {
-        super(requiresAuthenticationRequestMatcher);
-        setAuthenticationManager(authenticationManager);
-        this.identityProvider = identityProvider;
-    }
-
-    @Override
-    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
-
-        // Only require authentication from an identity provider if the NiFi registry is running securely.
-        if (!httpServletRequest.isSecure()) {
-            throw new InvalidAuthenticationException("Authentication of user identity claim is only avaialble when running a securely.");
-        }
-
-        AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest);
-        if (authenticationRequest == null) {
-            throw new InvalidAuthenticationException("User credentials not found in httpServletRequest by " + identityProvider.getClass().getSimpleName());
-        }
-        Authentication authentication = new AuthenticationRequestToken(authenticationRequest, identityProvider.getClass(), httpServletRequest.getRemoteAddr());
-        Authentication authenticationResult = getAuthenticationManager().authenticate(authentication); // See IdentityAuthenticationProvider for authentication impl.
-        if (authenticationResult == null) {
-            throw new InvalidAuthenticationException("User credentials not authenticated by " + identityProvider.getClass().getSimpleName());
-        }
-
-        return authenticationResult;
-        // Super class will invoke successfulAuthentication() or unsuccessfulAuthentication() depending on the outcome of the authentication attempt
-    }
-
-    @Override
-    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
-
-        logger.info("Authentication success for " + authResult);
-
-        SecurityContextHolder.getContext().setAuthentication(authResult);
-        if (StringUtils.isNotBlank(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN))) {
-            response.setHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString());
-        }
-
-        // continue the filter chain, which now holds a NiFiUser in the SecurityContext's authentication
-        chain.doFilter(request, response);
-    }
-
-    @Override
-    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
-        this.logger.debug("Authentication request failed: " + failed.toString(), failed);
-
-        SecurityContextHolder.clearContext();
-        this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
-
-        // populate the response
-        if (StringUtils.isNotBlank(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN))) {
-            response.setHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_DETAILS, failed.getMessage());
-        }
-
-        // set the response status
-        response.setContentType("text/plain");
-
-        // write the response message
-        PrintWriter out = response.getWriter();
-
-        // use the type of authentication exception to determine the response code
-        if (failed instanceof InvalidAuthenticationException) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            out.println(failed.getMessage());
-        } else if (failed instanceof UntrustedProxyException) { // thrown in X509IdentityProviderAuthenticationProvider
-            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
-            out.println(failed.getMessage());
-        } else if (failed instanceof AuthenticationServiceException) {
-            logger.error(String.format("Unable to authorize: %s", failed.getMessage()), failed);
-            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-            out.println(String.format("Unable to authorize: %s", failed.getMessage()));
-        } else {
-            logger.error(String.format("Unable to authorize: %s", failed.getMessage()), failed);
-            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
-            out.println("Access is denied.");
-        }
-
-        // log the failure
-        logger.warn(String.format("Rejecting access to web api: %s", failed.getMessage()));
-        logger.debug(StringUtils.EMPTY, failed);
-    }
-
-}
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java
index 40c2662..cd5e2bf 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java
@@ -64,8 +64,8 @@
         }
 
         if (credentialsAlreadyPresent()) {
-            logger.debug("Credentials already extracted for {}, skipping credentials extraction filter for {}",
-                    SecurityContextHolder.getContext().getAuthentication().getPrincipal(),
+            logger.debug("Credentials already extracted for [{}], skipping credentials extraction filter using {}",
+                    SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString(),
                     identityProvider.getClass().getSimpleName());
             filterChain.doFilter(servletRequest, servletResponse);
             return;
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java
new file mode 100644
index 0000000..c940359
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.springframework.http.HttpMethod;
+
+public interface HttpMethodAuthorizationRules {
+
+    default boolean requiresAuthorization(HttpMethod httpMethod) {
+        return true;
+    }
+
+    default RequestAction mapHttpMethodToAction(HttpMethod httpMethod) {
+
+        switch (httpMethod) {
+            case TRACE:
+            case OPTIONS:
+            case HEAD:
+            case GET:
+                return RequestAction.READ;
+            case POST:
+            case PUT:
+            case PATCH:
+                return RequestAction.WRITE;
+            case DELETE:
+                return RequestAction.DELETE;
+            default:
+                throw new IllegalArgumentException("Unknown http method: " + httpMethod);
+        }
+
+    }
+
+}
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java
new file mode 100644
index 0000000..6e551e1
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java
@@ -0,0 +1,218 @@
+/*
+ * 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.authorization;
+
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This filter is designed to perform a resource authorization check in the Spring Security filter chain.
+ *
+ * It authorizes the current authenticated user for the {@link RequestAction} (based on the HttpMethod) requested
+ * on the {@link ResourceType} (based on the URI path).
+ *
+ * This filter is designed to be place after any authentication and before any application endpoints.
+ *
+ * This filter can be used in place of or in addition to authorization checks that occur in the application
+ * downstream of this filter.
+ *
+ * To configure this filter, provide an {@link AuthorizationService} that will be used to perform the authorization
+ * check, as well as a set of rules that control which resource and HTTP methods are handled by this filter.
+ *
+ * Any (ResourceType, HttpMethod) pair that is not configured to require authorization by this filter will be
+ * allowed to proceed in the filter chain without an authorization check.
+ *
+ * Any (ResourceType, HttpMethod) pair that is configured to require authorization by this filter will map
+ * the HttpMethod to a NiFi Registry RequestAction (configurable when creating this filter), and the
+ * (Resource Authorizable, RequestAction) pair will be sent to the AuthorizationService, which will use the
+ * configured Authorizer to authorize the current user for the action on the requested resource.
+ */
+public class ResourceAuthorizationFilter extends GenericFilterBean {
+
+    private static final Logger logger = LoggerFactory.getLogger(ResourceAuthorizationFilter.class);
+
+    private Map<ResourceType, HttpMethodAuthorizationRules> resourceTypeAuthorizationRules;
+    private AuthorizationService authorizationService;
+    private AuthorizableLookup authorizableLookup;
+
+    ResourceAuthorizationFilter(Builder builder) {
+        if (builder.getAuthorizationService() == null || builder.getResourceTypeAuthorizationRules() == null) {
+            throw new IllegalArgumentException("Builder is missing one or more required fields [authorizationService, resourceTypeAuthorizationRules].");
+        }
+        this.resourceTypeAuthorizationRules = builder.getResourceTypeAuthorizationRules();
+        this.authorizationService = builder.getAuthorizationService();
+        this.authorizableLookup = this.authorizationService.getAuthorizableLookup();
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+
+        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
+        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
+
+        boolean authorizationCheckIsRequired = false;
+        String resourcePath = null;
+        RequestAction action = null;
+
+        // Only require authorization if the NiFi Registry is running securely.
+        if (servletRequest.isSecure()) {
+
+            // Only require authorization for resources for which this filter has been configured
+            resourcePath = httpServletRequest.getServletPath();
+            if (resourcePath != null) {
+                final ResourceType resourceType = ResourceType.mapFullResourcePathToResourceType(resourcePath);
+                final HttpMethodAuthorizationRules authorizationRules = resourceTypeAuthorizationRules.get(resourceType);
+                if (authorizationRules != null) {
+                    final String httpMethodStr = httpServletRequest.getMethod().toUpperCase();
+                    HttpMethod httpMethod = HttpMethod.resolve(httpMethodStr);
+
+                    // Only require authorization for HTTP methods included in this resource type's rule set
+                    if (httpMethod != null && authorizationRules.requiresAuthorization(httpMethod)) {
+                        authorizationCheckIsRequired = true;
+                        action = authorizationRules.mapHttpMethodToAction(httpMethod);
+                    }
+                }
+            }
+        }
+
+        if (!authorizationCheckIsRequired) {
+            forwardRequestWithoutAuthorizationCheck(httpServletRequest, httpServletResponse, filterChain);
+            return;
+        }
+
+        // Perform authorization check
+        try {
+            authorizeAccess(resourcePath, action);
+            successfulAuthorization(httpServletRequest, httpServletResponse, filterChain);
+        } catch (Exception e) {
+            logger.debug("Exception occurred while performing authorization check.", e);
+            failedAuthorization(httpServletRequest, httpServletResponse, filterChain, e);
+        }
+    }
+
+    private boolean userIsAuthenticated() {
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        return (user != null && !user.isAnonymous());
+    }
+
+    private void authorizeAccess(String path, RequestAction action) throws AccessDeniedException {
+
+        if (path == null || action == null) {
+            throw new IllegalArgumentException("Authorization is required, but a required input [resource, action] is absent.");
+        }
+
+        Authorizable authorizable = authorizableLookup.getAuthorizableByResource(path);
+
+        if (authorizable == null) {
+            throw new IllegalStateException("Resource Authorization Filter configured for non-authorizable resource: " + path);
+        }
+
+        // throws AccessDeniedException if current user is not authorized to perform requested action on resource
+        authorizationService.authorize(authorizable, action);
+    }
+
+    private void forwardRequestWithoutAuthorizationCheck(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
+        logger.debug("Request filter authorization check is not required for this HTTP Method on this resource. " +
+                "Allowing request to proceed. An additional authorization check might be performed downstream of this filter.");
+        chain.doFilter(req, res);
+    }
+
+    private void successfulAuthorization(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
+        logger.debug("Request filter authorization check passed. Allowing request to proceed.");
+        chain.doFilter(req, res);
+    }
+
+    private void failedAuthorization(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Exception failure) throws IOException, ServletException {
+        logger.debug("Request filter authorization check failed. Blocking access.");
+
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final String identity = (user != null) ? user.toString() : "<no user found>";
+        final int status = !userIsAuthenticated() ? HttpServletResponse.SC_UNAUTHORIZED : HttpServletResponse.SC_FORBIDDEN;
+
+        logger.info("{} does not have permission to perform this action on the requested resource. {} Returning {} response.", identity, failure.getMessage(), status);
+        logger.debug("", failure);
+
+        if (!response.isCommitted()) {
+            response.setStatus(status);
+            response.setContentType("text/plain");
+            response.getWriter().println(String.format("Access is denied due to: %s Contact the system administrator.", failure.getLocalizedMessage()));
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        private AuthorizationService authorizationService;
+        final private Map<ResourceType, HttpMethodAuthorizationRules> resourceTypeAuthorizationRules;
+
+        // create via ResourceAuthorizationFilter.builder()
+        private Builder() {
+            this.resourceTypeAuthorizationRules = new HashMap<>();
+        }
+
+        public AuthorizationService getAuthorizationService() {
+            return authorizationService;
+        }
+
+        public Builder setAuthorizationService(AuthorizationService authorizationService) {
+            this.authorizationService = authorizationService;
+            return this;
+        }
+
+        public Map<ResourceType, HttpMethodAuthorizationRules> getResourceTypeAuthorizationRules() {
+            return resourceTypeAuthorizationRules;
+        }
+
+        public Builder addResourceType(ResourceType resourceType) {
+            this.resourceTypeAuthorizationRules.put(resourceType, new HttpMethodAuthorizationRules() {});
+            return this;
+        }
+
+        public Builder addResourceType(ResourceType resourceType, HttpMethodAuthorizationRules authorizationRules) {
+            this.resourceTypeAuthorizationRules.put(resourceType, authorizationRules);
+            return this;
+        }
+
+        public ResourceAuthorizationFilter build() {
+            return new ResourceAuthorizationFilter(this);
+        }
+    }
+
+}
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java
new file mode 100644
index 0000000..daa5a37
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java
@@ -0,0 +1,40 @@
+/*
+ * 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.authorization;
+
+import org.springframework.http.HttpMethod;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class StandardHttpMethodAuthorizationRules implements HttpMethodAuthorizationRules {
+
+    final private Set<HttpMethod> methodsRequiringAuthorization;
+
+    public StandardHttpMethodAuthorizationRules() {
+        this(EnumSet.allOf(HttpMethod.class));
+    }
+
+    public StandardHttpMethodAuthorizationRules(Set<HttpMethod> methodsRequiringAuthorization) {
+        this.methodsRequiringAuthorization = methodsRequiringAuthorization;
+    }
+
+    @Override
+    public boolean requiresAuthorization(HttpMethod httpMethod) {
+        return methodsRequiringAuthorization.contains(httpMethod);
+    }
+}
diff --git a/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy b/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
new file mode 100644
index 0000000..e27dfbe
--- /dev/null
+++ b/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
@@ -0,0 +1,170 @@
+/*
+ * 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.AccessDeniedException
+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.web.security.authorization.HttpMethodAuthorizationRules
+import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter
+import org.apache.nifi.registry.web.security.authorization.StandardHttpMethodAuthorizationRules
+import org.springframework.http.HttpMethod
+import org.springframework.mock.web.MockHttpServletRequest
+import org.springframework.mock.web.MockHttpServletResponse
+import spock.lang.Specification
+
+import javax.servlet.FilterChain
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+class ResourceAuthorizationFilterSpec extends Specification {
+
+    AuthorizableLookup authorizableLookup = new StandardAuthorizableLookup()
+    AuthorizationService mockAuthorizationService = Mock(AuthorizationService)
+    FilterChain mockFilterChain = Mock(FilterChain)
+    ResourceAuthorizationFilter.Builder resourceAuthorizationFilterBuilder
+
+    // runs before every feature method
+    def setup() {
+        mockAuthorizationService.getAuthorizableLookup() >> authorizableLookup
+        resourceAuthorizationFilterBuilder = ResourceAuthorizationFilter.builder().setAuthorizationService(mockAuthorizationService)
+    }
+
+    // runs after every feature method
+    def cleanup() {
+        //mockAuthorizationService = null
+        //mockFilterChain = null
+        resourceAuthorizationFilterBuilder = null
+    }
+
+    // runs before the first feature method
+    def setupSpec() {}
+
+    // runs after the last feature method
+    def cleanupSpec() {}
+
+
+    def "unsecured requests are allowed without an authorization check"() {
+
+        setup:
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build()
+        def httpServletRequest = createUnsecuredRequest()
+        def httpServletResponse = createResponse()
+
+        when: "doFilter() is called"
+        resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded without authorization check"
+        0 * mockAuthorizationService._
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+    }
+
+
+    def "secure requests to an unguarded resource are allowed without an authorization check"() {
+
+        setup:
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build()
+        def httpServletRequest = createSecureRequest(HttpMethod.POST, ResourceType.Bucket)
+        def httpServletResponse = createResponse()
+
+        when: "doFilter() is called"
+        resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded without authorization check"
+        0 * mockAuthorizationService._
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+    }
+
+
+    def "secure requests to an unguarded HTTP method are allowed without an authorization check"() {
+
+        setup:
+        HttpMethodAuthorizationRules rules = new StandardHttpMethodAuthorizationRules(EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE))
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator, rules).build()
+        def httpServletRequest = createSecureRequest(HttpMethod.GET, ResourceType.Actuator)
+        def httpServletResponse = createResponse()
+
+        when: "doFilter() is called"
+        resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded without authorization check"
+        0 * mockAuthorizationService._
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+    }
+
+
+    def "secure requests matching resource configuration rules perform authorization check"() {
+
+        setup:
+        // Stubbing setup for mockAuthorizationService is done in the then block as we are also verifying interactions with mock
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build()
+        def authorizedRequest = createSecureRequest(HttpMethod.GET, ResourceType.Actuator)
+        def unauthorizedRequest = createSecureRequest(HttpMethod.POST, ResourceType.Actuator)
+        def httpServletResponse = createResponse()
+
+
+        when: "doFilter() is called with an authorized request"
+        resourceAuthorizationFilter.doFilter(authorizedRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded after authorization check"
+        1 * mockAuthorizationService.authorize(_ as Authorizable, RequestAction.READ) >> { allowAccess() }
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+
+        when: "doFilter() is called with an unauthorized request"
+        resourceAuthorizationFilter.doFilter(unauthorizedRequest, httpServletResponse, mockFilterChain)
+
+        then: "authorization check is performed and response is not forwarded"
+        1 * mockAuthorizationService.authorize(_ as Authorizable, RequestAction.WRITE) >> { denyAccess() }
+        0 * mockFilterChain.doFilter(*_)
+
+    }
+
+    static private HttpServletRequest createUnsecuredRequest() {
+        HttpServletRequest req = new MockHttpServletRequest()
+        req.setScheme("http")
+        req.setSecure(false)
+        return req
+    }
+
+    static private HttpServletRequest createSecureRequest(HttpMethod httpMethod, ResourceType resourceType) {
+        HttpServletRequest req = new MockHttpServletRequest()
+        req.setMethod(httpMethod.name())
+        req.setScheme("https")
+        req.setServletPath(resourceType.getValue())
+        req.setSecure(true)
+        return req
+    }
+
+    static private HttpServletResponse createResponse() {
+        HttpServletResponse res = new MockHttpServletResponse()
+        return res
+    }
+
+    static private void allowAccess() {
+        // Do nothing (no thrown exception indicates access is allowed
+    }
+
+    static private void denyAccess() {
+        throw new AccessDeniedException("This is an expected AccessDeniedException.")
+    }
+
+}
diff --git a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
index 0ffdb0d..29cd215 100644
--- a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
+++ b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
@@ -85,6 +85,7 @@
 
         // Given: an empty registry returns these resources
         String expected = "[" +
+                "{\"identifier\":\"/actuator\",\"name\":\"Actuator\"}," +
                 "{\"identifier\":\"/policies\",\"name\":\"Access Policies\"}," +
                 "{\"identifier\":\"/tenants\",\"name\":\"Tenants\"}," +
                 "{\"identifier\":\"/proxy\",\"name\":\"Proxy User Requests\"}," +