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\"}," +