Refactored user identity parsing and proxied entity chain formatting.

Added unit tests.

Signed-off-by: Andy LoPresto <alopresto@apache.org>
This commit is contained in:
Andy LoPresto 2017-02-01 21:32:35 -08:00
parent 4bf267c8bb
commit bd88e4335a
No known key found for this signature in database
GPG Key ID: 3C6EF65B2F7DEF69
17 changed files with 1181 additions and 245 deletions

View File

@ -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();
}

View File

@ -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();
}
};
// 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<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();
}
};
// 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);
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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 == []
}
}

View File

@ -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());
}
}));
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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 + ">";
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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;
}
};
}

View File

@ -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);
List<String> 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) {

View File

@ -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());

View File

@ -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)
}
}

View File

@ -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<String> 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.
* <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 {
return rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
}
}
/**
* 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 {
return sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
}
}
private X509AuthenticationRequestToken getX509Request(final String proxyChain, final String identity) {
return new X509AuthenticationRequestToken(proxyChain, extractor, new X509Certificate[]{getX509Certificate(identity)}, "");
}
private X509Certificate getX509Certificate(final String identity) {
final X509Certificate certificate = mock(X509Certificate.class);
when(certificate.getSubjectDN()).then(invocation -> {
final Principal principal = mock(Principal.class);
when(principal.getName()).thenReturn(identity);
return principal;
});
return certificate;
}
}