From bd88e4335ad151592f1310996e9a0513b7f0829a Mon Sep 17 00:00:00 2001 From: Andy LoPresto Date: Wed, 1 Feb 2017 21:32:35 -0800 Subject: [PATCH] Refactored user identity parsing and proxied entity chain formatting. Added unit tests. Signed-off-by: Andy LoPresto --- .../nifi/web/NiFiWebRequestContext.java | 5 + .../resource/DataAuthorizable.java | 57 ++-- .../authorization/user/NiFiUserUtils.java | 17 +- .../authorization/user/StandardNiFiUser.java | 35 ++- .../user/NiFiUserUtilsTest.groovy | 111 +++++++ .../resource/DataAuthorizableTest.java | 160 ++++++++++ .../ThreadPoolRequestReplicator.java | 89 +++--- .../TestThreadPoolRequestReplicator.java | 111 ++++++- .../nifi/web/HttpServletRequestContext.java | 38 +-- .../nifi/web/StandardNiFiContentAccess.java | 24 +- .../StandardNiFiWebConfigurationContext.java | 32 +- .../nifi/web/ContentRequestContext.java | 5 + .../nifi/web/ContentViewerController.java | 4 +- .../web/security/ProxiedEntitiesUtils.java | 130 ++++++--- .../x509/X509AuthenticationProvider.java | 67 +++-- .../security/ProxiedEntitiesUtilsTest.groovy | 265 +++++++++++++++++ .../x509/X509AuthenticationProviderTest.java | 276 ++++++++++++++++++ 17 files changed, 1181 insertions(+), 245 deletions(-) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/groovy/org/apache/nifi/authorization/user/NiFiUserUtilsTest.groovy create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/java/org/apache/nifi/authorization/resource/DataAuthorizableTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/ProxiedEntitiesUtilsTest.groovy create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/x509/X509AuthenticationProviderTest.java diff --git a/nifi-framework-api/src/main/java/org/apache/nifi/web/NiFiWebRequestContext.java b/nifi-framework-api/src/main/java/org/apache/nifi/web/NiFiWebRequestContext.java index 9dd44abf50..bb3bbd3752 100644 --- a/nifi-framework-api/src/main/java/org/apache/nifi/web/NiFiWebRequestContext.java +++ b/nifi-framework-api/src/main/java/org/apache/nifi/web/NiFiWebRequestContext.java @@ -49,8 +49,13 @@ public interface NiFiWebRequestContext { * <CN=original-proxied-entity><CN=first-proxy><CN=second-proxy>... * * + * Update: + * This method has been deprecated since the entire proxy + * chain is able to be rebuilt using the current user if necessary. + * * @return the proxied entities chain or null if no chain */ + @Deprecated String getProxiedEntitiesChain(); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/DataAuthorizable.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/DataAuthorizable.java index 7269560d8d..012e09dcc7 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/DataAuthorizable.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/DataAuthorizable.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.authorization.resource; +import java.util.Map; import org.apache.nifi.authorization.AccessDeniedException; import org.apache.nifi.authorization.AuthorizationResult; import org.apache.nifi.authorization.AuthorizationResult.Result; @@ -23,13 +24,8 @@ import org.apache.nifi.authorization.Authorizer; import org.apache.nifi.authorization.RequestAction; import org.apache.nifi.authorization.Resource; import org.apache.nifi.authorization.user.NiFiUser; -import org.apache.nifi.authorization.user.NiFiUserUtils; -import org.apache.nifi.authorization.user.StandardNiFiUser; import org.apache.nifi.web.ResourceNotFoundException; -import java.util.List; -import java.util.Map; - /** * Authorizable for authorizing access to data. Data based authorizable requires authorization for the entire DN chain. */ @@ -67,28 +63,24 @@ public class DataAuthorizable implements Authorizable, EnforcePolicyPermissionsT AuthorizationResult result = null; - // calculate the dn chain - final List dnChain = NiFiUserUtils.buildProxiedEntitiesChain(user); - for (final String identity : dnChain) { + // authorize each element in the chain + NiFiUser chainedUser = user; + do { try { - final String clientAddress = user.getIdentity().equals(identity) ? user.getClientAddress() : null; - final NiFiUser chainUser = new StandardNiFiUser(identity, clientAddress) { - @Override - public boolean isAnonymous() { - // allow current user to drive anonymous flag as anonymous users are never chained... supports single user case - return user.isAnonymous(); - } - }; + // perform the current user authorization + result = Authorizable.super.checkAuthorization(authorizer, action, chainedUser, resourceContext); - result = Authorizable.super.checkAuthorization(authorizer, action, chainUser, resourceContext); + // if authorization is not approved, reject + if (!Result.Approved.equals(result.getResult())) { + return result; + } + + // go to the next user in the chain + chainedUser = chainedUser.getChain(); } catch (final ResourceNotFoundException e) { result = AuthorizationResult.denied("Unknown source component."); } - - if (!Result.Approved.equals(result.getResult())) { - break; - } - } + } while (chainedUser != null); if (result == null) { result = AuthorizationResult.denied(); @@ -103,23 +95,18 @@ public class DataAuthorizable implements Authorizable, EnforcePolicyPermissionsT throw new AccessDeniedException("Unknown user."); } - // calculate the dn chain - final List dnChain = NiFiUserUtils.buildProxiedEntitiesChain(user); - for (final String identity : dnChain) { + // authorize each element in the chain + NiFiUser chainedUser = user; + do { try { - final String clientAddress = user.getIdentity().equals(identity) ? user.getClientAddress() : null; - final NiFiUser chainUser = new StandardNiFiUser(identity, clientAddress) { - @Override - public boolean isAnonymous() { - // allow current user to drive anonymous flag as anonymous users are never chained... supports single user case - return user.isAnonymous(); - } - }; + // perform the current user authorization + Authorizable.super.authorize(authorizer, action, chainedUser, resourceContext); - Authorizable.super.authorize(authorizer, action, chainUser, resourceContext); + // go to the next user in the chain + chainedUser = chainedUser.getChain(); } catch (final ResourceNotFoundException e) { throw new AccessDeniedException("Unknown source component."); } - } + } while (chainedUser != null); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/NiFiUserUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/NiFiUserUtils.java index 6a4776abaf..93e070d993 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/NiFiUserUtils.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/NiFiUserUtils.java @@ -16,13 +16,13 @@ */ package org.apache.nifi.authorization.user; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import java.util.ArrayList; -import java.util.List; - /** * Utility methods for retrieving information about the current application user. * @@ -72,13 +72,18 @@ public final class NiFiUserUtils { // build the dn chain NiFiUser chainedUser = user; - do { + while (chainedUser != null) { // add the entry for this user - proxyChain.add(chainedUser.getIdentity()); + if (chainedUser.isAnonymous()) { + // use an empty string to represent an anonymous user in the proxy entities chain + proxyChain.add(StringUtils.EMPTY); + } else { + proxyChain.add(chainedUser.getIdentity()); + } // go to the next user in the chain chainedUser = chainedUser.getChain(); - } while (chainedUser != null); + } return proxyChain; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/StandardNiFiUser.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/StandardNiFiUser.java index 372d89f728..2a82795023 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/StandardNiFiUser.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/StandardNiFiUser.java @@ -23,30 +23,55 @@ import java.util.Objects; */ public class StandardNiFiUser implements NiFiUser { - public static final StandardNiFiUser ANONYMOUS = new StandardNiFiUser("anonymous"); + public static final String ANONYMOUS_IDENTITY = "anonymous"; + public static final StandardNiFiUser ANONYMOUS = new StandardNiFiUser(ANONYMOUS_IDENTITY, null, null, true); private final String identity; private final NiFiUser chain; private final String clientAddress; + private final boolean isAnonymous; public StandardNiFiUser(String identity) { - this(identity, null, null); + this(identity, null, null, false); } public StandardNiFiUser(String identity, String clientAddress) { - this(identity, null, clientAddress); + this(identity, null, clientAddress, false); } public StandardNiFiUser(String identity, NiFiUser chain) { - this(identity, chain, null); + this(identity, chain, null, false); } public StandardNiFiUser(String identity, NiFiUser chain, String clientAddress) { + this(identity, chain, clientAddress, false); + } + + /** + * This constructor is private as the only instance of this class which should have {@code isAnonymous} set to true is the singleton ANONYMOUS. + * + * @param identity the identity string for the user (i.e. "Andy" or "CN=alopresto, OU=Apache NiFi") + * @param chain the proxy chain that leads to this users + * @param clientAddress the source address of the request + * @param isAnonymous true to represent the canonical "anonymous" user + */ + private StandardNiFiUser(String identity, NiFiUser chain, String clientAddress, boolean isAnonymous) { this.identity = identity; this.chain = chain; this.clientAddress = clientAddress; + this.isAnonymous = isAnonymous; } + /** + * This static builder allows the chain and clientAddress to be populated without allowing calling code to provide a non-anonymous identity of the anonymous user. + * + * @param chain the proxied entities in {@see NiFiUser} form + * @param clientAddress the address the request originated from + * @return an anonymous user instance with the identity "anonymous" + */ + public static StandardNiFiUser populateAnonymousUser(NiFiUser chain, String clientAddress) { + return new StandardNiFiUser(ANONYMOUS_IDENTITY, chain, clientAddress, true); + } @Override public String getIdentity() { @@ -60,7 +85,7 @@ public class StandardNiFiUser implements NiFiUser { @Override public boolean isAnonymous() { - return this == ANONYMOUS; + return isAnonymous; } @Override diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/groovy/org/apache/nifi/authorization/user/NiFiUserUtilsTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/groovy/org/apache/nifi/authorization/user/NiFiUserUtilsTest.groovy new file mode 100644 index 0000000000..e76b1713e8 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/groovy/org/apache/nifi/authorization/user/NiFiUserUtilsTest.groovy @@ -0,0 +1,111 @@ +/* + * 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.authorization.user + +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@RunWith(JUnit4.class) +class NiFiUserUtilsTest { + private static final Logger logger = LoggerFactory.getLogger(NiFiUserUtilsTest.class) + + private static final String SAFE_USER_NAME_ANDY = "alopresto" + private static final String SAFE_USER_DN_ANDY = "CN=${SAFE_USER_NAME_ANDY}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_JOHN = "jdoe" + private static final String SAFE_USER_DN_JOHN = "CN=${SAFE_USER_NAME_JOHN}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_PROXY_1 = "proxy1.nifi.apache.org" + private static final String SAFE_USER_DN_PROXY_1 = "CN=${SAFE_USER_NAME_PROXY_1}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_PROXY_2 = "proxy2.nifi.apache.org" + private static final String SAFE_USER_DN_PROXY_2 = "CN=${SAFE_USER_NAME_PROXY_2}, OU=Apache NiFi" + + @BeforeClass + static void setUpOnce() throws Exception { + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() { + } + + @After + void tearDown() { + } + + @Test + void testShouldBuildProxyChain() throws Exception { + // Arrange + def mockProxy1 = [getIdentity: { -> SAFE_USER_NAME_PROXY_1}, getChain: { -> null}, isAnonymous: { -> false}] as NiFiUser + def mockJohn = [getIdentity: { -> SAFE_USER_NAME_JOHN}, getChain: { -> mockProxy1}, isAnonymous: { -> false}] as NiFiUser + + // Act + List proxiedEntitiesChain = NiFiUserUtils.buildProxiedEntitiesChain(mockJohn) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == [SAFE_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1] + } + + @Test + void testShouldBuildProxyChainFromSingleUser() throws Exception { + // Arrange + def mockJohn = [getIdentity: { -> SAFE_USER_NAME_JOHN}, getChain: { -> null}, isAnonymous: { -> false}] as NiFiUser + + // Act + List proxiedEntitiesChain = NiFiUserUtils.buildProxiedEntitiesChain(mockJohn) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == [SAFE_USER_NAME_JOHN] + } + + @Test + void testShouldBuildProxyChainFromAnonymousUser() throws Exception { + // Arrange + def mockProxy1 = [getIdentity: { -> SAFE_USER_NAME_PROXY_1}, getChain: { -> null}, isAnonymous: { -> false}] as NiFiUser + def mockAnonymous = [getIdentity: { -> "anonymous"}, getChain: { -> mockProxy1}, isAnonymous: { -> true}] as NiFiUser + + // Act + List proxiedEntitiesChain = NiFiUserUtils.buildProxiedEntitiesChain(mockAnonymous) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == ["", SAFE_USER_NAME_PROXY_1] + } + + @Test + void testBuildProxyChainFromNullUserShouldBeEmpty() throws Exception { + // Arrange + + // Act + List proxiedEntitiesChain = NiFiUserUtils.buildProxiedEntitiesChain(null) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == [] + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/java/org/apache/nifi/authorization/resource/DataAuthorizableTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/java/org/apache/nifi/authorization/resource/DataAuthorizableTest.java new file mode 100644 index 0000000000..069bf7919d --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/java/org/apache/nifi/authorization/resource/DataAuthorizableTest.java @@ -0,0 +1,160 @@ +/* + * 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.authorization.resource; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.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; + +import org.apache.nifi.authorization.AccessDeniedException; +import org.apache.nifi.authorization.AuthorizationRequest; +import org.apache.nifi.authorization.AuthorizationResult; +import org.apache.nifi.authorization.AuthorizationResult.Result; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.RequestAction; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.StandardNiFiUser; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +public class DataAuthorizableTest { + + private static final String IDENTITY_1 = "identity-1"; + private static final String PROXY_1 = "proxy-1"; + private static final String PROXY_2 = "proxy-2"; + + private Authorizable testProcessorAuthorizable; + private Authorizer testAuthorizer; + private DataAuthorizable testDataAuthorizable; + + @Before + public void setup() { + testProcessorAuthorizable = mock(Authorizable.class); + when(testProcessorAuthorizable.getParentAuthorizable()).thenReturn(null); + when(testProcessorAuthorizable.getResource()).thenReturn(ResourceFactory.getComponentResource(ResourceType.Processor, "id", "name")); + + testAuthorizer = mock(Authorizer.class); + when(testAuthorizer.authorize(any(AuthorizationRequest.class))).then(invocation -> { + final AuthorizationRequest request = invocation.getArgumentAt(0, AuthorizationRequest.class); + + if (IDENTITY_1.equals(request.getIdentity())) { + return AuthorizationResult.approved(); + } else if (PROXY_1.equals(request.getIdentity())) { + return AuthorizationResult.approved(); + } else if (PROXY_2.equals(request.getIdentity())) { + return AuthorizationResult.approved(); + } + + return AuthorizationResult.denied(); + }); + + testDataAuthorizable = new DataAuthorizable(testProcessorAuthorizable); + } + + @Test(expected = AccessDeniedException.class) + public void testAuthorizeNullUser() { + testDataAuthorizable.authorize(testAuthorizer, RequestAction.READ, null, null); + } + + @Test + public void testCheckAuthorizationNullUser() { + final AuthorizationResult result = testDataAuthorizable.checkAuthorization(testAuthorizer, RequestAction.READ, null, null); + assertEquals(Result.Denied, result.getResult()); + } + + @Test(expected = AccessDeniedException.class) + public void testAuthorizeUnauthorizedUser() { + final NiFiUser user = new StandardNiFiUser("unknown"); + testDataAuthorizable.authorize(testAuthorizer, RequestAction.READ, user, null); + } + + @Test + public void testCheckAuthorizationUnauthorizedUser() { + final NiFiUser user = new StandardNiFiUser("unknown"); + final AuthorizationResult result = testDataAuthorizable.checkAuthorization(testAuthorizer, RequestAction.READ, user, null); + assertEquals(Result.Denied, result.getResult()); + } + + @Test + public void testAuthorizedUser() { + final NiFiUser user = new StandardNiFiUser(IDENTITY_1); + testDataAuthorizable.authorize(testAuthorizer, RequestAction.READ, user, null); + + verify(testAuthorizer, times(1)).authorize(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Object o) { + return IDENTITY_1.equals(((AuthorizationRequest) o).getIdentity()); + } + })); + } + + @Test + public void testCheckAuthorizationUser() { + final NiFiUser user = new StandardNiFiUser(IDENTITY_1); + final AuthorizationResult result = testDataAuthorizable.checkAuthorization(testAuthorizer, RequestAction.READ, user, null); + + assertEquals(Result.Approved, result.getResult()); + verify(testAuthorizer, times(1)).authorize(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Object o) { + return IDENTITY_1.equals(((AuthorizationRequest) o).getIdentity()); + } + })); + } + + @Test + public void testAuthorizedUserChain() { + final NiFiUser proxy2 = new StandardNiFiUser(PROXY_2); + final NiFiUser proxy1 = new StandardNiFiUser(PROXY_1, proxy2); + final NiFiUser user = new StandardNiFiUser(IDENTITY_1, proxy1); + testDataAuthorizable.authorize(testAuthorizer, RequestAction.READ, user, null); + + verify(testAuthorizer, times(3)).authorize(any(AuthorizationRequest.class)); + verifyAuthorizeForUser(IDENTITY_1); + verifyAuthorizeForUser(PROXY_1); + verifyAuthorizeForUser(PROXY_2); + } + + @Test + public void testCheckAuthorizationUserChain() { + final NiFiUser proxy2 = new StandardNiFiUser(PROXY_2); + final NiFiUser proxy1 = new StandardNiFiUser(PROXY_1, proxy2); + final NiFiUser user = new StandardNiFiUser(IDENTITY_1, proxy1); + final AuthorizationResult result = testDataAuthorizable.checkAuthorization(testAuthorizer, RequestAction.READ, user, null); + + assertEquals(Result.Approved, result.getResult()); + verify(testAuthorizer, times(3)).authorize(any(AuthorizationRequest.class)); + verifyAuthorizeForUser(IDENTITY_1); + verifyAuthorizeForUser(PROXY_1); + verifyAuthorizeForUser(PROXY_2); + } + + private void verifyAuthorizeForUser(final String identity) { + verify(testAuthorizer, times(1)).authorize(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Object o) { + return identity.equals(((AuthorizationRequest) o).getIdentity()); + } + })); + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java index ff7900096d..3b4470fd70 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java @@ -23,35 +23,6 @@ import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter; import com.sun.jersey.core.util.MultivaluedMapImpl; -import org.apache.nifi.authorization.AccessDeniedException; -import org.apache.nifi.authorization.user.NiFiUser; -import org.apache.nifi.authorization.user.NiFiUserUtils; -import org.apache.nifi.cluster.coordination.ClusterCoordinator; -import org.apache.nifi.cluster.coordination.http.HttpResponseMapper; -import org.apache.nifi.cluster.coordination.http.StandardHttpResponseMapper; -import org.apache.nifi.cluster.coordination.node.NodeConnectionState; -import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; -import org.apache.nifi.cluster.manager.NodeResponse; -import org.apache.nifi.cluster.manager.exception.ConnectingNodeMutableRequestException; -import org.apache.nifi.cluster.manager.exception.DisconnectedNodeMutableRequestException; -import org.apache.nifi.cluster.manager.exception.IllegalClusterStateException; -import org.apache.nifi.cluster.manager.exception.NoConnectedNodesException; -import org.apache.nifi.cluster.manager.exception.UnknownNodeException; -import org.apache.nifi.cluster.manager.exception.UriConstructionException; -import org.apache.nifi.cluster.protocol.NodeIdentifier; -import org.apache.nifi.events.EventReporter; -import org.apache.nifi.reporting.Severity; -import org.apache.nifi.util.ComponentIdGenerator; -import org.apache.nifi.util.FormatUtils; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.security.ProxiedEntitiesUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.HttpMethod; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response.Status; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; @@ -76,6 +47,34 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; import java.util.stream.Collectors; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response.Status; +import org.apache.nifi.authorization.AccessDeniedException; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.apache.nifi.cluster.coordination.ClusterCoordinator; +import org.apache.nifi.cluster.coordination.http.HttpResponseMapper; +import org.apache.nifi.cluster.coordination.http.StandardHttpResponseMapper; +import org.apache.nifi.cluster.coordination.node.NodeConnectionState; +import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; +import org.apache.nifi.cluster.manager.NodeResponse; +import org.apache.nifi.cluster.manager.exception.ConnectingNodeMutableRequestException; +import org.apache.nifi.cluster.manager.exception.DisconnectedNodeMutableRequestException; +import org.apache.nifi.cluster.manager.exception.IllegalClusterStateException; +import org.apache.nifi.cluster.manager.exception.NoConnectedNodesException; +import org.apache.nifi.cluster.manager.exception.UnknownNodeException; +import org.apache.nifi.cluster.manager.exception.UriConstructionException; +import org.apache.nifi.cluster.protocol.NodeIdentifier; +import org.apache.nifi.events.EventReporter; +import org.apache.nifi.reporting.Severity; +import org.apache.nifi.util.ComponentIdGenerator; +import org.apache.nifi.util.FormatUtils; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.ProxiedEntitiesUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ThreadPoolRequestReplicator implements RequestReplicator { @@ -220,6 +219,18 @@ public class ThreadPoolRequestReplicator implements RequestReplicator { return replicate(nodeIdSet, method, uri, entity, headers, true, true); } + void addProxiedEntitiesHeader(final Map headers) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new AccessDeniedException("Unknown user"); + } + + // Add the user as a proxied entity so that when the receiving NiFi receives the request, + // it knows that we are acting as a proxy on behalf of the current user. + final String proxiedEntitiesChain = ProxiedEntitiesUtils.buildProxiedEntitiesChainString(user); + headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesChain); + } + @Override public AsyncClusterResponse replicate(Set nodeIds, String method, URI uri, Object entity, Map headers, final boolean indicateReplicated, final boolean performVerification) { @@ -230,14 +241,8 @@ public class ThreadPoolRequestReplicator implements RequestReplicator { updatedHeaders.put(RequestReplicator.REPLICATION_INDICATOR_HEADER, "true"); } - - // If the user is authenticated, add them as a proxied entity so that when the receiving NiFi receives the request, - // it knows that we are acting as a proxy on behalf of the current user. - final NiFiUser user = NiFiUserUtils.getNiFiUser(); - if (user != null && !user.isAnonymous()) { - final String proxiedEntitiesChain = ProxiedEntitiesUtils.buildProxiedEntitiesChainString(user); - updatedHeaders.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesChain); - } + // include the proxied entities header + addProxiedEntitiesHeader(updatedHeaders); if (indicateReplicated) { // If we are replicating a request and indicating that it is replicated, then this means that we are @@ -275,14 +280,10 @@ public class ThreadPoolRequestReplicator implements RequestReplicator { @Override public AsyncClusterResponse forwardToCoordinator(final NodeIdentifier coordinatorNodeId, final String method, final URI uri, final Object entity, final Map headers) { - // If the user is authenticated, add them as a proxied entity so that when the receiving NiFi receives the request, - // it knows that we are acting as a proxy on behalf of the current user. final Map updatedHeaders = new HashMap<>(headers); - final NiFiUser user = NiFiUserUtils.getNiFiUser(); - if (user != null && !user.isAnonymous()) { - final String proxiedEntitiesChain = ProxiedEntitiesUtils.buildProxiedEntitiesChainString(user); - updatedHeaders.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesChain); - } + + // include the proxied entities header + addProxiedEntitiesHeader(updatedHeaders); return replicate(Collections.singleton(coordinatorNodeId), method, uri, entity, updatedHeaders, false, null, false, false, null); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java index 02578a5b27..3c782a7ac1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java @@ -21,6 +21,13 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientHandlerException; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.ClientResponse.Status; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.core.header.InBoundHeaders; +import com.sun.jersey.core.header.OutBoundHeaders; import java.io.ByteArrayInputStream; import java.net.SocketTimeoutException; import java.net.URI; @@ -35,10 +42,11 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; - import javax.ws.rs.HttpMethod; - import org.apache.commons.collections4.map.MultiValueMap; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserDetails; +import org.apache.nifi.authorization.user.StandardNiFiUser; import org.apache.nifi.cluster.coordination.ClusterCoordinator; import org.apache.nifi.cluster.coordination.node.NodeConnectionState; import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; @@ -50,6 +58,8 @@ import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.api.entity.Entity; import org.apache.nifi.web.api.entity.ProcessorEntity; +import org.apache.nifi.web.security.ProxiedEntitiesUtils; +import org.apache.nifi.web.security.token.NiFiAuthenticationToken; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -57,14 +67,8 @@ import org.mockito.Mockito; import org.mockito.internal.util.reflection.Whitebox; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; - -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientHandlerException; -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.ClientResponse.Status; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.core.header.InBoundHeaders; -import com.sun.jersey.core.header.OutBoundHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; public class TestThreadPoolRequestReplicator { @@ -87,6 +91,10 @@ public class TestThreadPoolRequestReplicator { final URI uri = new URI("http://localhost:8080/processors/1"); final Entity entity = new ProcessorEntity(); + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + final AsyncClusterResponse response = replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, new HashMap<>(), true, true); // We should get back the same response object @@ -105,6 +113,29 @@ public class TestThreadPoolRequestReplicator { }); } + @Test + public void testRequestChain() { + final String proxyIdentity2 = "proxy-2"; + final String proxyIdentity1 = "proxy-1"; + final String userIdentity = "user"; + + withReplicator(replicator -> { + final Set nodeIds = new HashSet<>(); + nodeIds.add(new NodeIdentifier("1", "localhost", 8000, "localhost", 8001, "localhost", 8002, 8003, false)); + final URI uri = new URI("http://localhost:8080/processors/1"); + final Entity entity = new ProcessorEntity(); + + // set the user + final NiFiUser proxy2 = new StandardNiFiUser(proxyIdentity2); + final NiFiUser proxy1 = new StandardNiFiUser(proxyIdentity1, proxy2); + final NiFiUser user = new StandardNiFiUser(userIdentity, proxy1); + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(user)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, new HashMap<>(), true, true); + }, ClientResponse.Status.OK, 0L, null, "<" + userIdentity + "><" + proxyIdentity1 + "><" + proxyIdentity2 +">"); + } + @Test(timeout = 15000) public void testLongWaitForResponse() { withReplicator(replicator -> { @@ -114,6 +145,10 @@ public class TestThreadPoolRequestReplicator { final URI uri = new URI("http://localhost:8080/processors/1"); final Entity entity = new ProcessorEntity(); + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + final AsyncClusterResponse response = replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, new HashMap<>(), true, true); // We should get back the same response object @@ -146,6 +181,10 @@ public class TestThreadPoolRequestReplicator { final URI uri = new URI("http://localhost:8080/processors/1"); final Entity entity = new ProcessorEntity(); + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + final AsyncClusterResponse response = replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, new HashMap<>(), true, true); assertNotNull(response.awaitMergedResponse(1, TimeUnit.SECONDS)); }, null, 0L, new IllegalArgumentException("Exception created for unit test")); @@ -186,6 +225,10 @@ public class TestThreadPoolRequestReplicator { }; try { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + final AsyncClusterResponse clusterResponse = replicator.replicate(nodeIds, HttpMethod.POST, new URI("http://localhost:80/processors/1"), new ProcessorEntity(), new HashMap<>(), true, true); clusterResponse.awaitMergedResponse(); @@ -239,6 +282,10 @@ public class TestThreadPoolRequestReplicator { }; try { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + try { replicator.replicate(HttpMethod.POST, new URI("http://localhost:80/processors/1"), new ProcessorEntity(), new HashMap<>()); Assert.fail("Expected ConnectingNodeMutableRequestException"); @@ -306,6 +353,10 @@ public class TestThreadPoolRequestReplicator { }; try { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + final AsyncClusterResponse clusterResponse = replicator.replicate(nodeIds, HttpMethod.POST, new URI("http://localhost:80/processors/1"), new ProcessorEntity(), new HashMap<>(), true, true); clusterResponse.awaitMergedResponse(); @@ -352,9 +403,17 @@ public class TestThreadPoolRequestReplicator { preNotifyLatch.await(); try { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // ensure the proxied entities header is set + final Map updatedHeaders = new HashMap<>(); + replicator.addProxiedEntitiesHeader(updatedHeaders); + // Pass in Collections.emptySet() for the node ID's so that an Exception is thrown replicator.replicate(Collections.emptySet(), "GET", new URI("localhost:8080/nifi"), Collections.emptyMap(), - Collections.emptyMap(), true, null, true, true, monitor); + updatedHeaders, true, null, true, true, monitor); Assert.fail("replicate did not throw IllegalArgumentException"); } catch (final IllegalArgumentException iae) { // expected @@ -402,7 +461,15 @@ public class TestThreadPoolRequestReplicator { final URI uri = new URI("http://localhost:8080/processors/1"); final Entity entity = new ProcessorEntity(); - replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, new HashMap<>(), true, null, true, true, monitor); + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // ensure the proxied entities header is set + final Map updatedHeaders = new HashMap<>(); + replicator.addProxiedEntitiesHeader(updatedHeaders); + + replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, updatedHeaders, true, null, true, true, monitor); // wait for monitor to be notified. postNotifyLatch.await(); @@ -447,7 +514,15 @@ public class TestThreadPoolRequestReplicator { final URI uri = new URI("http://localhost:8080/processors/1"); final Entity entity = new ProcessorEntity(); - replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, new HashMap<>(), true, null, true, true, monitor); + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // ensure the proxied entities header is set + final Map updatedHeaders = new HashMap<>(); + replicator.addProxiedEntitiesHeader(updatedHeaders); + + replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, updatedHeaders, true, null, true, true, monitor); // wait for monitor to be notified. postNotifyLatch.await(); @@ -460,6 +535,10 @@ public class TestThreadPoolRequestReplicator { } private void withReplicator(final WithReplicator function, final Status status, final long delayMillis, final RuntimeException failure) { + withReplicator(function, status, delayMillis, failure, "<>"); + } + + private void withReplicator(final WithReplicator function, final Status status, final long delayMillis, final RuntimeException failure, final String expectedRequestChain) { final ClusterCoordinator coordinator = createClusterCoordinator(); final NiFiProperties nifiProps = NiFiProperties.createBasicNiFiProperties(null, null); final ThreadPoolRequestReplicator replicator = new ThreadPoolRequestReplicator(2, new Client(), coordinator, "1 sec", "1 sec", null, null, nifiProps) { @@ -478,6 +557,12 @@ public class TestThreadPoolRequestReplicator { throw failure; } + final OutBoundHeaders headers = (OutBoundHeaders) Whitebox.getInternalState(resourceBuilder, "metadata"); + final Object proxiedEntities = headers.getFirst(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + + // ensure the request chain is in the request + Assert.assertEquals(expectedRequestChain, proxiedEntities); + // Return given response from all nodes. final ClientResponse clientResponse = new ClientResponse(status, new InBoundHeaders(), new ByteArrayInputStream(new byte[0]), null); return new NodeResponse(nodeId, method, uri, clientResponse, -1L, requestId); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-custom-ui-utilities/src/main/java/org/apache/nifi/web/HttpServletRequestContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-custom-ui-utilities/src/main/java/org/apache/nifi/web/HttpServletRequestContext.java index 311fbc7fa4..06f389f853 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-custom-ui-utilities/src/main/java/org/apache/nifi/web/HttpServletRequestContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-custom-ui-utilities/src/main/java/org/apache/nifi/web/HttpServletRequestContext.java @@ -16,7 +16,6 @@ */ package org.apache.nifi.web; -import java.security.cert.X509Certificate; import javax.servlet.http.HttpServletRequest; /** @@ -42,19 +41,7 @@ public class HttpServletRequestContext implements NiFiWebRequestContext { @Override public String getProxiedEntitiesChain() { - String xProxiedEntitiesChain = request.getHeader("X-ProxiedEntitiesChain"); - final X509Certificate cert = extractClientCertificate(request); - if (cert != null) { - final String extractedPrincipal = extractPrincipal(cert); - final String formattedPrincipal = formatProxyDn(extractedPrincipal); - if (xProxiedEntitiesChain == null || xProxiedEntitiesChain.trim().isEmpty()) { - xProxiedEntitiesChain = formattedPrincipal; - } else { - xProxiedEntitiesChain += formattedPrincipal; - } - } - - return xProxiedEntitiesChain; + return null; } /** @@ -74,27 +61,4 @@ public class HttpServletRequestContext implements NiFiWebRequestContext { return request.getParameter(ID_PARAM); } - /** - * Utility methods that have been copied into this class to reduce the - * dependency footprint of this artifact. These utility methods typically - * live in web-utilities but that would pull in spring, jersey, jackson, - * etc. - */ - private X509Certificate extractClientCertificate(HttpServletRequest request) { - X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); - - if (certs != null && certs.length > 0) { - return certs[0]; - } - - return null; - } - - private String extractPrincipal(X509Certificate cert) { - return cert.getSubjectDN().getName().trim(); - } - - private String formatProxyDn(String dn) { - return "<" + dn + ">"; - } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java index c39fbc37c0..027aa73619 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java @@ -19,6 +19,16 @@ package org.apache.nifi.web; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse.Status; import com.sun.jersey.core.util.MultivaluedMapImpl; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MultivaluedMap; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authorization.AccessDeniedException; import org.apache.nifi.cluster.coordination.ClusterCoordinator; @@ -30,17 +40,6 @@ import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.controller.repository.claim.ContentDirection; import org.apache.nifi.util.NiFiProperties; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.core.MultivaluedMap; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * */ @@ -77,9 +76,6 @@ public class StandardNiFiContentAccess implements ContentAccess { // set the headers final Map headers = new HashMap<>(); - if (StringUtils.isNotBlank(request.getProxiedEntitiesChain())) { - headers.put("X-ProxiedEntitiesChain", request.getProxiedEntitiesChain()); - } // ensure we were able to detect the cluster node id if (request.getClusterNodeId() == null) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java index 8146a3933b..a0fae434fa 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java @@ -17,6 +17,20 @@ package org.apache.nifi.web; import com.sun.jersey.core.util.MultivaluedMapImpl; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.action.Action; import org.apache.nifi.action.Component; @@ -65,21 +79,6 @@ import org.apache.nifi.web.util.ClientResponseUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; - /** * Implements the NiFiWebConfigurationContext interface to support a context in both standalone and clustered environments. */ @@ -854,9 +853,6 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration private Map getHeaders(final NiFiWebRequestContext config) { final Map headers = new HashMap<>(); headers.put("Accept", "application/json,application/xml"); - if (StringUtils.isNotBlank(config.getProxiedEntitiesChain())) { - headers.put("X-ProxiedEntitiesChain", config.getProxiedEntitiesChain()); - } return headers; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-access/src/main/java/org/apache/nifi/web/ContentRequestContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-access/src/main/java/org/apache/nifi/web/ContentRequestContext.java index 6154576607..a7d83e3e7f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-access/src/main/java/org/apache/nifi/web/ContentRequestContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-access/src/main/java/org/apache/nifi/web/ContentRequestContext.java @@ -45,7 +45,12 @@ public interface ContentRequestContext { /** * The proxy chain for the current request, if applicable. * + * Update: + * This method has been deprecated since the entire proxy + * chain is able to be rebuilt using the current user if necessary. + * * @return the proxied entities chain */ + @Deprecated String getProxiedEntitiesChain(); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java index b1decd0e82..b2114d5b30 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java @@ -22,7 +22,6 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; - import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -299,7 +298,6 @@ public class ContentViewerController extends HttpServlet { private ContentRequestContext getContentRequest(final HttpServletRequest request) { final String ref = request.getParameter("ref"); final String clientId = request.getParameter("clientId"); - final String proxiedEntitiesChain = request.getHeader("X-ProxiedEntitiesChain"); final URI refUri = URI.create(ref); final String query = refUri.getQuery(); @@ -334,7 +332,7 @@ public class ContentViewerController extends HttpServlet { @Override public String getProxiedEntitiesChain() { - return proxiedEntitiesChain; + return null; } }; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java index 0ff9fed146..09f45bf470 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java @@ -16,29 +16,36 @@ */ package org.apache.nifi.web.security; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authorization.user.NiFiUser; import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * */ public class ProxiedEntitiesUtils { + private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class); public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted"; public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails"; - private static final Pattern proxyChainPattern = Pattern.compile("<(.*?)>"); + private static final String GT = ">"; + private static final String ESCAPED_GT = "\\\\>"; + private static final String LT = "<"; + private static final String ESCAPED_LT = "\\\\<"; + + private static final String ANONYMOUS_CHAIN = "<>"; /** * Formats the specified DN to be set as a HTTP header using well known conventions. @@ -47,7 +54,51 @@ public class ProxiedEntitiesUtils { * @return the dn formatted as an HTTP header */ public static String formatProxyDn(String dn) { - return "<" + dn + ">"; + return LT + sanitizeDn(dn) + GT; + } + + /** + * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user. + *

+ * Example: + *

+ * Provided DN: {@code jdoe> {@code } would allow the user to impersonate jdoe + * + * @param rawDn the unsanitized DN + * @return the sanitized DN + */ + private static String sanitizeDn(String rawDn) { + if (StringUtils.isEmpty(rawDn)) { + return rawDn; + } else { + String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT); + if (!sanitizedDn.equals(rawDn)) { + logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]"); + } + return sanitizedDn; + } + } + + /** + * Reconstitutes the original DN from the sanitized version passed in the proxy chain. + *

+ * Example: + *

+ * {@code alopresto\>\ {@code alopresto> tokenizeProxiedEntitiesChain(String rawProxyChain) { final List proxyChain = new ArrayList<>(); - final Matcher rawProxyChainMatcher = proxyChainPattern.matcher(rawProxyChain); - while (rawProxyChainMatcher.find()) { - proxyChain.add(rawProxyChainMatcher.group(1)); + if (!StringUtils.isEmpty(rawProxyChain)) { + // Split the String on the >< token + List elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><")); + + // Unsanitize each DN and collect back + elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList()); + + // Remove the leading < from the first element + elements.set(0, elements.get(0).replaceFirst(LT, "")); + + // Remove the trailing > from the last element + int last = elements.size() - 1; + String lastElement = elements.get(last); + if (lastElement.endsWith(GT)) { + elements.set(last, lastElement.substring(0, lastElement.length() - 1)); + } + + proxyChain.addAll(elements); } return proxyChain; @@ -74,42 +140,12 @@ public class ProxiedEntitiesUtils { */ public static String buildProxiedEntitiesChainString(final NiFiUser user) { // calculate the dn chain - final List proxyChain = NiFiUserUtils.buildProxiedEntitiesChain(user); - return formatProxyDn(StringUtils.join(proxyChain, "><")); - } - - /** - * Builds the proxy chain from the specified request and user. - * - * @param request the request - * @param username the username - * @return the proxy chain in list form - */ - public static List buildProxiedEntitiesChain(final HttpServletRequest request, final String username) { - final String chain = buildProxiedEntitiesChainString(request, username); - return tokenizeProxiedEntitiesChain(chain); - } - - /** - * Builds the dn chain from the specified request and user. - * - * @param request the request - * @param username the username - * @return the dn chain in string form - */ - public static String buildProxiedEntitiesChainString(final HttpServletRequest request, final String username) { - String principal; - if (username.startsWith("<") && username.endsWith(">")) { - principal = username; - } else { - principal = formatProxyDn(username); + List proxyChain = NiFiUserUtils.buildProxiedEntitiesChain(user); + if (proxyChain.isEmpty()) { + return ANONYMOUS_CHAIN; } - - // look for a proxied user - if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { - principal = request.getHeader(PROXY_ENTITIES_CHAIN) + principal; - } - return principal; + proxyChain = proxyChain.stream().map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList()); + return StringUtils.join(proxyChain, ""); } public static void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java index 16160a47bc..b5835d0fef 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java @@ -16,6 +16,11 @@ */ package org.apache.nifi.web.security.x509; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authentication.AuthenticationResponse; import org.apache.nifi.authorization.AuthorizationRequest; @@ -37,12 +42,6 @@ import org.apache.nifi.web.security.token.NiFiAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; - /** * */ @@ -79,19 +78,27 @@ public class X509AuthenticationProvider extends NiFiAuthenticationProvider { // add the chain as appropriate to each proxy NiFiUser proxy = null; - for (final ListIterator chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious();) { - final String identity = mapIdentity(chainIter.previous()); + for (final ListIterator chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { + String identity = chainIter.previous(); + + // determine if the user is anonymous + final boolean isAnonymous = StringUtils.isBlank(identity); + if (isAnonymous) { + identity = StandardNiFiUser.ANONYMOUS_IDENTITY; + } else { + identity = mapIdentity(identity); + } if (chainIter.hasPrevious()) { // authorize this proxy in order to authenticate this user final AuthorizationRequest proxyAuthorizationRequest = new AuthorizationRequest.Builder() - .identity(identity) - .anonymous(false) - .accessAttempt(true) - .action(RequestAction.WRITE) - .resource(ResourceFactory.getProxyResource()) - .userContext(proxy == null ? getUserContext(request) : null) // only set the context for the real user - .build(); + .identity(identity) + .anonymous(isAnonymous) + .accessAttempt(true) + .action(RequestAction.WRITE) + .resource(ResourceFactory.getProxyResource()) + .userContext(proxy == null ? getUserContext(request) : null) // only set the context for the real user + .build(); final AuthorizationResult proxyAuthorizationResult = authorizer.authorize(proxyAuthorizationRequest); if (!Result.Approved.equals(proxyAuthorizationResult.getResult())) { @@ -99,20 +106,34 @@ public class X509AuthenticationProvider extends NiFiAuthenticationProvider { } } - // only set the client address for user making the request, we don't know the client address of the proxies - if (proxy == null) { - proxy = new StandardNiFiUser(identity, proxy, request.getClientAddress()); - } else { - proxy = new StandardNiFiUser(identity, proxy, null); - } + // Only set the client address for user making the request because we don't know the client address of the proxies + String clientAddress = (proxy == null) ? request.getClientAddress() : null; + proxy = createUser(identity, proxy, clientAddress, isAnonymous); } return new NiFiAuthenticationToken(new NiFiUserDetails(proxy)); } } - private Map getUserContext(final X509AuthenticationRequestToken request) { - final Map userContext; + /** + * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values. + * + * @param identity the user's identity + * @param chain the proxied entities + * @param clientAddress the requesting IP address + * @param isAnonymous if true, an anonymous user will be returned (identity will be ignored) + * @return the populated user + */ + protected static NiFiUser createUser(String identity, NiFiUser chain, String clientAddress, boolean isAnonymous) { + if (isAnonymous) { + return StandardNiFiUser.populateAnonymousUser(chain, clientAddress); + } else { + return new StandardNiFiUser(identity, chain, clientAddress); + } + } + + private Map getUserContext(final X509AuthenticationRequestToken request) { + final Map userContext; if (!StringUtils.isBlank(request.getClientAddress())) { userContext = new HashMap<>(); userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), request.getClientAddress()); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/ProxiedEntitiesUtilsTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/ProxiedEntitiesUtilsTest.groovy new file mode 100644 index 0000000000..460b4eb15e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/ProxiedEntitiesUtilsTest.groovy @@ -0,0 +1,265 @@ +/* + * 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.web.security + +import org.apache.nifi.authorization.user.NiFiUser +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@RunWith(JUnit4.class) +class ProxiedEntitiesUtilsTest { + private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class) + + private static final String SAFE_USER_NAME_ANDY = "alopresto" + private static final String SAFE_USER_DN_ANDY = "CN=${SAFE_USER_NAME_ANDY}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_JOHN = "jdoe" + private static final String SAFE_USER_DN_JOHN = "CN=${SAFE_USER_NAME_JOHN}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_PROXY_1 = "proxy1.nifi.apache.org" + private static final String SAFE_USER_DN_PROXY_1 = "CN=${SAFE_USER_NAME_PROXY_1}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_PROXY_2 = "proxy2.nifi.apache.org" + private static final String SAFE_USER_DN_PROXY_2 = "CN=${SAFE_USER_NAME_PROXY_2}, OU=Apache NiFi" + + private static + final String MALICIOUS_USER_NAME_JOHN = "${SAFE_USER_NAME_JOHN}, OU=Apache NiFi> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() { + } + + @After + void tearDown() { + } + + private static String sanitizeDn(String dn = "") { + dn.replaceAll(/>/, '\\\\>').replaceAll('<', '\\\\<') + } + + private static String printUnicodeString(final String raw) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < raw.size(); i++) { + int codePoint = Character.codePointAt(raw, i) + int charCount = Character.charCount(codePoint) + if (charCount > 1) { + i += charCount - 1 // 2. + if (i >= raw.length()) { + throw new IllegalArgumentException("Code point indicated more characters than available") + } + } + sb.append(String.format("\\u%04x ", codePoint)) + } + return sb.toString().trim() + } + + @Test + void testSanitizeDnShouldHandleFuzzing() throws Exception { + // Arrange + final String DESIRED_NAME = SAFE_USER_NAME_JOHN + logger.info(" Desired name: ${DESIRED_NAME} | ${printUnicodeString(DESIRED_NAME)}") + + // Contains various attempted >< escapes, trailing NULL, and BACKSPACE + 'n' + final List MALICIOUS_NAMES = [MALICIOUS_USER_NAME_JOHN, + SAFE_USER_NAME_JOHN + ">", + SAFE_USER_NAME_JOHN + "><>", + SAFE_USER_NAME_JOHN + "\\>", + SAFE_USER_NAME_JOHN + "\u003e", + SAFE_USER_NAME_JOHN + "\u005c\u005c\u003e", + SAFE_USER_NAME_JOHN + "\u0000", + SAFE_USER_NAME_JOHN + "\u0008n"] + + // Act + MALICIOUS_NAMES.each { String name -> + logger.info(" Raw name: ${name} | ${printUnicodeString(name)}") + String sanitizedName = ProxiedEntitiesUtils.sanitizeDn(name) + logger.info("Sanitized name: ${sanitizedName} | ${printUnicodeString(sanitizedName)}") + + // Assert + assert sanitizedName != DESIRED_NAME + } + } + + @Test + void testShouldFormatProxyDn() throws Exception { + // Arrange + final String DN = SAFE_USER_DN_JOHN + logger.info(" Provided proxy DN: ${DN}") + + final String EXPECTED_PROXY_DN = "<${DN}>" + logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}") + + // Act + String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN) + logger.info("Forjohned proxy DN: ${forjohnedProxyDn}") + + // Assert + assert forjohnedProxyDn == EXPECTED_PROXY_DN + } + + @Test + void testFormatProxyDnShouldHandleMaliciousInput() throws Exception { + // Arrange + final String DN = MALICIOUS_USER_DN_JOHN + logger.info(" Provided proxy DN: ${DN}") + + final String SANITIZED_DN = sanitizeDn(DN) + final String EXPECTED_PROXY_DN = "<${SANITIZED_DN}>" + logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}") + + // Act + String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN) + logger.info("Forjohned proxy DN: ${forjohnedProxyDn}") + + // Assert + assert forjohnedProxyDn == EXPECTED_PROXY_DN + } + + @Test + void testShouldBuildProxyChain() throws Exception { + // Arrange + def mockProxy1 = [getIdentity: { -> SAFE_USER_NAME_PROXY_1 }, getChain: { -> null }, isAnonymous: { -> false}] as NiFiUser + def mockJohn = [getIdentity: { -> SAFE_USER_NAME_JOHN }, getChain: { -> mockProxy1 }, isAnonymous: { -> false}] as NiFiUser + + // Act + String proxiedEntitiesChain = ProxiedEntitiesUtils.buildProxiedEntitiesChainString(mockJohn) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == "<${SAFE_USER_NAME_JOHN}><${SAFE_USER_NAME_PROXY_1}>" as String + } + + @Test + void testBuildProxyChainFromNullUserShouldBeAnonymous() throws Exception { + // Arrange + + // Act + String proxiedEntitiesChain = ProxiedEntitiesUtils.buildProxiedEntitiesChainString(null) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == "<>" + } + + @Test + void testBuildProxyChainFromAnonymousUserShouldBeAnonymous() throws Exception { + // Arrange + def mockProxy1 = [getIdentity: { -> SAFE_USER_NAME_PROXY_1 }, getChain: { -> null }, isAnonymous: { -> false}] as NiFiUser + def mockAnonymous = [getIdentity: { -> "anonymous" }, getChain: { -> mockProxy1 }, isAnonymous: { -> true}] as NiFiUser + + // Act + String proxiedEntitiesChain = ProxiedEntitiesUtils.buildProxiedEntitiesChainString(mockAnonymous) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == "<><${SAFE_USER_NAME_PROXY_1}>" as String + } + + @Test + void testBuildProxyChainShouldHandleMaliciousUser() throws Exception { + // Arrange + def mockProxy1 = [getIdentity: { -> SAFE_USER_NAME_PROXY_1 }, getChain: { -> null }, isAnonymous: { -> false}] as NiFiUser + def mockJohn = [getIdentity: { -> MALICIOUS_USER_NAME_JOHN }, getChain: { -> mockProxy1 }, isAnonymous: { -> false}] as NiFiUser + + // Act + String proxiedEntitiesChain = ProxiedEntitiesUtils.buildProxiedEntitiesChainString(mockJohn) + logger.info("Proxied entities chain: ${proxiedEntitiesChain}") + + // Assert + assert proxiedEntitiesChain == "<${MALICIOUS_USER_NAME_JOHN_ESCAPED}><${SAFE_USER_NAME_PROXY_1}>" as String + } + + @Test + void testShouldTokenizeProxiedEntitiesChainWithUserNames() throws Exception { + // Arrange + final List NAMES = [SAFE_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2] + final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames}") + + // Assert + assert tokenizedNames == NAMES + } + + @Test + void testShouldTokenizeProxiedEntitiesChainWithDNs() throws Exception { + // Arrange + final List DNS = [SAFE_USER_DN_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2] + final String RAW_PROXY_CHAIN = "<${DNS.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedDns = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedDns.collect { "\"${it}\"" }}") + + // Assert + assert tokenizedDns == DNS + } + + @Test + void testShouldTokenizeProxiedEntitiesChainWithAnonymousUser() throws Exception { + // Arrange + final List NAMES = ["", SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2] + final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames}") + + // Assert + assert tokenizedNames == NAMES + } + + @Test + void testTokenizeProxiedEntitiesChainShouldHandleMaliciousUser() throws Exception { + // Arrange + final List NAMES = [MALICIOUS_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2] + final String RAW_PROXY_CHAIN = "<${NAMES.collect { sanitizeDn(it) }.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames.collect { "\"${it}\"" }}") + + // Assert + assert tokenizedNames == NAMES + assert tokenizedNames.size() == NAMES.size() + assert !tokenizedNames.contains(SAFE_USER_NAME_JOHN) + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/x509/X509AuthenticationProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/x509/X509AuthenticationProviderTest.java new file mode 100644 index 0000000000..43aea8645a --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/x509/X509AuthenticationProviderTest.java @@ -0,0 +1,276 @@ +/* + * 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.web.security.x509; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.authentication.AuthenticationResponse; +import org.apache.nifi.authorization.AuthorizationRequest; +import org.apache.nifi.authorization.AuthorizationResult; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserDetails; +import org.apache.nifi.authorization.user.StandardNiFiUser; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.InvalidAuthenticationException; +import org.apache.nifi.web.security.UntrustedProxyException; +import org.apache.nifi.web.security.token.NiFiAuthenticationToken; +import org.junit.Before; +import org.junit.Test; + +public class X509AuthenticationProviderTest { + + private static final String INVALID_CERTIFICATE = "invalid-certificate"; + private static final String IDENTITY_1 = "identity-1"; + private static final String ANONYMOUS = ""; + + private static final String UNTRUSTED_PROXY = "untrusted-proxy"; + private static final String PROXY_1 = "proxy-1"; + private static final String PROXY_2 = "proxy-2"; + + private static final String GT = ">"; + private static final String ESCAPED_GT = "\\\\>"; + private static final String LT = "<"; + private static final String ESCAPED_LT = "\\\\<"; + + private X509AuthenticationProvider x509AuthenticationProvider; + private X509IdentityProvider certificateIdentityProvider; + private SubjectDnX509PrincipalExtractor extractor; + private Authorizer authorizer; + + @Before + public void setup() { + extractor = new SubjectDnX509PrincipalExtractor(); + + certificateIdentityProvider = mock(X509IdentityProvider.class); + when(certificateIdentityProvider.authenticate(any(X509Certificate[].class))).then(invocation -> { + final X509Certificate[] certChain = invocation.getArgumentAt(0, X509Certificate[].class); + final String identity = extractor.extractPrincipal(certChain[0]).toString(); + + if (INVALID_CERTIFICATE.equals(identity)) { + throw new IllegalArgumentException(); + } + + return new AuthenticationResponse(identity, identity, TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS), ""); + }); + + authorizer = mock(Authorizer.class); + when(authorizer.authorize(any(AuthorizationRequest.class))).then(invocation -> { + final AuthorizationRequest request = invocation.getArgumentAt(0, AuthorizationRequest.class); + + if (UNTRUSTED_PROXY.equals(request.getIdentity())) { + return AuthorizationResult.denied(); + } + + return AuthorizationResult.approved(); + }); + + x509AuthenticationProvider = new X509AuthenticationProvider(certificateIdentityProvider, authorizer, NiFiProperties.createBasicNiFiProperties(null, null)); + } + + @Test(expected = InvalidAuthenticationException.class) + public void testInvalidCertificate() { + x509AuthenticationProvider.authenticate(getX509Request("", INVALID_CERTIFICATE)); + } + + @Test + public void testNoProxyChain() { + final NiFiAuthenticationToken auth = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(getX509Request("", IDENTITY_1)); + final NiFiUser user = ((NiFiUserDetails) auth.getDetails()).getNiFiUser(); + + assertNotNull(user); + assertEquals(IDENTITY_1, user.getIdentity()); + assertFalse(user.isAnonymous()); + } + + @Test(expected = UntrustedProxyException.class) + public void testUntrustedProxy() { + x509AuthenticationProvider.authenticate(getX509Request(buildProxyChain(IDENTITY_1), UNTRUSTED_PROXY)); + } + + @Test + public void testOneProxy() { + final NiFiAuthenticationToken auth = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(getX509Request(buildProxyChain(IDENTITY_1), PROXY_1)); + final NiFiUser user = ((NiFiUserDetails) auth.getDetails()).getNiFiUser(); + + assertNotNull(user); + assertEquals(IDENTITY_1, user.getIdentity()); + assertFalse(user.isAnonymous()); + + assertNotNull(user.getChain()); + assertEquals(PROXY_1, user.getChain().getIdentity()); + assertFalse(user.getChain().isAnonymous()); + } + + @Test + public void testAnonymousWithOneProxy() { + final NiFiAuthenticationToken auth = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(getX509Request(buildProxyChain(ANONYMOUS), PROXY_1)); + final NiFiUser user = ((NiFiUserDetails) auth.getDetails()).getNiFiUser(); + + assertNotNull(user); + assertEquals(StandardNiFiUser.ANONYMOUS_IDENTITY, user.getIdentity()); + assertTrue(user.isAnonymous()); + + assertNotNull(user.getChain()); + assertEquals(PROXY_1, user.getChain().getIdentity()); + assertFalse(user.getChain().isAnonymous()); + } + + @Test + public void testTwoProxies() { + final NiFiAuthenticationToken auth = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(getX509Request(buildProxyChain(IDENTITY_1, PROXY_2), PROXY_1)); + final NiFiUser user = ((NiFiUserDetails) auth.getDetails()).getNiFiUser(); + + assertNotNull(user); + assertEquals(IDENTITY_1, user.getIdentity()); + assertFalse(user.isAnonymous()); + + assertNotNull(user.getChain()); + assertEquals(PROXY_2, user.getChain().getIdentity()); + assertFalse(user.getChain().isAnonymous()); + + assertNotNull(user.getChain().getChain()); + assertEquals(PROXY_1, user.getChain().getChain().getIdentity()); + assertFalse(user.getChain().getChain().isAnonymous()); + } + + @Test(expected = UntrustedProxyException.class) + public void testUntrustedProxyInChain() { + x509AuthenticationProvider.authenticate(getX509Request(buildProxyChain(IDENTITY_1, UNTRUSTED_PROXY), PROXY_1)); + } + + @Test + public void testAnonymousProxyInChain() { + final NiFiAuthenticationToken auth = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(getX509Request(buildProxyChain(IDENTITY_1, ANONYMOUS), PROXY_1)); + final NiFiUser user = ((NiFiUserDetails) auth.getDetails()).getNiFiUser(); + + assertNotNull(user); + assertEquals(IDENTITY_1, user.getIdentity()); + assertFalse(user.isAnonymous()); + + assertNotNull(user.getChain()); + assertEquals(StandardNiFiUser.ANONYMOUS_IDENTITY, user.getChain().getIdentity()); + assertTrue(user.getChain().isAnonymous()); + + assertNotNull(user.getChain().getChain()); + assertEquals(PROXY_1, user.getChain().getChain().getIdentity()); + assertFalse(user.getChain().getChain().isAnonymous()); + } + + @Test + public void testShouldCreateAnonymousUser() { + // Arrange + String identity = "someone"; + + // Act + NiFiUser user = X509AuthenticationProvider.createUser(identity, null, null, true); + + // Assert + assert user != null; + assert user instanceof StandardNiFiUser; + assert user.getIdentity().equals(StandardNiFiUser.ANONYMOUS_IDENTITY); + assert user.isAnonymous(); + } + + @Test + public void testShouldCreateKnownUser() { + // Arrange + String identity = "someone"; + + // Act + NiFiUser user = X509AuthenticationProvider.createUser(identity, null, null, false); + + // Assert + assert user != null; + assert user instanceof StandardNiFiUser; + assert user.getIdentity().equals(identity); + assert !user.isAnonymous(); + } + + private String buildProxyChain(final String... identities) { + List elements = Arrays.asList(identities); + return StringUtils.join(elements.stream().map(X509AuthenticationProviderTest::formatDn).collect(Collectors.toList()), ""); + } + + private static String formatDn(String rawDn) { + return "<" + sanitizeDn(rawDn) + ">"; + } + + /** + * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user. + *

+ * Example: + *

+ * Provided DN: {@code jdoe> {@code } would allow the user to impersonate jdoe + * + * @param rawDn the unsanitized DN + * @return the sanitized DN + */ + private static String sanitizeDn(String rawDn) { + if (StringUtils.isEmpty(rawDn)) { + return rawDn; + } else { + return rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT); + } + } + + /** + * Reconstitutes the original DN from the sanitized version passed in the proxy chain. + *

+ * Example: + *

+ * {@code alopresto\>\ {@code alopresto> { + final Principal principal = mock(Principal.class); + when(principal.getName()).thenReturn(identity); + return principal; + }); + return certificate; + } + +} \ No newline at end of file