blob: 6e551e1ac5d1273228705d50bef3fcbbca0587ac [file] [log] [blame]
/*
* 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);
}
}
}