mirror of https://github.com/apache/nifi.git
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:
parent
4bf267c8bb
commit
bd88e4335a
|
@ -49,8 +49,13 @@ public interface NiFiWebRequestContext {
|
|||
* <CN=original-proxied-entity><CN=first-proxy><CN=second-proxy>...
|
||||
* </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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 == []
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 + ">";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue