Refactored user identity parsing and proxied entity chain formatting.
Added unit tests.

Signed-off-by: Andy LoPresto <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/bd88e433
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/bd88e433
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/bd88e433

Branch: refs/heads/master
Commit: bd88e4335ad151592f1310996e9a0513b7f0829a
Parents: 4bf267c
Author: Andy LoPresto <[email protected]>
Authored: Wed Feb 1 21:32:35 2017 -0800
Committer: Andy LoPresto <[email protected]>
Committed: Tue Feb 14 13:39:05 2017 -0800

----------------------------------------------------------------------
 .../apache/nifi/web/NiFiWebRequestContext.java  |   5 +
 .../resource/DataAuthorizable.java              |  61 ++--
 .../nifi/authorization/user/NiFiUserUtils.java  |  17 +-
 .../authorization/user/StandardNiFiUser.java    |  35 ++-
 .../authorization/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 +--
 .../apache/nifi/web/ContentRequestContext.java  |   5 +
 .../nifi/web/ContentViewerController.java       |   4 +-
 .../nifi/web/security/ProxiedEntitiesUtils.java | 130 +++++----
 .../x509/X509AuthenticationProvider.java        |  67 +++--
 .../security/ProxiedEntitiesUtilsTest.groovy    | 265 ++++++++++++++++++
 .../x509/X509AuthenticationProviderTest.java    | 276 +++++++++++++++++++
 17 files changed, 1183 insertions(+), 247 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-framework-api/src/main/java/org/apache/nifi/web/NiFiWebRequestContext.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 9dd44ab..bb3bbd3 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 {
      * 
&lt;CN=original-proxied-entity&gt;&lt;CN=first-proxy&gt;&lt;CN=second-proxy&gt;...
      * </code>
      *
+     * 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();
 
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/resource/DataAuthorizable.java
----------------------------------------------------------------------
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 7269560..012e09d 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<String> 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();
-                    }
-                };
-
-                result = Authorizable.super.checkAuthorization(authorizer, 
action, chainUser, resourceContext);
+                // perform the current user authorization
+                result = Authorizable.super.checkAuthorization(authorizer, 
action, chainedUser, 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<String> 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();
-                    }
-                };
-
-                Authorizable.super.authorize(authorizer, action, chainUser, 
resourceContext);
+                // perform the current user authorization
+                Authorizable.super.authorize(authorizer, action, chainedUser, 
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);
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/NiFiUserUtils.java
----------------------------------------------------------------------
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 6a4776a..93e070d 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;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/user/StandardNiFiUser.java
----------------------------------------------------------------------
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 372d89f..2a82795 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

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/groovy/org/apache/nifi/authorization/user/NiFiUserUtilsTest.groovy
----------------------------------------------------------------------
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 0000000..e76b171
--- /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<String> 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<String> 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<String> 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<String> proxiedEntitiesChain = 
NiFiUserUtils.buildProxiedEntitiesChain(null)
+        logger.info("Proxied entities chain: ${proxiedEntitiesChain}")
+
+        // Assert
+        assert proxiedEntitiesChain == []
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/test/java/org/apache/nifi/authorization/resource/DataAuthorizableTest.java
----------------------------------------------------------------------
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 0000000..069bf79
--- /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<AuthorizationRequest>() {
+            @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<AuthorizationRequest>() {
+            @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<AuthorizationRequest>() {
+            @Override
+            public boolean matches(Object o) {
+                return identity.equals(((AuthorizationRequest) 
o).getIdentity());
+            }
+        }));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
----------------------------------------------------------------------
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 ff79000..3b4470f 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<String, String> 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<NodeIdentifier> nodeIds, String 
method, URI uri, Object entity, Map<String, String> 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<String, String> 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<String, String> 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);
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java
----------------------------------------------------------------------
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 02578a5..3c782a7 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<NodeIdentifier> 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<String, String> 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<String, String> 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<String, String> 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);

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-custom-ui-utilities/src/main/java/org/apache/nifi/web/HttpServletRequestContext.java
----------------------------------------------------------------------
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 311fbc7..06f389f 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 + ">";
-    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java
----------------------------------------------------------------------
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 c39fbc3..027aa73 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<String, String> 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) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java
----------------------------------------------------------------------
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 8146a39..a0fae43 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<String, String> getHeaders(final NiFiWebRequestContext config) 
{
         final Map<String, String> headers = new HashMap<>();
         headers.put("Accept", "application/json,application/xml");
-        if (StringUtils.isNotBlank(config.getProxiedEntitiesChain())) {
-            headers.put("X-ProxiedEntitiesChain", 
config.getProxiedEntitiesChain());
-        }
         return headers;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-access/src/main/java/org/apache/nifi/web/ContentRequestContext.java
----------------------------------------------------------------------
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 6154576..a7d83e3 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();
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
----------------------------------------------------------------------
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 b1decd0..b2114d5 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;
             }
         };
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ProxiedEntitiesUtils.java
----------------------------------------------------------------------
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 0ff9fed..09f45bf 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.
+     * <p>
+     * Example:
+     * <p>
+     * Provided DN: {@code jdoe><alopresto} -> {@code 
<jdoe><alopresto><proxy...>} 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.
+     * <p>
+     * Example:
+     * <p>
+     * {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
+     *
+     * @param sanitizedDn the sanitized DN
+     * @return the original DN
+     */
+    private static String unsanitizeDn(String sanitizedDn) {
+        if (StringUtils.isEmpty(sanitizedDn)) {
+            return sanitizedDn;
+        } else {
+            String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, 
GT).replaceAll(ESCAPED_LT, LT);
+            if (!unsanitizedDn.equals(sanitizedDn)) {
+                logger.warn("The provided DN [" + sanitizedDn + "] had been 
escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
+            }
+            return unsanitizedDn;
+        }
     }
 
     /**
@@ -58,9 +109,24 @@ public class ProxiedEntitiesUtils {
      */
     public static List<String> tokenizeProxiedEntitiesChain(String 
rawProxyChain) {
         final List<String> 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<String> 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<String> 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<String> 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);
-        }
-
-        // look for a proxied user
-        if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
-            principal = request.getHeader(PROXY_ENTITIES_CHAIN) + principal;
+        List<String> proxyChain = 
NiFiUserUtils.buildProxiedEntitiesChain(user);
+        if (proxyChain.isEmpty()) {
+            return ANONYMOUS_CHAIN;
         }
