This is an automated email from the ASF dual-hosted git repository. kdoran pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nifi-registry.git
The following commit(s) were added to refs/heads/master by this push: new 4825565 NIFIREG-358 Refactoring proxy authorization to be part of Authorizables 4825565 is described below commit 4825565e48b935e07c3b6039b16e3144f150fa20 Author: Bryan Bende <bbe...@apache.org> AuthorDate: Fri Feb 7 12:22:37 2020 -0500 NIFIREG-358 Refactoring proxy authorization to be part of Authorizables NIFIREG-358 Catching UntrustedProxyException when asking for authorized resources since it would be considered unauthorized This closes #258. Signed-off-by: Kevin Doran <kdo...@apache.org> --- .../security/authorization/AuthorizerFactory.java | 12 - .../authorization/FrameworkAuthorizer.java | 189 ---------- .../authorization/FrameworkManagedAuthorizer.java | 54 --- .../authorization/StandardAuthorizableLookup.java | 78 +++- .../authorization/UntrustedProxyException.java | 29 ++ .../authorization/resource/Authorizable.java | 14 - .../resource/ProxyChainAuthorizable.java | 145 ++++++++ .../resource/PublicCheckingAuthorizable.java | 107 ++++++ .../registry/service/AuthorizationService.java | 3 +- .../service/AuthorizationServiceSpec.groovy | 3 +- .../authorization/TestFrameworkAuthorizer.java | 278 -------------- .../TestStandardAuthorizableLookup.java | 404 +++++++++++++++++++++ .../authorization/AuthorizationRequest.java | 5 + .../web/mapper/UntrustedProxyExceptionMapper.java | 48 +++ .../ResourceAuthorizationFilterSpec.groovy | 4 +- 15 files changed, 814 insertions(+), 559 deletions(-) diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java index 959e29e..f69ac3c 100644 --- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java @@ -248,12 +248,8 @@ public class AuthorizerFactory implements UserGroupProviderLookup, AccessPolicyP try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(authorizerClassLoader)) { authorizer.onConfigured(authorizerConfigurationContext); } - - // wrap the integrity checked Authorizer with the FrameworkAuthorizer - authorizer = createFrameworkAuthorizer(authorizer); } - } catch (AuthorizerFactoryException e) { throw e; } catch (Exception e) { @@ -427,14 +423,6 @@ public class AuthorizerFactory implements UserGroupProviderLookup, AccessPolicyP return instance; } - private Authorizer createFrameworkAuthorizer(final Authorizer baseAuthorizer) { - if (baseAuthorizer instanceof ManagedAuthorizer) { - return new FrameworkManagedAuthorizer((ManagedAuthorizer) baseAuthorizer, registryService); - } else { - return new FrameworkAuthorizer(baseAuthorizer, registryService); - } - } - private void performMethodInjection(final Object instance, final Class authorizerClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { for (final Method method : authorizerClass.getMethods()) { if (method.isAnnotationPresent(AuthorizerContext.class)) { diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkAuthorizer.java deleted file mode 100644 index 08fb8f0..0000000 --- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkAuthorizer.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.nifi.registry.security.authorization; - -import org.apache.nifi.registry.bucket.Bucket; -import org.apache.nifi.registry.exception.ResourceNotFoundException; -import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; -import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; -import org.apache.nifi.registry.security.authorization.resource.Authorizable; -import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; -import org.apache.nifi.registry.security.authorization.resource.ResourceType; -import org.apache.nifi.registry.security.authorization.user.NiFiUser; -import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; -import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; -import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; -import org.apache.nifi.registry.service.RegistryService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.Objects; - -/** - * Wraps an Authorizer and adds framework level logic for authorizing proxies, public resources, and anything else - * that needs to be done on top of the regular Authorizer. - */ -public class FrameworkAuthorizer implements Authorizer { - - public static Logger LOGGER = LoggerFactory.getLogger(FrameworkAuthorizer.class); - - private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { - @Override - public Authorizable getParentAuthorizable() { - return null; - } - - @Override - public Resource getResource() { - return ResourceFactory.getProxyResource(); - } - }; - - private final Authorizer wrappedAuthorizer; - private final RegistryService registryService; - - public FrameworkAuthorizer(final Authorizer wrappedAuthorizer, final RegistryService registryService) { - this.wrappedAuthorizer = Objects.requireNonNull(wrappedAuthorizer); - this.registryService = Objects.requireNonNull(registryService); - } - - @Override - public void initialize(final AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException { - wrappedAuthorizer.initialize(initializationContext); - } - - @Override - public void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { - wrappedAuthorizer.onConfigured(configurationContext); - } - - @Override - public AuthorizationResult authorize(final AuthorizationRequest request) throws AuthorizationAccessException { - final Resource resource = request.getResource(); - final RequestAction requestAction = request.getAction(); - - /** - * If the request is for a resource that has been made public and action is READ, then it should automatically be authorized. - * - * This needs to be checked before the proxy authorizations b/c access to a public resource should always be allowed. - */ - - final boolean allowPublicAccess = isPublicAccessAllowed(resource, requestAction); - if (allowPublicAccess) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Authorizing access to public resource '{}'", new Object[]{resource.getIdentifier()}); - } - return AuthorizationResult.approved(); - } - - /** - * Deny an anonymous user access to anything else, they should only have access to publicly readable resources checked above - */ - - if (request.isAnonymous()) { - return AuthorizationResult.denied("Anonymous access is not authorized"); - } - - /* - * If the request has a proxy chain, ensure each identity in the chain is an authorized proxy for the given action. - * - * The action comes from the original request. For example, if user1 is proxied by proxy1, and it is a WRITE request - * to /buckets/12345, then we need to determine if proxy1 is authorized to proxy WRITE requests. - */ - - final List<String> proxyChainIdentities = request.getProxyIdentities(); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Found {} proxy identities", new Object[]{proxyChainIdentities.size()}); - } - - for (final String proxyIdentity : proxyChainIdentities) { - final NiFiUser proxyNiFiUser = createProxyNiFiUser(proxyIdentity); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Authorizing proxy [{}] for {}", new Object[]{proxyIdentity, requestAction}); - } - - try { - PROXY_AUTHORIZABLE.authorize(wrappedAuthorizer, requestAction, proxyNiFiUser); - } catch (final AccessDeniedException e) { - final String actionString = requestAction.toString(); - return AuthorizationResult.denied(String.format("Untrusted proxy [%s] for %s operation.", proxyIdentity, actionString)); - } - } - - /** - * All other authorization decisions need to be delegated to the original wrapped Authorizer. - */ - - return wrappedAuthorizer.authorize(request); - } - - /** - * Determines if the given Resource is considered public for the action being performed. - * - * @param resource a Resource being authorized - * @param action the action being performed - * @return true if the resource is public for the given action, false otherwise - */ - private boolean isPublicAccessAllowed(final Resource resource, final RequestAction action) { - if (resource == null || action == null) { - return false; - } - - final String resourceIdentifier = resource.getIdentifier(); - if (resourceIdentifier == null || !resourceIdentifier.startsWith(ResourceType.Bucket.getValue() + "/")) { - return false; - } - - final int lastSlashIndex = resourceIdentifier.lastIndexOf("/"); - if (lastSlashIndex < 0 || lastSlashIndex >= resourceIdentifier.length() - 1) { - return false; - } - - final String bucketId = resourceIdentifier.substring(lastSlashIndex + 1); - try { - final Bucket bucket = registryService.getBucket(bucketId); - return bucket.isAllowPublicRead() && action == RequestAction.READ; - } catch (ResourceNotFoundException rnfe) { - // if not found then we can't determine public access, so return false to delegate to regular authorizer - LOGGER.debug("Cannot determine public access, bucket not found with id [{}]", new Object[]{bucketId}); - return false; - } catch (Exception e) { - LOGGER.error("Error checking public access to bucket with id [{}]", new Object[]{bucketId}, e); - return false; - } - } - - /** - * Creates a NiFiUser for the given proxy identity. - * - * This is only intended to be used for authorizing the given proxy identity against the /proxy resource, so we - * don't need to populate the rest of the info on this user. - * - * @param proxyIdentity the proxy identity - * @return the NiFiUser - */ - private NiFiUser createProxyNiFiUser(final String proxyIdentity) { - return new StandardNiFiUser.Builder().identity(proxyIdentity).build(); - } - - @Override - public void preDestruction() throws SecurityProviderDestructionException { - wrappedAuthorizer.preDestruction(); - } - -} diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkManagedAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkManagedAuthorizer.java deleted file mode 100644 index 478482e..0000000 --- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/FrameworkManagedAuthorizer.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.nifi.registry.security.authorization; - -import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; -import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; -import org.apache.nifi.registry.service.RegistryService; - -/** - * Similar to FrameworkAuthorizer, but specifically for wrapping a ManagedAuthorizer. - */ -public class FrameworkManagedAuthorizer extends FrameworkAuthorizer implements ManagedAuthorizer { - - private final ManagedAuthorizer wrappedManagedAuthorizer; - - public FrameworkManagedAuthorizer(final ManagedAuthorizer wrappedManagedAuthorizer, final RegistryService registryService) { - super(wrappedManagedAuthorizer, registryService); - this.wrappedManagedAuthorizer = wrappedManagedAuthorizer; - } - - @Override - public String getFingerprint() throws AuthorizationAccessException { - return wrappedManagedAuthorizer.getFingerprint(); - } - - @Override - public void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException { - wrappedManagedAuthorizer.inheritFingerprint(fingerprint); - } - - @Override - public void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { - wrappedManagedAuthorizer.checkInheritability(proposedFingerprint); - } - - @Override - public AccessPolicyProvider getAccessPolicyProvider() { - return wrappedManagedAuthorizer.getAccessPolicyProvider(); - } -} diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java index 18c2a52..6f68ebe 100644 --- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java @@ -17,15 +17,22 @@ package org.apache.nifi.registry.security.authorization; import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.Bucket; import org.apache.nifi.registry.exception.ResourceNotFoundException; import org.apache.nifi.registry.security.authorization.resource.Authorizable; import org.apache.nifi.registry.security.authorization.resource.InheritingAuthorizable; +import org.apache.nifi.registry.security.authorization.resource.ProxyChainAuthorizable; +import org.apache.nifi.registry.security.authorization.resource.PublicCheckingAuthorizable; import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; import org.apache.nifi.registry.security.authorization.resource.ResourceType; +import org.apache.nifi.registry.service.RegistryService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.Objects; + @Component public class StandardAuthorizableLookup implements AuthorizableLookup { @@ -103,14 +110,21 @@ public class StandardAuthorizableLookup implements AuthorizableLookup { } }; + private final RegistryService registryService; + + @Autowired + public StandardAuthorizableLookup(final RegistryService registryService) { + this.registryService = Objects.requireNonNull(registryService); + } + @Override public Authorizable getActuatorAuthorizable() { - return ACTUATOR_AUTHORIZABLE; + return new ProxyChainAuthorizable(ACTUATOR_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); } @Override public Authorizable getSwaggerAuthorizable() { - return SWAGGER_AUTHORIZABLE; + return new ProxyChainAuthorizable(SWAGGER_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); } @Override @@ -120,34 +134,42 @@ public class StandardAuthorizableLookup implements AuthorizableLookup { @Override public Authorizable getTenantsAuthorizable() { - return TENANTS_AUTHORIZABLE; + return new ProxyChainAuthorizable(TENANTS_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); } @Override public Authorizable getPoliciesAuthorizable() { - return POLICIES_AUTHORIZABLE; + return new ProxyChainAuthorizable(POLICIES_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); } @Override public Authorizable getBucketsAuthorizable() { - return BUCKETS_AUTHORIZABLE; + return new ProxyChainAuthorizable(BUCKETS_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); } @Override public Authorizable getBucketAuthorizable(String bucketIdentifier) { - // Note - this returns a special Authorizable type that inherits permissions from the parent Authorizable - return new InheritingAuthorizable() { + // Note - this creates a special Authorizable type that inherits permissions from the parent Authorizable + final Authorizable inheritingAuthorizable = new InheritingAuthorizable() { @Override public Authorizable getParentAuthorizable() { - return getBucketsAuthorizable(); + // Use the unwrapped buckets authorizable here so that we don't reauthorize the proxy chain + return BUCKETS_AUTHORIZABLE; } @Override public Resource getResource() { return ResourceFactory.getBucketResource(bucketIdentifier, "Bucket with ID " + bucketIdentifier); } + }; + + // Wrap the inheriting Authorizable with logic that first checks if public access is allowed, if not then delegates to the inheriting Authorizable + final Authorizable publicCheckingAuthorizable = new PublicCheckingAuthorizable(inheritingAuthorizable, this::isPublicAccessAllowed); + + // Return ProxyChainAuthorizable -> public checking Authorizable -> inheriting Authorizable + return new ProxyChainAuthorizable(publicCheckingAuthorizable, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); } @Override @@ -217,4 +239,44 @@ public class StandardAuthorizableLookup implements AuthorizableLookup { return authorizable; } + /** + * Determines if the given Resource is considered public for the action being performed. + * + * @param resource a Resource being authorized + * @param action the action being performed + * @return true if the resource is public for the given action, false otherwise + */ + private boolean isPublicAccessAllowed(final Resource resource, final RequestAction action) { + if (resource == null || action == null) { + return false; + } + + if (action != RequestAction.READ) { + return false; + } + + final String resourceIdentifier = resource.getIdentifier(); + if (resourceIdentifier == null || !resourceIdentifier.startsWith(ResourceType.Bucket.getValue() + "/")) { + return false; + } + + final int lastSlashIndex = resourceIdentifier.lastIndexOf("/"); + if (lastSlashIndex < 0 || lastSlashIndex >= resourceIdentifier.length() - 1) { + return false; + } + + final String bucketId = resourceIdentifier.substring(lastSlashIndex + 1); + try { + final Bucket bucket = registryService.getBucket(bucketId); + return bucket.isAllowPublicRead(); + } catch (ResourceNotFoundException rnfe) { + // if not found then we can't determine public access, so return false to delegate to regular authorizer + logger.debug("Cannot determine public access, bucket not found with id [{}]", new Object[]{bucketId}); + return false; + } catch (Exception e) { + logger.error("Error checking public access to bucket with id [{}]", new Object[]{bucketId}, e); + return false; + } + } + } diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java new file mode 100644 index 0000000..fbf1580 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authorization; + +public class UntrustedProxyException extends RuntimeException { + + public UntrustedProxyException(String message) { + super(message); + } + + public UntrustedProxyException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java index 04cb469..c461965 100644 --- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java @@ -27,9 +27,7 @@ import org.apache.nifi.registry.security.authorization.UserContextKeys; import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; import org.apache.nifi.registry.security.authorization.user.NiFiUser; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; public interface Authorizable { @@ -95,10 +93,6 @@ public interface Authorizable { userContext = null; } - // Note: We don't include the proxy identities here since this is not a direct attempt to access the resource and - // we just want to determine if the end user is authorized. The proxy identities will be authorized when calling - // Authorizable.authorize() during a direct access attempt for a resource. - final Resource resource = getResource(); final Resource requestedResource = getRequestedResource(); final AuthorizationRequest request = new AuthorizationRequest.Builder() @@ -211,18 +205,10 @@ public interface Authorizable { userContext = null; } - final List<String> proxyChain = new ArrayList<>(); - NiFiUser proxyUser = user.getChain(); - while (proxyUser != null) { - proxyChain.add(proxyUser.getIdentity()); - proxyUser = proxyUser.getChain(); - } - final Resource resource = getResource(); final Resource requestedResource = getRequestedResource(); final AuthorizationRequest request = new AuthorizationRequest.Builder() .identity(user.getIdentity()) - .proxyIdentities(proxyChain) .groups(user.getGroups()) .anonymous(user.isAnonymous()) .accessAttempt(true) diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java new file mode 100644 index 0000000..aec8d76 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authorization.resource; + +import org.apache.nifi.registry.security.authorization.AuthorizationResult; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; +import org.apache.nifi.registry.security.authorization.UntrustedProxyException; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * Authorizable that wraps another Authorizable and applies logic for authorizing the proxy chain, unless the resource + * allows public access, which then skips authorizing the proxy chain. + */ +public class ProxyChainAuthorizable implements Authorizable { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProxyChainAuthorizable.class); + + private final Authorizable wrappedAuthorizable; + private final Authorizable proxyAuthorizable; + private final BiFunction<Resource,RequestAction,Boolean> publicResourceCheck; + + public ProxyChainAuthorizable(final Authorizable wrappedAuthorizable, + final Authorizable proxyAuthorizable, + final BiFunction<Resource,RequestAction,Boolean> publicResourceCheck) { + this.wrappedAuthorizable = Objects.requireNonNull(wrappedAuthorizable); + this.proxyAuthorizable = Objects.requireNonNull(proxyAuthorizable); + this.publicResourceCheck = Objects.requireNonNull(publicResourceCheck); + } + + @Override + public Authorizable getParentAuthorizable() { + if (wrappedAuthorizable.getParentAuthorizable() == null) { + return null; + } else { + final Authorizable parentAuthorizable = wrappedAuthorizable.getParentAuthorizable(); + return new ProxyChainAuthorizable(parentAuthorizable, proxyAuthorizable, publicResourceCheck); + } + } + + @Override + public Resource getResource() { + return wrappedAuthorizable.getResource(); + } + + @Override + public AuthorizationResult checkAuthorization(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map<String, String> resourceContext) { + final Resource requestResource = wrappedAuthorizable.getRequestedResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{requestResource.getIdentifier()}); + } + + // if public access is allowed then we want to skip proxy authorization so just return + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(requestResource, action); + if (isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Proxy chain will not be checked, public access is allowed for {} on {}", + new Object[]{action.toString(), requestResource.getIdentifier()}); + } + return AuthorizationResult.approved(); + } + + // otherwise public access is not allowed so check the proxy chain for the given action + NiFiUser proxyUser = user.getChain(); + while (proxyUser != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Checking proxy [{}] for {}", new Object[]{proxyUser, action}); + } + + // if the proxy is denied then break out of the loop and return a denied result + final AuthorizationResult proxyAuthorizationResult = proxyAuthorizable.checkAuthorization(authorizer, action, proxyUser); + if (proxyAuthorizationResult.getResult() == AuthorizationResult.Result.Denied) { + final String deniedMessage = String.format("Untrusted proxy [%s] for %s operation.", proxyUser.getIdentity(), action.toString()); + return AuthorizationResult.denied(deniedMessage); + } + + proxyUser = proxyUser.getChain(); + } + + // at this point the proxy chain was approved so continue to check the original Authorizable + return wrappedAuthorizable.checkAuthorization(authorizer, action, user, resourceContext); + } + + @Override + public void authorize(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map<String, String> resourceContext) throws AccessDeniedException { + final Resource requestResource = wrappedAuthorizable.getRequestedResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{requestResource.getIdentifier()}); + } + + // if public access is allowed then we want to skip proxy authorization so just return + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(requestResource, action); + if (isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Proxy chain will not be authorized, public access is allowed for {} on {}", + new Object[]{action.toString(), requestResource.getIdentifier()}); + } + return; + } + + // otherwise public access is not allowed so authorize proxy chain for the given action + NiFiUser proxyUser = user.getChain(); + while (proxyUser != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Authorizing proxy [{}] for {}", new Object[]{proxyUser, action}); + } + + try { + proxyAuthorizable.authorize(authorizer, action, proxyUser); + } catch (final AccessDeniedException e) { + final String actionString = action.toString(); + throw new UntrustedProxyException(String.format("Untrusted proxy [%s] for %s operation.", proxyUser.getIdentity(), actionString)); + } + proxyUser = proxyUser.getChain(); + } + + // at this point the proxy chain was authorized so continue to authorize the original Authorizable + wrappedAuthorizable.authorize(authorizer, action, user, resourceContext); + } + +} diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java new file mode 100644 index 0000000..7cacb59 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authorization.resource; + +import org.apache.nifi.registry.security.authorization.AuthorizationResult; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * Authorizable that first checks if public access is allowed for the resource and action. If it is then it short-circuits + * and returns approved, otherwise it continues and delegates to the wrapped Authorizable. + */ +public class PublicCheckingAuthorizable implements Authorizable { + + private static final Logger LOGGER = LoggerFactory.getLogger(PublicCheckingAuthorizable.class); + + private final Authorizable wrappedAuthorizable; + private final BiFunction<Resource, RequestAction,Boolean> publicResourceCheck; + + public PublicCheckingAuthorizable(final Authorizable wrappedAuthorizable, + final BiFunction<Resource,RequestAction,Boolean> publicResourceCheck) { + this.wrappedAuthorizable = Objects.requireNonNull(wrappedAuthorizable); + this.publicResourceCheck = Objects.requireNonNull(publicResourceCheck); + } + + @Override + public Authorizable getParentAuthorizable() { + return wrappedAuthorizable.getParentAuthorizable(); + } + + @Override + public Resource getResource() { + return wrappedAuthorizable.getResource(); + } + + @Override + public AuthorizationResult checkAuthorization(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map<String, String> resourceContext) { + final Resource resource = getResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{resource.getIdentifier()}); + } + + // if public access is allowed then return approved + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(resource, action); + if(isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Public access is allowed for {}", new Object[]{resource.getIdentifier()}); + } + return AuthorizationResult.approved(); + } + + // otherwise delegate to the original inheriting authorizable + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Delegating to inheriting authorizable for {}", new Object[]{resource.getIdentifier()}); + } + return wrappedAuthorizable.checkAuthorization(authorizer, action, user, resourceContext); + } + + @Override + public void authorize(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map<String, String> resourceContext) throws AccessDeniedException { + final Resource resource = getResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{resource.getIdentifier()}); + } + + // if public access is allowed then skip authorization and return + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(resource, action); + if(isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Public access is allowed for {}", new Object[]{resource.getIdentifier()}); + } + return; + } + + // otherwise delegate to the original authorizable + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Delegating to inheriting authorizable for {}", new Object[]{resource.getIdentifier()}); + } + + wrappedAuthorizable.authorize(authorizer, action, user, resourceContext); + } +} diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java index 12f39b6..503f27c 100644 --- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java @@ -39,6 +39,7 @@ import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProv import org.apache.nifi.registry.security.authorization.Group; import org.apache.nifi.registry.security.authorization.ManagedAuthorizer; import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.UntrustedProxyException; import org.apache.nifi.registry.security.authorization.UserAndGroups; import org.apache.nifi.registry.security.authorization.UserGroupProvider; import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; @@ -487,7 +488,7 @@ public class AuthorizationService { .getAuthorizableByResource(resource.getIdentifier()) .authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser()); return true; - } catch (AccessDeniedException e) { + } catch (AccessDeniedException | UntrustedProxyException e) { return false; } }) diff --git a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy index 33b9f40..8035bd8 100644 --- a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy +++ b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy @@ -41,8 +41,7 @@ class AuthorizationServiceSpec extends Specification { def setup() { accessPolicyProvider.getUserGroupProvider() >> userGroupProvider def standardAuthorizer = new StandardManagedAuthorizer(accessPolicyProvider, userGroupProvider) - def frameworkAuthorizer = new FrameworkManagedAuthorizer(standardAuthorizer, registryService) - authorizationService = new AuthorizationService(authorizableLookup, frameworkAuthorizer, registryService) + authorizationService = new AuthorizationService(authorizableLookup, standardAuthorizer, registryService) } // ----- User tests ------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestFrameworkAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestFrameworkAuthorizer.java deleted file mode 100644 index 2cc03f8..0000000 --- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestFrameworkAuthorizer.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.nifi.registry.security.authorization; - -import org.apache.nifi.registry.bucket.Bucket; -import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; -import org.apache.nifi.registry.service.RegistryService; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentMatcher; - -import java.util.Arrays; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class TestFrameworkAuthorizer { - - private Authorizer frameworkAuthorizer; - private Authorizer wrappedAuthorizer; - private RegistryService registryService; - - private Bucket bucketPublic; - private Bucket bucketNotPublic; - - @Before - public void setup() { - wrappedAuthorizer = mock(Authorizer.class); - registryService = mock(RegistryService.class); - frameworkAuthorizer = new FrameworkAuthorizer(wrappedAuthorizer, registryService); - - bucketPublic = new Bucket(); - bucketPublic.setIdentifier(UUID.randomUUID().toString()); - bucketPublic.setName("Public Bucket"); - bucketPublic.setAllowPublicRead(true); - - bucketNotPublic = new Bucket(); - bucketNotPublic.setIdentifier(UUID.randomUUID().toString()); - bucketNotPublic.setName("Non Public Bucket"); - bucketNotPublic.setAllowPublicRead(false); - - when(registryService.getBucket(bucketPublic.getIdentifier())).thenReturn(bucketPublic); - when(registryService.getBucket(bucketNotPublic.getIdentifier())).thenReturn(bucketNotPublic); - } - - @Test - public void testReadPublicBucketWhenAnonymous() { - final Resource resource = ResourceFactory.getBucketResource(bucketPublic.getIdentifier(), bucketPublic.getName()); - - final AuthorizationRequest request = new AuthorizationRequest.Builder() - .resource(resource) - .requestedResource(resource) - .action(RequestAction.READ) - .accessAttempt(true) - .identity("anonymous") - .anonymous(true) - .build(); - - final AuthorizationResult result = frameworkAuthorizer.authorize(request); - assertNotNull(result); - assertEquals(AuthorizationResult.Result.Approved, result.getResult()); - - // should never make it to wrapped authorizer - verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class)); - } - - @Test - public void testReadNonPublicBucketWhenAnonymous() { - final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName()); - - final AuthorizationRequest request = new AuthorizationRequest.Builder() - .resource(resource) - .requestedResource(resource) - .action(RequestAction.READ) - .accessAttempt(true) - .identity("anonymous") - .anonymous(true) - .build(); - - final AuthorizationResult result = frameworkAuthorizer.authorize(request); - assertNotNull(result); - assertEquals(AuthorizationResult.Result.Denied, result.getResult()); - - // should be denied before making it to the wrapped authorizer since the user is anonymous - verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class)); - } - - @Test - public void testWritePublicBucketWhenAnonymous() { - final Resource resource = ResourceFactory.getBucketResource(bucketPublic.getIdentifier(), bucketPublic.getName()); - - final AuthorizationRequest request = new AuthorizationRequest.Builder() - .resource(resource) - .requestedResource(resource) - .action(RequestAction.WRITE) - .accessAttempt(true) - .identity("anonymous") - .anonymous(true) - .build(); - - final AuthorizationResult result = frameworkAuthorizer.authorize(request); - assertNotNull(result); - assertEquals(AuthorizationResult.Result.Denied, result.getResult()); - - // should be denied before making it to wrapped authorizer since request is anonymous - verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class)); - } - - @Test - public void testReadPublicBucketWhenNotAnonymous() { - final Resource resource = ResourceFactory.getBucketResource(bucketPublic.getIdentifier(), bucketPublic.getName()); - - final AuthorizationRequest request = new AuthorizationRequest.Builder() - .resource(resource) - .requestedResource(resource) - .action(RequestAction.READ) - .accessAttempt(true) - .identity("user1") - .anonymous(false) - .proxyIdentities(Arrays.asList("proxy1", "proxy2")) - .build(); - - final AuthorizationResult result = frameworkAuthorizer.authorize(request); - assertNotNull(result); - assertEquals(AuthorizationResult.Result.Approved, result.getResult()); - - // should never make it to wrapped authorizer - verify(wrappedAuthorizer, times(0)).authorize(any(AuthorizationRequest.class)); - } - - @Test - public void testReadNonPublicBucketWhenNotAnonymousAndAuthorizedProxies() { - final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName()); - - final AuthorizationRequest request = new AuthorizationRequest.Builder() - .resource(resource) - .requestedResource(resource) - .action(RequestAction.READ) - .accessAttempt(true) - .identity("user1") - .anonymous(false) - .proxyIdentities(Arrays.asList("proxy1", "proxy2")) - .build(); - - // since the bucket is not public it will fall through to the wrapped authorizer - when(wrappedAuthorizer.authorize(any(AuthorizationRequest.class))) - .thenReturn(AuthorizationResult.approved()); - - final AuthorizationResult result = frameworkAuthorizer.authorize(request); - assertNotNull(result); - assertEquals(AuthorizationResult.Result.Approved, result.getResult()); - - // should make 3 calls to the wrapped authorizer to authorize user1, proxy1, proxy2 - verify(wrappedAuthorizer, times(3)).authorize(any(AuthorizationRequest.class)); - } - - @Test - public void testReadNonPublicBucketWhenNotAnonymousAndUnauthorizedProxy() { - final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName()); - - final AuthorizationRequest request = new AuthorizationRequest.Builder() - .resource(resource) - .requestedResource(resource) - .action(RequestAction.READ) - .accessAttempt(true) - .identity("user1") - .anonymous(false) - .proxyIdentities(Arrays.asList("proxy1", "proxy2")) - .build(); - - // since the bucket is not public and the user is not anonymous, it will continue to proxy authorization - - // simulate the first proxy being authorized for READ actions - final AuthorizationRequestMatcher proxy1Matcher = new AuthorizationRequestMatcher( - "proxy1", ResourceFactory.getProxyResource(), request.getAction()); - when(wrappedAuthorizer.authorize(argThat(proxy1Matcher))).thenReturn(AuthorizationResult.approved()); - - // simulate the second proxy being unauthorized for READ actions - final AuthorizationRequestMatcher proxy2Matcher = new AuthorizationRequestMatcher( - "proxy2", ResourceFactory.getProxyResource(), request.getAction()); - when(wrappedAuthorizer.authorize(argThat(proxy2Matcher))).thenReturn(AuthorizationResult.denied("denied")); - - final AuthorizationResult result = frameworkAuthorizer.authorize(request); - assertNotNull(result); - assertEquals(AuthorizationResult.Result.Denied, result.getResult()); - - // should make 2 calls to the wrapped authorizer for the two proxies - verify(wrappedAuthorizer, times(2)).authorize(any(AuthorizationRequest.class)); - } - - @Test - public void testReadNonPublicBucketWhenNotAnonymousAndUnauthorizedEndUser() { - final Resource resource = ResourceFactory.getBucketResource(bucketNotPublic.getIdentifier(), bucketNotPublic.getName()); - - final AuthorizationRequest request = new AuthorizationRequest.Builder() - .resource(resource) - .requestedResource(resource) - .action(RequestAction.READ) - .accessAttempt(true) - .identity("user1") - .anonymous(false) - .proxyIdentities(Arrays.asList("proxy1", "proxy2")) - .build(); - - // since the bucket is not public and the user is not anonymous, it will continue to proxy authorization - - // simulate the first proxy being authorized for READ actions - final AuthorizationRequestMatcher proxy1Matcher = new AuthorizationRequestMatcher( - "proxy1", ResourceFactory.getProxyResource(), request.getAction()); - when(wrappedAuthorizer.authorize(argThat(proxy1Matcher))).thenReturn(AuthorizationResult.approved()); - - // simulate the second proxy being authorized for READ actions - final AuthorizationRequestMatcher proxy2Matcher = new AuthorizationRequestMatcher( - "proxy2", ResourceFactory.getProxyResource(), request.getAction()); - when(wrappedAuthorizer.authorize(argThat(proxy2Matcher))).thenReturn(AuthorizationResult.approved()); - - // simulate the end user being unauthorized for READ actions - final AuthorizationRequestMatcher user1Matcher = new AuthorizationRequestMatcher( - "user1", resource, request.getAction()); - when(wrappedAuthorizer.authorize(argThat(user1Matcher))).thenReturn(AuthorizationResult.denied("denied")); - - final AuthorizationResult result = frameworkAuthorizer.authorize(request); - assertNotNull(result); - assertEquals(AuthorizationResult.Result.Denied, result.getResult()); - - // should make 3 calls to the wrapped authorizer for the two proxies and end user - verify(wrappedAuthorizer, times(3)).authorize(any(AuthorizationRequest.class)); - } - - - /** - * Matcher for matching Authorization requests. - */ - private static class AuthorizationRequestMatcher implements ArgumentMatcher<AuthorizationRequest> { - - private final String identity; - private final Resource resource; - private final RequestAction action; - - public AuthorizationRequestMatcher(final String identity, final Resource resource, final RequestAction action) { - this.identity = identity; - this.resource = resource; - this.action = action; - } - - @Override - public boolean matches(final AuthorizationRequest request) { - if (request == null) { - return false; - } - - return identity.equals(request.getIdentity()) - && resource.getIdentifier().equals(request.getResource().getIdentifier()) - && action == request.getAction(); - } - } -} diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java new file mode 100644 index 0000000..2804ac7 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; +import org.apache.nifi.registry.service.RegistryService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TestStandardAuthorizableLookup { + + private static final NiFiUser USER_NO_PROXY_CHAIN = new StandardNiFiUser.Builder() + .identity("user1") + .build(); + + private static final NiFiUser USER_WITH_PROXY_CHAIN = new StandardNiFiUser.Builder() + .identity("user1") + .chain(new StandardNiFiUser.Builder().identity("CN=localhost, OU=NIFI").build()) + .build(); + + private Authorizer authorizer; + private RegistryService registryService; + private AuthorizableLookup authorizableLookup; + + private Bucket bucketPublic; + private Bucket bucketNotPublic; + + @Before + public void setup() { + authorizer = mock(Authorizer.class); + registryService = mock(RegistryService.class); + authorizableLookup = new StandardAuthorizableLookup(registryService); + + bucketPublic = new Bucket(); + bucketPublic.setIdentifier(UUID.randomUUID().toString()); + bucketPublic.setName("Public Bucket"); + bucketPublic.setAllowPublicRead(true); + + bucketNotPublic = new Bucket(); + bucketNotPublic.setIdentifier(UUID.randomUUID().toString()); + bucketNotPublic.setName("Non Public Bucket"); + bucketNotPublic.setAllowPublicRead(false); + + when(registryService.getBucket(bucketPublic.getIdentifier())).thenReturn(bucketPublic); + when(registryService.getBucket(bucketNotPublic.getIdentifier())).thenReturn(bucketNotPublic); + } + + // Test check method for Bucket Authorizable + + @Test + public void testCheckReadPublicBucketWithNoProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, RequestAction.READ, USER_NO_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Approved, result.getResult()); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckReadPublicBucketWithProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, RequestAction.READ, USER_WITH_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Approved, result.getResult()); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithUnauthorizedUserAndNoProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // second request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // should reach authorizer and return denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, USER_NO_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Denied, result.getResult()); + + // Should call authorizer twice for specific bucket and top-level /buckets + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithUnauthorizedProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, USER_WITH_PROXY_CHAIN.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should return denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, USER_WITH_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Denied, result.getResult()); + + // Should never call authorizer once for /proxy and then return denied + verify(authorizer, times(1)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithUnauthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // third request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should return denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, user); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Denied, result.getResult()); + + // Should call authorizer three time for /proxy, /bucket/{id}, and /buckets + verify(authorizer, times(3)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithAuthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // the authorization should all return approved + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, user); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Approved, result.getResult()); + + // Should call authorizer two times for /proxy and /bucket/{id} + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + + // Test authorize method for Bucket Authorizable + + @Test + public void testAuthorizeReadPublicBucketWithNoProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + bucketAuthorizable.authorize(authorizer, RequestAction.READ, USER_NO_PROXY_CHAIN); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testAuthorizeReadPublicBucketWithProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + bucketAuthorizable.authorize(authorizer, RequestAction.READ, USER_WITH_PROXY_CHAIN); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testAuthorizeWritePublicBucketWithUnauthorizedUserAndNoProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // second request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // should reach authorizer and throw access denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + try { + bucketAuthorizable.authorize(authorizer, action, USER_NO_PROXY_CHAIN); + Assert.fail("Should have thrown exception"); + } catch (AccessDeniedException e) { + // Should never call authorizer twice for specific bucket and top-level /buckets + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + } + + @Test + public void testAuthorizeWritePublicBucketWithUnauthorizedProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, USER_WITH_PROXY_CHAIN.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should throw UntrustedProxyException + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + try { + bucketAuthorizable.authorize(authorizer, action, USER_WITH_PROXY_CHAIN); + Assert.fail("Should have thrown exception"); + } catch (UntrustedProxyException e) { + // Should call authorizer once for /proxy and then throw exception + verify(authorizer, times(1)).authorize(any(AuthorizationRequest.class)); + } + } + + @Test + public void testAuthorizeWritePublicBucketWithUnauthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // third request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should throw UntrustedProxyException + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + try { + bucketAuthorizable.authorize(authorizer, action, user); + Assert.fail("Should have thrown exception"); + } catch (AccessDeniedException e) { + // Should call authorizer three times for /proxy, /bucket/{id}, and /buckets + verify(authorizer, times(3)).authorize(any(AuthorizationRequest.class)); + } + } + + @Test + public void testAuthorizeWritePublicBucketWithAuthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // the authorization should all return approved so no exception + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + bucketAuthorizable.authorize(authorizer, action, user); + + // Should call authorizer two times for /proxy and /bucket/{id} + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + + private AuthorizationRequest getBucketAuthorizationRequest(final String bucketIdentifier, final RequestAction action, final NiFiUser user) { + return new AuthorizationRequest.Builder() + .resource(ResourceFactory.getBucketResource(bucketIdentifier, bucketIdentifier)) + .action(action) + .identity(user.getIdentity()) + .accessAttempt(true) + .anonymous(false) + .build(); + } + + private AuthorizationRequest getBucketsAuthorizationRequest(final RequestAction action, final NiFiUser user) { + return new AuthorizationRequest.Builder() + .resource(ResourceFactory.getBucketsResource()) + .action(action) + .identity(user.getIdentity()) + .accessAttempt(true) + .anonymous(false) + .build(); + } + + private AuthorizationRequest getProxyAuthorizationRequest(final RequestAction action, final NiFiUser user) { + return new AuthorizationRequest.Builder() + .resource(ResourceFactory.getProxyResource()) + .action(action) + .identity(user.getIdentity()) + .accessAttempt(true) + .anonymous(false) + .build(); + } + + /** + * ArugmentMatcher for AuthorizationRequest. + */ + private static class AuthorizationRequestMatcher implements ArgumentMatcher<AuthorizationRequest> { + + private final AuthorizationRequest expectedAuthorizationRequest; + + public AuthorizationRequestMatcher(final AuthorizationRequest expectedAuthorizationRequest) { + this.expectedAuthorizationRequest = expectedAuthorizationRequest; + } + + @Override + public boolean matches(final AuthorizationRequest authorizationRequest) { + if (authorizationRequest == null) { + return false; + } + + final String requestResourceId = authorizationRequest.getResource().getIdentifier(); + final String expectedResourceId = expectedAuthorizationRequest.getResource().getIdentifier(); + + final String requestAction = authorizationRequest.getAction().toString(); + final String expectedAction = expectedAuthorizationRequest.getAction().toString(); + + final String requestUserIdentity = authorizationRequest.getIdentity(); + final String expectedUserIdentity = authorizationRequest.getIdentity(); + + return requestResourceId.equals(expectedResourceId) + && requestAction.equals(expectedAction) + && requestUserIdentity.equals(expectedUserIdentity); + } + } +} diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java index 56b7b45..3e832fa 100644 --- a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java +++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java @@ -109,6 +109,8 @@ public class AuthorizationRequest { * The identities in the proxy chain for the request. Will be empty if the request was not proxied. * * @return The identities in the proxy chain + * + * @deprecated no longer populated */ public List<String> getProxyIdentities() { return proxyIdentities; @@ -210,6 +212,9 @@ public class AuthorizationRequest { return this; } + /** + * @deprecated no longer populated by the framework + */ public Builder proxyIdentities(final List<String> proxyIdentities) { this.proxyIdentities = proxyIdentities; return this; diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java new file mode 100644 index 0000000..453dbd5 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.UntrustedProxyException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps an UntrustedProxyException to a FORBIDDEN response. + */ +@Component +@Provider +public class UntrustedProxyExceptionMapper implements ExceptionMapper<UntrustedProxyException> { + + private static Logger LOGGER = LoggerFactory.getLogger(UntrustedProxyException.class); + + @Override + public Response toResponse(final UntrustedProxyException exception) { + LOGGER.info("{}. Returning {} response.", exception, Response.Status.FORBIDDEN); + LOGGER.debug(StringUtils.EMPTY, exception); + + return Response.status(Response.Status.FORBIDDEN) + .entity(exception.getMessage()) + .type("text/plain") + .build(); + } +} diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy b/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy index e27dfbe..806f73c 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy +++ b/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy @@ -20,6 +20,7 @@ import org.apache.nifi.registry.security.authorization.exception.AccessDeniedExc import org.apache.nifi.registry.security.authorization.resource.Authorizable import org.apache.nifi.registry.security.authorization.resource.ResourceType import org.apache.nifi.registry.service.AuthorizationService +import org.apache.nifi.registry.service.RegistryService import org.apache.nifi.registry.web.security.authorization.HttpMethodAuthorizationRules import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter import org.apache.nifi.registry.web.security.authorization.StandardHttpMethodAuthorizationRules @@ -34,7 +35,8 @@ import javax.servlet.http.HttpServletResponse class ResourceAuthorizationFilterSpec extends Specification { - AuthorizableLookup authorizableLookup = new StandardAuthorizableLookup() + RegistryService registryService = Mock(RegistryService) + AuthorizableLookup authorizableLookup = new StandardAuthorizableLookup(registryService) AuthorizationService mockAuthorizationService = Mock(AuthorizationService) FilterChain mockFilterChain = Mock(FilterChain) ResourceAuthorizationFilter.Builder resourceAuthorizationFilterBuilder