-        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) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java
----------------------------------------------------------------------
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 16160a4..b5835d0 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<String> chainIter = 
proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious();) {
-                final String identity = mapIdentity(chainIter.previous());
+            for (final ListIterator<String> 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<String,String> getUserContext(final 
X509AuthenticationRequestToken request) {
-        final Map<String,String> 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<String, String> getUserContext(final 
X509AuthenticationRequestToken request) {
+        final Map<String, String> userContext;
         if (!StringUtils.isBlank(request.getClientAddress())) {
             userContext = new HashMap<>();
             userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), 
request.getClientAddress());

http://git-wip-us.apache.org/repos/asf/nifi/blob/bd88e433/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/ProxiedEntitiesUtilsTest.groovy
----------------------------------------------------------------------
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 0000000..460b4eb
--- /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><CN=${SAFE_USER_NAME_PROXY_1}"
+    private static final String MALICIOUS_USER_DN_JOHN = 
"CN=${MALICIOUS_USER_NAME_JOHN}, OU=Apache NiFi"
+
+    private static
+    final String MALICIOUS_USER_NAME_JOHN_ESCAPED = 
sanitizeDn(MALICIOUS_USER_NAME_JOHN)
+    private static final String MALICIOUS_USER_DN_JOHN_ESCAPED = 
sanitizeDn(MALICIOUS_USER_DN_JOHN)
+
+    @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() {
+    }
+
+    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)
+    }
+}

Reply via email to