NIFI-655:

- Refactoring certificate extraction and validation.
- Refactoring how expiration is specified in the login identity providers.
- Adding unit tests for the access endpoints.
- Code clean up.
This commit is contained in:
Matt Gilman 2015-11-17 17:02:41 -05:00
parent 7529694f23
commit a196207725
34 changed files with 969 additions and 696 deletions

View File

@ -23,16 +23,19 @@ public class AuthenticationResponse {
private final String identity;
private final String username;
private final long expiration;
/**
* Creates an authentication response. The username and how long the authentication is valid in milliseconds
*
* @param identity The user identity
* @param username The username
* @param expiration The expiration in milliseconds
*/
public AuthenticationResponse(final String identity, final String username) {
public AuthenticationResponse(final String identity, final String username, final long expiration) {
this.identity = identity;
this.username = username;
this.expiration = expiration;
}
public String getIdentity() {
@ -43,4 +46,13 @@ public class AuthenticationResponse {
return username;
}
/**
* Returns the expiration of a given authentication in milliseconds.
*
* @return The expiration in milliseconds
*/
public long getExpiration() {
return expiration;
}
}

View File

@ -36,13 +36,6 @@ public interface LoginIdentityProvider {
*/
AuthenticationResponse authenticate(LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException;
/**
* Returns the expiration of a given authentication in milliseconds.
*
* @return The expiration in milliseconds
*/
long getExpiration();
/**
* Called immediately after instance creation for implementers to perform additional setup
*

View File

@ -28,7 +28,7 @@ import org.apache.nifi.web.security.node.NodeAuthorizedUserFilter;
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.x509.X509AuthenticationFilter;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
import org.apache.nifi.web.security.x509.X509CertificateValidator;
import org.apache.nifi.web.security.x509.X509IdentityProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -42,7 +42,6 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
* NiFi Web Api Spring security
@ -56,9 +55,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
private UserService userService;
private AuthenticationUserDetailsService userDetailsService;
private JwtService jwtService;
private X509CertificateValidator certificateValidator;
private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor;
private X509IdentityProvider certificateIdentityProvider;
private LoginIdentityProvider loginIdentityProvider;
public NiFiWebApiSecurityConfiguration() {
@ -113,7 +111,11 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
}
private NodeAuthorizedUserFilter buildNodeAuthorizedUserFilter() {
return new NodeAuthorizedUserFilter(properties);
final NodeAuthorizedUserFilter nodeFilter = new NodeAuthorizedUserFilter();
nodeFilter.setProperties(properties);
nodeFilter.setCertificateExtractor(certificateExtractor);
nodeFilter.setCertificateIdentityProvider(certificateIdentityProvider);
return nodeFilter;
}
private JwtAuthenticationFilter buildJwtFilter() throws Exception {
@ -127,9 +129,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
private X509AuthenticationFilter buildX509Filter() throws Exception {
final X509AuthenticationFilter x509Filter = new X509AuthenticationFilter();
x509Filter.setProperties(properties);
x509Filter.setPrincipalExtractor(principalExtractor);
x509Filter.setCertificateExtractor(certificateExtractor);
x509Filter.setCertificateValidator(certificateValidator);
x509Filter.setCertificateIdentityProvider(certificateIdentityProvider);
x509Filter.setAuthenticationManager(authenticationManager());
return x509Filter;
}
@ -165,18 +166,14 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
this.loginIdentityProvider = loginIdentityProvider;
}
@Autowired
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
@Autowired
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor;
}
@Autowired
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
this.principalExtractor = principalExtractor;
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
this.certificateIdentityProvider = certificateIdentityProvider;
}
}

View File

@ -30,8 +30,6 @@ import com.wordnik.swagger.annotations.ApiParam;
import com.wordnik.swagger.annotations.ApiResponse;
import com.wordnik.swagger.annotations.ApiResponses;
import java.net.URI;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
@ -41,6 +39,7 @@ import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
@ -48,8 +47,6 @@ import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.authentication.exception.IdentityAccessException;
import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
import org.apache.nifi.security.util.CertificateUtils;
import org.apache.nifi.util.StringUtils;
import static org.apache.nifi.web.api.ApplicationResource.CLIENT_ID;
import org.apache.nifi.web.api.dto.AccessStatusDTO;
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
import org.apache.nifi.web.api.dto.RevisionDTO;
@ -62,15 +59,15 @@ import org.apache.nifi.web.security.jwt.JwtService;
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
import org.apache.nifi.web.security.x509.X509CertificateValidator;
import org.apache.nifi.web.security.x509.X509IdentityProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
* RESTful endpoint for managing a cluster.
@ -82,13 +79,15 @@ import org.springframework.security.web.authentication.preauth.x509.X509Principa
)
public class AccessResource extends ApplicationResource {
private static final Logger logger = LoggerFactory.getLogger(AccessResource.class);
private static final String AUTHORIZATION = "Authorization";
private NiFiProperties properties;
private X509CertificateValidator certificateValidator;
private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor;
private LoginIdentityProvider loginIdentityProvider;
private X509CertificateExtractor certificateExtractor;
private X509IdentityProvider certificateIdentityProvider;
private JwtService jwtService;
private AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService;
@ -172,57 +171,64 @@ public class AccessResource extends ApplicationResource {
final AccessStatusDTO accessStatus = new AccessStatusDTO();
try {
// look for a certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest);
// if no certificate, just check the credentials
if (certificate == null) {
final String principal = jwtService.getAuthentication(httpServletRequest);
// if there is not certificate, consider a token
if (certificates == null) {
// look for an authorization token
final String authorization = httpServletRequest.getHeader(AUTHORIZATION);
// ensure we have something we can work with (certificate or crendentials)
if (principal == null) {
// if there is no authorization header, we don't know the user
if (authorization == null) {
accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
accessStatus.setMessage("No credentials supplied, unknown user.");
} else {
// set the user identity
accessStatus.setIdentity(principal);
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
// TODO - use this token with the JWT service
final String token = StringUtils.substringAfterLast(authorization, " ");
// without a certificate, this is not a proxied request
final List<String> chain = Arrays.asList(principal);
// TODO - do not call this method of the jwt service
final String principal = jwtService.getAuthentication(httpServletRequest);
// check authorization for this user
checkAuthorization(chain);
// TODO - catch jwt exception?
// ensure we have something we can work with (certificate or crendentials)
if (principal == null) {
throw new IllegalArgumentException("The specific token is not valid.");
} else {
// set the user identity
accessStatus.setIdentity(principal);
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
// no issues with authorization
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setStatus("Account is active and authorized");
// without a certificate, this is not a proxied request
final List<String> chain = Arrays.asList(principal);
// check authorization for this user
checkAuthorization(chain);
// no issues with authorization
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setMessage("Account is active and authorized");
}
}
} else {
// we have a certificate so let's consider a proxy chain
final String principal = principalExtractor.extractPrincipal(certificate).toString();
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
try {
// validate the certificate
certificateValidator.validateClientCertificate(httpServletRequest, certificate);
} catch (CertificateExpiredException cee) {
throw new IllegalArgumentException(String.format("Client certificate for (%s) is expired.", principal), cee);
} catch (CertificateNotYetValidException cnyve) {
throw new IllegalArgumentException(String.format("Client certificate for (%s) is not yet valid.", principal), cnyve);
} catch (final Exception e) {
throw new IllegalArgumentException(e.getMessage(), e);
// get the proxy chain and ensure its populated
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(httpServletRequest, authenticationResponse.getIdentity());
if (proxyChain.isEmpty()) {
logger.error(String.format("Unable to parse the proxy chain %s from the incoming request.", authenticationResponse.getIdentity()));
throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
}
// set the user identity
accessStatus.setIdentity(principal);
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
// ensure the proxy chain is authorized
checkAuthorization(ProxiedEntitiesUtils.buildProxyChain(httpServletRequest, principal));
checkAuthorization(proxyChain);
// set the user identity
accessStatus.setIdentity(proxyChain.get(0));
accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0)));
// no issues with authorization
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setStatus("Account is active and authorized");
accessStatus.setMessage("Account is active and authorized");
}
} catch (final UsernameNotFoundException unfe) {
accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name());
@ -277,6 +283,7 @@ public class AccessResource extends ApplicationResource {
@ApiResponses(
value = {
@ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@ApiResponse(code = 403, message = "Client is not authorized to make this request."),
@ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
}
)
@ -297,58 +304,42 @@ public class AccessResource extends ApplicationResource {
final LoginAuthenticationToken loginAuthenticationToken;
// if we don't have username/password, consider JWT or x509
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
// look for a certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest);
// if there is no certificate, look for an existing token
if (certificate == null) {
// if not configured for login, don't consider existing tokens
if (loginIdentityProvider == null) {
throw new IllegalStateException("Login not supported.");
}
// look for the principal
final String principal = jwtService.getAuthentication(httpServletRequest);
if (principal == null) {
throw new AuthenticationCredentialsNotFoundException("Unable to issue token as issue token as no credentials were found in the request.");
}
// create the authentication token
loginAuthenticationToken = new LoginAuthenticationToken(principal, loginIdentityProvider.getExpiration());
} else {
// extract the principal
final String principal = principalExtractor.extractPrincipal(certificate).toString();
try {
certificateValidator.validateClientCertificate(httpServletRequest, certificate);
} catch (CertificateExpiredException cee) {
throw new IllegalArgumentException(String.format("Client certificate for (%s) is expired.", principal), cee);
} catch (CertificateNotYetValidException cnyve) {
throw new IllegalArgumentException(String.format("Client certificate for (%s) is not yet valid.", principal), cnyve);
} catch (final Exception e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
// authorize the proxy if necessary
authorizeProxyIfNecessary(ProxiedEntitiesUtils.buildProxyChain(httpServletRequest, principal));
// create the authentication token
loginAuthenticationToken = new LoginAuthenticationToken(principal, loginIdentityProvider.getExpiration());
// if there is not certificate, consider login credentials
if (certificates == null) {
// ensure we have login credentials
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
throw new IllegalArgumentException("The username and password must be specified.");
}
} else {
try {
// attempt to authenticate
final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password));
// create the authentication token
loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getUsername(), loginIdentityProvider.getExpiration());
loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getUsername(), authenticationResponse.getExpiration());
} catch (final InvalidLoginCredentialsException ilce) {
throw new IllegalArgumentException("The supplied username and password are not valid.", ilce);
} catch (final IdentityAccessException iae) {
throw new AdministrationException(iae.getMessage(), iae);
}
} else {
// consider a certificate
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
// get the proxy chain and ensure its populated
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(httpServletRequest, authenticationResponse.getIdentity());
if (proxyChain.isEmpty()) {
logger.error(String.format("Unable to parse the proxy chain %s from the incoming request.", authenticationResponse.getIdentity()));
throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
}
// authorize the proxy if necessary
authorizeProxyIfNecessary(proxyChain);
// create the authentication token
loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration());
}
// generate JWT for response
@ -371,11 +362,14 @@ public class AccessResource extends ApplicationResource {
userDetailsService.loadUserDetails(new NiFiAuthenticationRequestToken(proxyChain));
} catch (final UsernameNotFoundException unfe) {
// if a username not found exception was thrown, the proxies were authorized and now
// we can issue a new ID token to the end user
// we can issue a new token to the end user which they will use to identify themselves
// when they enter a new account request
} catch (final AuthenticationServiceException ase) {
// throw an administration exception which will return a 500
throw new AdministrationException(ase.getMessage(), ase);
} catch (final Exception e) {
// any other issue we're going to treat as an authentication exception which will return 401
throw new AdministrationException(e.getMessage(), e) {
};
// any other issue we're going to treat as access denied exception which will return 403
throw new AccessDeniedException(e.getMessage(), e);
}
}
}
@ -393,16 +387,12 @@ public class AccessResource extends ApplicationResource {
this.jwtService = jwtService;
}
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor;
}
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
this.principalExtractor = principalExtractor;
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
this.certificateIdentityProvider = certificateIdentityProvider;
}
public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService) {

View File

@ -53,6 +53,8 @@ import org.apache.nifi.web.util.WebUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.web.security.user.NiFiUserUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
@ -363,9 +365,9 @@ public abstract class ApplicationResource {
if (httpServletRequest.isSecure()) {
// add the certificate DN to the proxy chain
final String xProxiedEntitiesChain = ProxiedEntitiesUtils.getXProxiedEntitiesChain(httpServletRequest);
if (StringUtils.isNotBlank(xProxiedEntitiesChain)) {
result.put(PROXIED_ENTITIES_CHAIN_HTTP_HEADER, xProxiedEntitiesChain);
final NiFiUser user = NiFiUserUtils.getNiFiUser();
if (user != null) {
result.put(PROXIED_ENTITIES_CHAIN_HTTP_HEADER, ProxiedEntitiesUtils.buildProxiedEntitiesChainString(user));
}
// add the user's authorities (if any) to the headers

View File

@ -823,7 +823,7 @@ public class ControllerFacade {
final Map<String, String> attributes = event.getAttributes();
// calculate the dn chain
final List<String> dnChain = ProxiedEntitiesUtils.getXProxiedEntitiesChain(user);
final List<String> dnChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(user);
// ensure the users in this chain are allowed to download this content
final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes);

View File

@ -243,9 +243,8 @@
</bean>
<bean id="accessResource" class="org.apache.nifi.web.api.AccessResource" scope="singleton">
<property name="properties" ref="nifiProperties"/>
<property name="certificateValidator" ref="certificateValidator"/>
<property name="certificateExtractor" ref="certificateExtractor"/>
<property name="principalExtractor" ref="principalExtractor"/>
<property name="certificateIdentityProvider" ref="certificateIdentityProvider"/>
<property name="loginIdentityProvider" ref="loginIdentityProvider"/>
<property name="jwtService" ref="jwtService"/>
<property name="userDetailsService" ref="userDetailsService"/>

View File

@ -0,0 +1,292 @@
/*
* 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.integration.accesscontrol;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.apache.commons.io.FileUtils;
import org.apache.nifi.integration.util.NiFiTestServer;
import org.apache.nifi.integration.util.NiFiTestUser;
import org.apache.nifi.integration.util.SourceTestProcessor;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.NarClassLoaders;
import org.apache.nifi.security.util.SslContextFactory;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
import org.apache.nifi.web.api.dto.AccessStatusDTO;
import org.apache.nifi.web.api.dto.ProcessorDTO;
import org.apache.nifi.web.api.dto.RevisionDTO;
import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
import org.apache.nifi.web.api.entity.AccessStatusEntity;
import org.apache.nifi.web.api.entity.ProcessorEntity;
import org.apache.nifi.web.util.WebUtils;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Access token endpoint test.
*/
public class AccessTokenEndpointTest {
private static final String CLIENT_ID = "token-endpoint-id";
private static final String CONTEXT_PATH = "/nifi-api";
private static final String FLOW_XML_PATH = "target/test-classes/access-control/flow-admin.xml";
private static NiFiTestServer SERVER;
private static NiFiTestUser TOKEN_USER;
private static String BASE_URL;
@BeforeClass
public static void setup() throws Exception {
// configure the location of the nifi properties
File nifiPropertiesFile = new File("src/test/resources/access-control/nifi.properties");
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, nifiPropertiesFile.getAbsolutePath());
// update the flow.xml property
NiFiProperties props = NiFiProperties.getInstance();
props.setProperty("nifi.flow.configuration.file", FLOW_XML_PATH);
// delete the database directory to avoid issues with re-registration in testRequestAccessUsingToken
FileUtils.deleteDirectory(props.getDatabaseRepositoryPath().toFile());
// load extensions
NarClassLoaders.load(props);
ExtensionManager.discoverExtensions();
// start the server
SERVER = new NiFiTestServer("src/main/webapp", CONTEXT_PATH);
SERVER.startServer();
SERVER.loadFlow();
// get the base url
BASE_URL = SERVER.getBaseUrl() + CONTEXT_PATH;
// create the user
final Client client = WebUtils.createClient(null, createTrustContext(props));
TOKEN_USER = new NiFiTestUser(client, null);
}
private static SSLContext createTrustContext(final NiFiProperties props) throws Exception {
return SslContextFactory.createTrustSslContext(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE),
props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD).toCharArray(),
props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE), "TLS");
}
// -----------
// LOGIN CONIG
// -----------
/**
* Test getting access configuration.
*
* @throws Exception ex
*/
@Test
public void testGetAccessConfig() throws Exception {
String url = BASE_URL + "/access/config";
ClientResponse response = TOKEN_USER.testGet(url);
// ensure the request is successful
Assert.assertEquals(200, response.getStatus());
// extract the process group
AccessConfigurationEntity accessConfigEntity = response.getEntity(AccessConfigurationEntity.class);
// ensure there is content
Assert.assertNotNull(accessConfigEntity);
// extract the process group dto
AccessConfigurationDTO accessConfig = accessConfigEntity.getConfig();
// verify config
Assert.assertTrue(accessConfig.getSupportsLogin());
Assert.assertFalse(accessConfig.getSupportsAnonymous());
}
/**
* Obtains a token and creates a processor using it.
*
* @throws Exception ex
*/
@Test
public void testCreateProcessorUsingToken() throws Exception {
String url = BASE_URL + "/access/token";
ClientResponse response = TOKEN_USER.testCreateToken(url, "user@nifi", "whateve");
// ensure the request is successful
Assert.assertEquals(201, response.getStatus());
// get the token
String token = response.getEntity(String.class);
// attempt to create a processor with it
createProcessor(token);
}
private ProcessorDTO createProcessor(final String token) throws Exception {
String url = BASE_URL + "/controller/process-groups/root/processors";
// authorization header
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + token);
// create the processor
ProcessorDTO processor = new ProcessorDTO();
processor.setName("Copy");
processor.setType(SourceTestProcessor.class.getName());
// create the revision
final RevisionDTO revision = new RevisionDTO();
revision.setClientId(CLIENT_ID);
revision.setVersion(NiFiTestUser.REVISION);
// create the entity body
ProcessorEntity entity = new ProcessorEntity();
entity.setRevision(revision);
entity.setProcessor(processor);
// perform the request
ClientResponse response = TOKEN_USER.testPostWithHeaders(url, entity, headers);
// ensure the request is successful
Assert.assertEquals(201, response.getStatus());
// get the entity body
entity = response.getEntity(ProcessorEntity.class);
// verify creation
processor = entity.getProcessor();
Assert.assertEquals("Copy", processor.getName());
Assert.assertEquals("org.apache.nifi.integration.util.SourceTestProcessor", processor.getType());
return processor;
}
/**
* Verifies the response when bad credentials are specified.
*
* @throws Exception ex
*/
@Test
public void testInvalidCredentials() throws Exception {
String url = BASE_URL + "/access/token";
ClientResponse response = TOKEN_USER.testCreateToken(url, "user@nifi", "not a real password");
// ensure the request is successful
Assert.assertEquals(400, response.getStatus());
}
/**
* Verifies the response when the user is known.
*
* @throws Exception ex
*/
@Test
public void testUnkownUser() throws Exception {
String url = BASE_URL + "/access/token";
ClientResponse response = TOKEN_USER.testCreateToken(url, "not a real user", "not a real password");
// ensure the request is successful
Assert.assertEquals(400, response.getStatus());
}
/**
* Request access using access token.
*
* @throws Exception ex
*/
@Test
public void testRequestAccessUsingToken() throws Exception {
String accessStatusUrl = BASE_URL + "/access";
String accessTokenUrl = BASE_URL + "/access/token";
String registrationUrl = BASE_URL + "/controller/users";
ClientResponse response = TOKEN_USER.testGet(accessStatusUrl);
// ensure the request is successful
Assert.assertEquals(200, response.getStatus());
AccessStatusEntity accessStatusEntity = response.getEntity(AccessStatusEntity.class);
AccessStatusDTO accessStatus = accessStatusEntity.getAccessStatus();
// verify unknown
Assert.assertEquals("UNKNOWN", accessStatus.getStatus());
response = TOKEN_USER.testCreateToken(accessTokenUrl, "unregistered-user@nifi", "password");
// ensure the request is successful
Assert.assertEquals(201, response.getStatus());
// get the token
String token = response.getEntity(String.class);
// authorization header
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + token);
// check the status with the token
response = TOKEN_USER.testGetWithHeaders(accessStatusUrl, null, headers);
// ensure the request is successful
Assert.assertEquals(200, response.getStatus());
accessStatusEntity = response.getEntity(AccessStatusEntity.class);
accessStatus = accessStatusEntity.getAccessStatus();
// verify unregistered
Assert.assertEquals("UNREGISTERED", accessStatus.getStatus());
response = TOKEN_USER.testRegisterUser(registrationUrl, "Gimme access", headers);
// ensure the request is successful
Assert.assertEquals(201, response.getStatus());
// check the status with the token
response = TOKEN_USER.testGetWithHeaders(accessStatusUrl, null, headers);
// ensure the request is successful
Assert.assertEquals(200, response.getStatus());
accessStatusEntity = response.getEntity(AccessStatusEntity.class);
accessStatus = accessStatusEntity.getAccessStatus();
// verify unregistered
Assert.assertEquals("NOT_ACTIVE", accessStatus.getStatus());
}
@AfterClass
public static void cleanup() throws Exception {
// shutdown the server
SERVER.shutdownServer();
SERVER = null;
// look for the flow.xml
File flow = new File(FLOW_XML_PATH);
if (flow.exists()) {
flow.delete();
}
}
}

View File

@ -37,7 +37,7 @@ import org.apache.nifi.authorization.DownloadAuthorization;
*/
public class NiFiTestAuthorizationProvider implements AuthorityProvider {
private Map<String, Set<Authority>> users;
private final Map<String, Set<Authority>> users;
/**
* Creates a new FileAuthorizationProvider.
@ -48,6 +48,7 @@ public class NiFiTestAuthorizationProvider implements AuthorityProvider {
users.put("CN=Lastname Firstname Middlename monitor, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown", EnumSet.of(Authority.ROLE_MONITOR));
users.put("CN=Lastname Firstname Middlename dfm, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown", EnumSet.of(Authority.ROLE_DFM));
users.put("CN=Lastname Firstname Middlename admin, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown", EnumSet.of(Authority.ROLE_ADMIN));
users.put("user@nifi", EnumSet.of(Authority.ROLE_DFM));
}
@Override

View File

@ -0,0 +1,75 @@
/*
* 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.integration.util;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.authorization.exception.ProviderCreationException;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext;
import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext;
import org.apache.nifi.authentication.exception.IdentityAccessException;
import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
/**
*
*/
public class NiFiTestLoginIdentityProvider implements LoginIdentityProvider {
private final Map<String, String> users;
/**
* Creates a new FileAuthorizationProvider.
*/
public NiFiTestLoginIdentityProvider() {
users = new HashMap<>();
users.put("user@nifi", "whateve");
users.put("unregistered-user@nifi", "password");
}
private void checkUser(final String user, final String password) {
if (!users.containsKey(user)) {
throw new InvalidLoginCredentialsException("Unknown user");
}
if (!users.get(user).equals(password)) {
throw new InvalidLoginCredentialsException("Invalid password");
}
}
@Override
public AuthenticationResponse authenticate(LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException {
checkUser(credentials.getUsername(), credentials.getPassword());
return new AuthenticationResponse(credentials.getUsername(), credentials.getUsername(), TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS));
}
@Override
public void initialize(LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
}
@Override
public void onConfigured(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
}
@Override
public void preDestruction() {
}
}

View File

@ -78,8 +78,12 @@ public class NiFiTestServer {
private void createSecureConnector() {
org.eclipse.jetty.util.ssl.SslContextFactory contextFactory = new org.eclipse.jetty.util.ssl.SslContextFactory();
// need client auth
contextFactory.setNeedClientAuth(properties.getNeedClientAuth());
// require client auth when not supporting login or anonymous access
if (StringUtils.isBlank(properties.getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER)) && properties.getAnonymousAuthorities().isEmpty()) {
contextFactory.setNeedClientAuth(true);
} else {
contextFactory.setWantClientAuth(true);
}
/* below code sets JSSE system properties when values are provided */
// keystore properties
@ -163,7 +167,6 @@ public class NiFiTestServer {
}
public Client getClient() {
// create the client
return WebUtils.createClient(null, SslContextFactory.createSslContext(properties));
}

View File

@ -34,9 +34,27 @@ public class NiFiTestUser {
private final Client client;
private final String proxyDn;
public NiFiTestUser(Client client, String dn) {
public NiFiTestUser(Client client, String proxyDn) {
this.client = client;
this.proxyDn = ProxiedEntitiesUtils.formatProxyDn(dn);
if (proxyDn != null) {
this.proxyDn = ProxiedEntitiesUtils.formatProxyDn(proxyDn);
} else {
this.proxyDn = null;
}
}
/**
* Conditionally adds the proxied entities chain.
*
* @param builder the resource builder
* @return the resource builder
*/
private WebResource.Builder addProxiedEntities(final WebResource.Builder builder) {
if (proxyDn == null) {
return builder;
} else {
return builder.header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
}
}
/**
@ -58,6 +76,18 @@ public class NiFiTestUser {
* @return response
*/
public ClientResponse testGet(String url, Map<String, String> queryParams) {
return testGetWithHeaders(url, queryParams, null);
}
/**
* Performs a GET using the specified url and query parameters.
*
* @param url url
* @param queryParams params
* @param headers http headers
* @return response
*/
public ClientResponse testGetWithHeaders(String url, Map<String, String> queryParams, Map<String, String> headers) {
// get the resource
WebResource resource = client.resource(url);
@ -68,8 +98,18 @@ public class NiFiTestUser {
}
}
// get the builder
WebResource.Builder builder = addProxiedEntities(resource.accept(MediaType.APPLICATION_JSON));
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
builder = builder.header(key, headers.get(key));
}
}
// perform the query
return resource.accept(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn).get(ClientResponse.class);
return builder.get(ClientResponse.class);
}
/**
@ -92,14 +132,34 @@ public class NiFiTestUser {
* @throws Exception ex
*/
public ClientResponse testPost(String url, Object entity) throws Exception {
return testPostWithHeaders(url, entity, null);
}
/**
* Performs a POST using the specified url and entity body.
*
* @param url url
* @param entity entity
* @param headers http headers
* @return response
* @throws Exception ex
*/
public ClientResponse testPostWithHeaders(String url, Object entity, Map<String, String> headers) throws Exception {
// get the resource
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON));
// include the request entity
if (entity != null) {
resourceBuilder = resourceBuilder.entity(entity);
}
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
}
// perform the request
return resourceBuilder.post(ClientResponse.class);
}
@ -109,18 +169,38 @@ public class NiFiTestUser {
*
* @param url url
* @param entity entity
* @return repsonse
* @return response
* @throws Exception ex
*/
public ClientResponse testPostMultiPart(String url, Object entity) throws Exception {
return testPostMultiPartWithHeaders(url, entity, null);
}
/**
* Performs a POST using the specified url and entity body.
*
* @param url url
* @param entity entity
* @param headers http headers
* @return response
* @throws Exception ex
*/
public ClientResponse testPostMultiPartWithHeaders(String url, Object entity, Map<String, String> headers) throws Exception {
// get the resource
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_XML).type(MediaType.MULTIPART_FORM_DATA).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_XML).type(MediaType.MULTIPART_FORM_DATA));
// include the request entity
if (entity != null) {
resourceBuilder = resourceBuilder.entity(entity);
}
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
}
// perform the request
return resourceBuilder.post(ClientResponse.class);
}
@ -134,6 +214,19 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex
*/
public ClientResponse testPost(String url, Map<String, String> formData) throws Exception {
return testPostWithHeaders(url, formData, null);
}
/**
* Performs a POST using the specified url and form data.
*
* @param url url
* @param formData form data
* @param headers http headers
* @return response
* @throws java.lang.Exception ex
*/
public ClientResponse testPostWithHeaders(String url, Map<String, String> formData, Map<String, String> headers) throws Exception {
// convert the form data
MultivaluedMapImpl entity = new MultivaluedMapImpl();
for (String key : formData.keySet()) {
@ -141,14 +234,20 @@ public class NiFiTestUser {
}
// get the resource
WebResource.Builder resourceBuilder
= client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED));
// add the form data if necessary
if (!entity.isEmpty()) {
resourceBuilder = resourceBuilder.entity(entity);
}
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
}
// perform the request
return resourceBuilder.post(ClientResponse.class);
}
@ -162,14 +261,34 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex
*/
public ClientResponse testPut(String url, Object entity) throws Exception {
return testPutWithHeaders(url, entity, null);
}
/**
* Performs a PUT using the specified url and entity body.
*
* @param url url
* @param entity entity
* @param headers http headers
* @return response
* @throws java.lang.Exception ex
*/
public ClientResponse testPutWithHeaders(String url, Object entity, Map<String, String> headers) throws Exception {
// get the resource
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON));
// include the request entity
if (entity != null) {
resourceBuilder = resourceBuilder.entity(entity);
}
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
}
// perform the request
return resourceBuilder.put(ClientResponse.class);
}
@ -183,6 +302,19 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex
*/
public ClientResponse testPut(String url, Map<String, String> formData) throws Exception {
return testPutWithHeaders(url, formData, null);
}
/**
* Performs a PUT using the specified url and form data.
*
* @param url url
* @param formData form data
* @param headers http headers
* @return response
* @throws java.lang.Exception ex
*/
public ClientResponse testPutWithHeaders(String url, Map<String, String> formData, Map<String, String> headers) throws Exception {
// convert the form data
MultivaluedMapImpl entity = new MultivaluedMapImpl();
for (String key : formData.keySet()) {
@ -190,14 +322,20 @@ public class NiFiTestUser {
}
// get the resource
WebResource.Builder resourceBuilder
= client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED));
// add the form data if necessary
if (!entity.isEmpty()) {
resourceBuilder = resourceBuilder.entity(entity);
}
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
}
// perform the request
return resourceBuilder.put(ClientResponse.class);
}
@ -210,24 +348,26 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex
*/
public ClientResponse testDelete(String url) throws Exception {
return testDelete(url, (Object) null);
return testDelete(url, null);
}
/**
* Performs a DELETE using the specified url and entity.
*
* @param url url
* @param entity entity
* @return repsonse
* @param headers http headers
* @return response
* @throws java.lang.Exception ex
*/
public ClientResponse testDelete(String url, Object entity) throws Exception {
public ClientResponse testDeleteWithHeaders(String url, Map<String, String> headers) throws Exception {
// get the resource
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON));
// append any query parameters
if (entity != null) {
resourceBuilder = resourceBuilder.entity(entity);
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
}
// perform the query
@ -254,7 +394,56 @@ public class NiFiTestUser {
}
// perform the request
return resource.accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn).delete(ClientResponse.class);
return addProxiedEntities(resource.accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED)).delete(ClientResponse.class);
}
/**
* Attempts to create a token with the specified username and password.
*
* @param url the url
* @param username the username
* @param password the password
* @return response
* @throws Exception ex
*/
public ClientResponse testCreateToken(String url, String username, String password) throws Exception {
// convert the form data
MultivaluedMapImpl entity = new MultivaluedMapImpl();
entity.add("username", username);
entity.add("password", password);
// get the resource
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.TEXT_PLAIN).type(MediaType.APPLICATION_FORM_URLENCODED)).entity(entity);
// perform the request
return resourceBuilder.post(ClientResponse.class);
}
/**
* Attempts to create a token with the specified username and password.
*
* @param url the url
* @param justification justification
* @param headers http headers
* @return response
* @throws Exception ex
*/
public ClientResponse testRegisterUser(String url, String justification, Map<String, String> headers) throws Exception {
// convert the form data
MultivaluedMapImpl entity = new MultivaluedMapImpl();
entity.add("justification", justification);
// get the resource
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.TEXT_PLAIN).type(MediaType.APPLICATION_FORM_URLENCODED)).entity(entity);
// append any headers
if (headers != null && !headers.isEmpty()) {
for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
}
// perform the request
return resourceBuilder.post(ClientResponse.class);
}
}

View File

@ -0,0 +1,15 @@
# 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.
org.apache.nifi.integration.util.NiFiTestLoginIdentityProvider

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
@ -13,6 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<services>
</services>
<!--
This file lists all authority providers to use when running securely.
-->
<loginIdentityProviders>
<provider>
<identifier>test-provider</identifier>
<class>org.apache.nifi.integration.util.NiFiTestLoginIdentityProvider</class>
</provider>
</loginIdentityProviders>

View File

@ -14,16 +14,15 @@
# limitations under the License.
# Core Properties #
nifi.version=nifi 0.2.1-SNAPSHOT
nifi.version=nifi version
nifi.flow.configuration.file=
nifi.flow.configuration.archive.dir=target/archive
nifi.flowcontroller.autoResumeState=true
nifi.flowcontroller.graceful.shutdown.period=10 sec
nifi.flowservice.writedelay.interval=2 sec
nifi.reporting.task.configuration.file=target/test-classes/access-control/reporting-tasks.xml
nifi.controller.service.configuration.file=target/test-classes/access-control/controller-services.xml
nifi.authority.provider.configuration.file=target/test-classes/access-control/authority-providers.xml
nifi.login.identity.provider.configuration.file=target/test-classes/access-control/login-identity-providers.xml
nifi.templates.directory=target/test-classes/access-control/templates
nifi.ui.banner.text=TEST BANNER
nifi.ui.autorefresh.interval=30 sec
@ -93,11 +92,11 @@ nifi.security.truststoreType=JKS
nifi.security.truststorePasswd=localtest
nifi.security.needClientAuth=true
nifi.security.user.authority.provider=test-provider
nifi.security.user.login.identity.provider=
nifi.security.user.login.identity.provider=test-provider
nifi.security.authorizedUsers.file=target/test-classes/access-control/users.xml
nifi.security.user.credential.cache.duration=1 hr
nifi.security.support.new.account.requests=
nifi.security.default.user.roles=
nifi.security.anonymous.authorities=
# cluster common properties (cluster manager and nodes must have same values) #
nifi.cluster.protocol.heartbeat.interval=5 sec

View File

@ -1,17 +0,0 @@
<?xml version="1.0"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<tasks>
</tasks>

View File

@ -29,12 +29,14 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.user.NiFiUserUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
@ -85,8 +87,13 @@ public abstract class NiFiAuthenticationFilter implements Filter {
private void authenticate(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
try {
final Authentication authenticated = attemptAuthentication(request, response);
final NiFiAuthenticationRequestToken authenticated = attemptAuthentication(request, response);
if (authenticated != null) {
// log the request attempt - response details will be logged later
logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)",
ProxiedEntitiesUtils.formatProxyDn(StringUtils.join(authenticated.getChain(), "><")), request.getMethod(),
request.getRequestURL().toString(), request.getRemoteAddr()));
final Authentication authorized = authenticationManager.authenticate(authenticated);
successfulAuthorization(request, response, authorized);
}
@ -97,7 +104,7 @@ public abstract class NiFiAuthenticationFilter implements Filter {
}
}
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response);
public abstract NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response);
protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
if (logger.isDebugEnabled()) {
@ -127,6 +134,9 @@ public abstract class NiFiAuthenticationFilter implements Filter {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println("Access is denied.");
}
} else if (ae instanceof BadCredentialsException) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
out.println(ae.getMessage());
} else if (ae instanceof AccountStatusException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println(ae.getMessage());

View File

@ -47,10 +47,12 @@ public class NiFiAuthenticationProvider implements AuthenticationProvider {
final UserDetails userDetails = userDetailsService.loadUserDetails(request);
// build an authentication for accesing nifi
return new NiFiAuthorizationToken(userDetails);
final NiFiAuthorizationToken result = new NiFiAuthorizationToken(userDetails);
result.setDetails(request.getDetails());
return result;
} catch (final UsernameNotFoundException unfe) {
// if the result was an authenticated new account request and it could not be authorized because the user was not found,
// return the token so the new account could be created. this must go here to ensure that any proxies have been authorized
// if the authentication request is for a new account and it could not be authorized because the user was not found,
// return the token so the new account could be created. this must go here toe nsure that any proxies have been authorized
if (isNewAccountAuthenticationToken(request)) {
return new NewAccountAuthenticationToken(((NewAccountAuthenticationRequestToken) authentication).getNewAccountRequest());
} else {

View File

@ -16,15 +16,12 @@
*/
package org.apache.nifi.web.security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.nifi.web.security.x509.SubjectDnX509PrincipalExtractor;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.user.NiFiUser;
import org.springframework.security.core.Authentication;
@ -42,33 +39,50 @@ public class ProxiedEntitiesUtils {
private static final Pattern proxyChainPattern = Pattern.compile("<(.*?)>");
/**
* @param request http request
* @return the X-ProxiedEntitiesChain from the specified request
* Formats the specified DN to be set as a HTTP header using well known conventions.
*
* @param dn raw dn
* @return the dn formatted as an HTTP header
*/
public static String getXProxiedEntitiesChain(final HttpServletRequest request) {
String xProxiedEntitiesChain = request.getHeader("X-ProxiedEntitiesChain");
final X509Certificate cert = new X509CertificateExtractor().extractClientCertificate(request);
if (cert != null) {
final SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
final String extractedPrincipal = principalExtractor.extractPrincipal(cert).toString();
final String formattedPrincipal = formatProxyDn(extractedPrincipal);
if (StringUtils.isBlank(xProxiedEntitiesChain)) {
xProxiedEntitiesChain = formattedPrincipal;
} else {
xProxiedEntitiesChain += formattedPrincipal;
}
}
return xProxiedEntitiesChain;
public static String formatProxyDn(String dn) {
return "<" + dn + ">";
}
/**
* Builds the dn chain for the specified user.
* Tokenizes the specified proxy chain.
*
* @param rawProxyChain raw chain
* @return tokenized proxy chain
*/
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));
}
return proxyChain;
}
/**
* Builds the proxy chain for the specified user.
*
* @param user The current user
* @return The dn chain for that user
* @return The proxy chain for that user in String form
*/
public static List<String> getXProxiedEntitiesChain(final NiFiUser user) {
public static String buildProxiedEntitiesChainString(final NiFiUser user) {
// calculate the dn chain
final List<String> proxyChain = buildProxiedEntitiesChain(user);
return formatProxyDn(StringUtils.join(proxyChain, "><"));
}
/**
* Builds the proxy chain for the specified user.
*
* @param user The current user
* @return The proxy chain for that user in List form
*/
public static List<String> buildProxiedEntitiesChain(final NiFiUser user) {
// calculate the dn chain
final List<String> proxyChain = new ArrayList<>();
@ -86,56 +100,25 @@ public class ProxiedEntitiesUtils {
}
/**
* Formats the specified DN to be set as a HTTP header using well known conventions.
* Builds the proxy chain from the specified request and user.
*
* @param dn raw dn
* @return the dn formatted as an HTTP header
* @param request the request
* @param username the username
* @return the proxy chain in list form
*/
public static String formatProxyDn(String dn) {
return "<" + dn + ">";
public static List<String> buildProxiedEntitiesChain(final HttpServletRequest request, final String username) {
final String chain = buildProxiedEntitiesChainString(request, username);
return tokenizeProxiedEntitiesChain(chain);
}
// /**
// * Tokenizes the specified proxy chain.
// *
// * @param rawProxyChain raw chain
// * @return tokenized proxy chain
// */
// public static Deque<String> tokenizeProxyChain(String rawProxyChain) {
// final Deque<String> dnList = new ArrayDeque<>();
//
// // parse the proxy chain
// final Matcher rawProxyChainMatcher = proxyChainPattern.matcher(rawProxyChain);
// while (rawProxyChainMatcher.find()) {
// dnList.push(rawProxyChainMatcher.group(1));
// }
//
// return dnList;
// }
public static List<String> buildProxyChain(final HttpServletRequest request, final String username) {
String principal;
if (username.startsWith("<") && username.endsWith(">")) {
principal = username;
} else {
principal = formatProxyDn(username);
}
// look for a proxied user
if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
principal = request.getHeader(PROXY_ENTITIES_CHAIN) + principal;
}
// parse the proxy chain
final List<String> proxyChain = new ArrayList<>();
final Matcher rawProxyChainMatcher = proxyChainPattern.matcher(principal);
while (rawProxyChainMatcher.find()) {
proxyChain.add(rawProxyChainMatcher.group(1));
}
return proxyChain;
}
public static String extractProxiedEntitiesChain(final HttpServletRequest request, final String username) {
/**
* 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;

View File

@ -54,10 +54,8 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
/**
* Loads the user details for the specified dn.
*
* Synchronizing because we want each request to be authorized atomically
* since each may contain any number of DNs. We wanted an access decision
* made for each individual request as a whole (without other request
* potentially impacting it).
* Synchronizing because we want each request to be authorized atomically since each may contain any number of DNs. We wanted an access decision made for each individual request as a whole
* (without other request potentially impacting it).
*
* @param request request
* @return user details
@ -109,9 +107,6 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
// attempt to create a new user account for the proxying client
userService.createPendingUserAccount(dn, "Automatic account request generated for unknown proxy.");
// propagate the exception to return the appropriate response
throw new UsernameNotFoundException(String.format("An account request was generated for the proxy '%s'.", dn));
} catch (AdministrationException ae) {
throw new AuthenticationServiceException(String.format("Unable to create an account request for '%s': %s", dn, ae.getMessage()), ae);
} catch (IllegalArgumentException iae) {
@ -122,10 +117,10 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
throw new AccountStatusException(message) {
};
}
} else {
logger.warn(String.format("Untrusted proxy '%s' must be authorized with '%s' authority: %s", dn, Authority.ROLE_PROXY.toString(), unfe.getMessage()));
throw new UntrustedProxyException(String.format("Untrusted proxy '%s' must be authorized with '%s'.", dn, Authority.ROLE_PROXY.toString()));
}
logger.warn(String.format("Untrusted proxy '%s' must be authorized with '%s' authority: %s", dn, Authority.ROLE_PROXY.toString(), unfe.getMessage()));
throw new UntrustedProxyException(String.format("Untrusted proxy '%s' must be authorized with '%s'.", dn, Authority.ROLE_PROXY.toString()));
} catch (AuthenticationException ae) {
logger.warn(String.format("Untrusted proxy '%s' must be authorized with '%s' authority: %s", dn, Authority.ROLE_PROXY.toString(), ae.getMessage()));
throw new UntrustedProxyException(String.format("Untrusted proxy '%s' must be authorized with '%s'.", dn, Authority.ROLE_PROXY.toString()));

View File

@ -25,7 +25,6 @@ import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.user.NewAccountRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
/**
*/
@ -36,7 +35,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
private JwtService jwtService;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
public NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// only suppport jwt login when running securely
if (!request.isSecure()) {
return null;

View File

@ -82,7 +82,7 @@ public class JwtService {
/**
* Generates a signed JWT token from the provided (Spring Security) login authentication token.
*
* @param authenticationToken
* @param authenticationToken the authentication token
* @return a signed JWT containing the user identity and the identity provider, Base64-encoded
*/
public String generateSignedToken(final LoginAuthenticationToken authenticationToken) {

View File

@ -26,34 +26,26 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.nifi.controller.FlowController;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.web.security.user.NiFiUserDetails;
import org.apache.nifi.web.security.x509.SubjectDnX509PrincipalExtractor;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.token.NiFiAuthorizationToken;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
import org.apache.nifi.web.security.x509.X509IdentityProvider;
import org.apache.nifi.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.GenericFilterBean;
/**
* Custom filter to extract a user's authorities from the request where the user
* was authenticated by the cluster manager and populate the threadlocal with
* the authorized user. If the request contains the appropriate header with
* authorities and the application instance is a node connected to the cluster,
* then the authentication/authorization steps remaining in the filter chain are
* skipped.
* Custom filter to extract a user's authorities from the request where the user was authenticated by the cluster manager and populate the threadlocal with the authorized user. If the request contains
* the appropriate header with authorities and the application instance is a node connected to the cluster, then the authentication/authorization steps remaining in the filter chain are skipped.
*
* Checking if the application instance is a connected node is important because
* it prevents external clients from faking the request headers and bypassing
* the authentication processing chain.
* Checking if the application instance is a connected node is important because it prevents external clients from faking the request headers and bypassing the authentication processing chain.
*/
public class NodeAuthorizedUserFilter extends GenericFilterBean {
@ -61,14 +53,9 @@ public class NodeAuthorizedUserFilter extends GenericFilterBean {
public static final String PROXY_USER_DETAILS = "X-ProxiedEntityUserDetails";
private final NiFiProperties properties;
private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private final X509CertificateExtractor certificateExtractor = new X509CertificateExtractor();
private final X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
public NodeAuthorizedUserFilter(NiFiProperties properties) {
this.properties = properties;
}
private NiFiProperties properties;
private X509CertificateExtractor certificateExtractor;
private X509IdentityProvider certificateIdentityProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
@ -87,16 +74,15 @@ public class NodeAuthorizedUserFilter extends GenericFilterBean {
// check that we are connected to the cluster
if (flowController.getNodeId() != null) {
try {
// get the DN from the cert in the request
final X509Certificate certificate = certificateExtractor.extractClientCertificate((HttpServletRequest) request);
// attempt to extract the client certificate
final X509Certificate[] certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
if (certificate != null) {
// extract the principal from the certificate
final Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
final String dn = certificatePrincipal.toString();
// authenticate the certificate
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificate);
// only consider the pre-authorized user when the request came from the NCM according to the DN in the certificate
final String clusterManagerDN = flowController.getClusterManagerDN();
if (clusterManagerDN != null && clusterManagerDN.equals(dn)) {
// only consider the pre-authorized user when the request came directly from the NCM according to the DN in the certificate
final String clusterManagerIdentity = flowController.getClusterManagerDN();
if (clusterManagerIdentity != null && clusterManagerIdentity.equals(authenticationResponse.getIdentity())) {
// deserialize hex encoded object
final Serializable userDetailsObj = WebUtils.deserializeHexToObject(hexEncodedUserDetails);
@ -109,19 +95,33 @@ public class NodeAuthorizedUserFilter extends GenericFilterBean {
logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", user.getIdentity(), httpServletRequest.getMethod(),
httpServletRequest.getRequestURL().toString(), request.getRemoteAddr()));
// we do not create the authentication token with the X509 certificate because the certificate is from the sending system, not the proxied user
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities());
token.setDetails(authenticationDetailsSource.buildDetails(request));
// create the authorized nifi token
final NiFiAuthorizationToken token = new NiFiAuthorizationToken(userDetails);
SecurityContextHolder.getContext().setAuthentication(token);
}
}
}
} catch (final ClassNotFoundException cnfe) {
LOGGER.warn("Classpath issue detected because failed to deserialize authorized user in request header due to: " + cnfe, cnfe);
} catch (final IllegalArgumentException iae) {
// unable to authenticate a serialized user from the incoming request
}
}
}
chain.doFilter(request, response);
}
public void setProperties(NiFiProperties properties) {
this.properties = properties;
}
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
this.certificateIdentityProvider = certificateIdentityProvider;
}
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor;
}
}

View File

@ -266,13 +266,6 @@ public class LoginIdentityProviderFactoryBean implements FactoryBean, Disposable
}
}
@Override
public long getExpiration() {
try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) {
return baseProvider.getExpiration();
}
}
@Override
public void initialize(LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) {

View File

@ -16,12 +16,11 @@
*/
package org.apache.nifi.web.security.x509;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken;
@ -29,8 +28,7 @@ import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.user.NewAccountRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.authentication.BadCredentialsException;
/**
* Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate.
@ -39,54 +37,31 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(X509AuthenticationFilter.class);
private X509PrincipalExtractor principalExtractor;
private X509CertificateExtractor certificateExtractor;
private X509CertificateValidator certificateValidator;
private X509IdentityProvider certificateIdentityProvider;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
public NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// only suppport x509 login when running securely
if (!request.isSecure()) {
return null;
}
// extract the cert
X509Certificate certificate = certificateExtractor.extractClientCertificate(request);
// ensure the cert was found
if (certificate == null) {
// look for a client certificate
final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(request);
if (certificates == null) {
return null;
}
// extract the principal
Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
final String principal = ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
// attempt to authenticate if certificates were found
final AuthenticationResponse authenticationResponse;
try {
certificateValidator.validateClientCertificate(request, certificate);
} catch (CertificateExpiredException cee) {
final String message = String.format("Client certificate for (%s) is expired.", principal);
logger.info(message, cee);
if (logger.isDebugEnabled()) {
logger.debug("", cee);
}
return null;
} catch (CertificateNotYetValidException cnyve) {
final String message = String.format("Client certificate for (%s) is not yet valid.", principal);
logger.info(message, cnyve);
if (logger.isDebugEnabled()) {
logger.debug("", cnyve);
}
return null;
} catch (final Exception e) {
logger.info(e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("", e);
}
return null;
authenticationResponse = certificateIdentityProvider.authenticate(certificates);
} catch (final IllegalArgumentException iae) {
throw new BadCredentialsException(iae.getMessage(), iae);
}
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxyChain(request, principal);
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(request, authenticationResponse.getIdentity());
if (isNewAccountRequest(request)) {
return new NewAccountAuthenticationRequestToken(new NewAccountRequest(proxyChain, getJustification(request)));
} else {
@ -95,16 +70,12 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
}
/* setters */
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
this.principalExtractor = principalExtractor;
}
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor;
}
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
this.certificateIdentityProvider = certificateIdentityProvider;
}
}

View File

@ -1,317 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.x509;
import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.admin.service.UserService;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.UntrustedProxyException;
import org.apache.nifi.util.NiFiProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
* Custom X509 filter that will inspect the HTTP headers for a proxied user
* before extracting the user details from the client certificate.
*/
public class X509AuthenticationFilterOld extends AbstractPreAuthenticatedProcessingFilter {
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 final X509CertificateExtractor certificateExtractor = new X509CertificateExtractor();
private final X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
private OcspCertificateValidator certificateValidator;
private NiFiProperties properties;
private UserService userService;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final HttpServletResponse httpResponse = (HttpServletResponse) response;
// determine if this request is attempting to create a new account
if (isNewAccountRequest((HttpServletRequest) request)) {
// determine if this nifi supports new account requests
if (properties.getSupportNewAccountRequests()) {
// ensure there is a certificate in the request
X509Certificate certificate = certificateExtractor.extractClientCertificate((HttpServletRequest) request);
if (certificate != null) {
// extract the principal from the certificate
Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
String principal = certificatePrincipal.toString();
// log the new user account request
logger.info("Requesting new user account for " + principal);
try {
// get the justification
String justification = request.getParameter("justification");
if (justification == null) {
justification = StringUtils.EMPTY;
}
// create the pending user account
userService.createPendingUserAccount(principal, justification);
// generate a response
httpResponse.setStatus(HttpServletResponse.SC_CREATED);
httpResponse.setContentType("text/plain");
// write the response message
PrintWriter out = response.getWriter();
out.println("Not authorized. User account created. Authorization pending.");
} catch (IllegalArgumentException iae) {
handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_BAD_REQUEST, iae.getMessage());
} catch (AdministrationException ae) {
handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ae.getMessage());
}
} else {
// can this really happen?
handleMissingCertificate((HttpServletRequest) request, httpResponse);
}
} else {
handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_NOT_FOUND, "This NiFi does not support new account requests.");
}
} else {
try {
// this not a request to create a user account - try to authorize
super.doFilter(request, response, chain);
} catch (AuthenticationException ae) {
// continue the filter chain since anonymous access should be supported
if (!properties.getNeedClientAuth()) {
chain.doFilter(request, response);
} else {
// create an appropriate response for the given exception
handleUnsuccessfulAuthentication((HttpServletRequest) request, httpResponse, ae);
}
}
}
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String principal;
// extract the cert
X509Certificate certificate = certificateExtractor.extractClientCertificate(request);
// ensure the cert was found
if (certificate == null) {
return null;
}
// extract the principal
Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
principal = ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
try {
// ensure the cert is valid
certificate.checkValidity();
} catch (CertificateExpiredException cee) {
final String message = String.format("Client certificate for (%s) is expired.", principal);
logger.info(message, cee);
if (logger.isDebugEnabled()) {
logger.debug("", cee);
}
return null;
} catch (CertificateNotYetValidException cnyve) {
final String message = String.format("Client certificate for (%s) is not yet valid.", principal);
logger.info(message, cnyve);
if (logger.isDebugEnabled()) {
logger.debug("", cnyve);
}
return null;
}
// validate the certificate in question
try {
certificateValidator.validate(request);
} catch (final Exception e) {
logger.info(e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("", e);
}
return null;
}
// look for a proxied user
if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
principal = request.getHeader(PROXY_ENTITIES_CHAIN) + principal;
}
// log the request attempt - response details will be logged later
logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", principal, request.getMethod(),
request.getRequestURL().toString(), request.getRemoteAddr()));
return principal;
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return certificateExtractor.extractClientCertificate(request);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
response.setHeader(PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString());
}
super.successfulAuthentication(request, response, authResult);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage());
}
super.unsuccessfulAuthentication(request, response, failed);
}
/**
* Determines if the specified request is attempting to register a new user
* account.
*
* @param request http request
* @return true if new user
*/
private boolean isNewAccountRequest(HttpServletRequest request) {
if ("POST".equalsIgnoreCase(request.getMethod())) {
String path = request.getPathInfo();
if (StringUtils.isNotBlank(path)) {
if ("/controller/users".equals(path)) {
return true;
}
}
}
return false;
}
/**
* Handles requests that were unable to be authorized.
*
* @param request request
* @param response response
* @param ae ex
* @throws IOException ex
*/
private void handleUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException {
// set the response status
response.setContentType("text/plain");
// write the response message
PrintWriter out = response.getWriter();
// use the type of authentication exception to determine the response code
if (ae instanceof UsernameNotFoundException) {
if (properties.getSupportNewAccountRequests()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
out.println("Not authorized.");
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println("Access is denied.");
}
} else if (ae instanceof AccountStatusException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println(ae.getMessage());
} else if (ae instanceof UntrustedProxyException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println(ae.getMessage());
} else if (ae instanceof AuthenticationServiceException) {
logger.error(String.format("Unable to authorize: %s", ae.getMessage()), ae);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
out.println(String.format("Unable to authorize: %s", ae.getMessage()));
} else {
logger.error(String.format("Unable to authorize: %s", ae.getMessage()), ae);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println("Access is denied.");
}
// log the failure
logger.info(String.format("Rejecting access to web api: %s", ae.getMessage()));
// optionally log the stack trace
if (logger.isDebugEnabled()) {
logger.debug(StringUtils.EMPTY, ae);
}
}
private void handleUserServiceError(HttpServletRequest request, HttpServletResponse response, int responseCode, String message) throws IOException {
// set the response status
response.setContentType("text/plain");
response.setStatus(responseCode);
// write the response message
PrintWriter out = response.getWriter();
out.println(message);
// log the failure
logger.info(String.format("Unable to process request because %s", message));
}
/**
* Handles requests that failed because they were bad input.
*
* @param request request
* @param response response
* @throws IOException ioe
*/
private void handleMissingCertificate(HttpServletRequest request, HttpServletResponse response) throws IOException {
// set the response status
response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
// write the response message
PrintWriter out = response.getWriter();
out.println("Unable to process request because the user certificate was not specified.");
// log the failure
logger.info("Unable to process request because the user certificate was not specified.");
}
/* setters */
public void setProperties(NiFiProperties properties) {
this.properties = properties;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setCertificateValidator(OcspCertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
}

View File

@ -35,11 +35,11 @@ public class X509CertificateExtractor {
* @param request http request
* @return cert
*/
public X509Certificate extractClientCertificate(HttpServletRequest request) {
public X509Certificate[] extractClientCertificate(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) {
return certs[0];
return certs;
}
if (logger.isDebugEnabled()) {

View File

@ -19,7 +19,6 @@ package org.apache.nifi.web.security.x509;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import javax.servlet.http.HttpServletRequest;
import org.apache.nifi.web.security.x509.ocsp.CertificateStatusException;
import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator;
import org.slf4j.Logger;
@ -37,20 +36,19 @@ public class X509CertificateValidator {
/**
* Extract the client certificate from the specified HttpServletRequest or null if none is specified.
*
* @param request the request
* @param certificate the certificate
* @param certificates the client certificates
* @throws java.security.cert.CertificateExpiredException cert is expired
* @throws java.security.cert.CertificateNotYetValidException cert is not yet valid
* @throws org.apache.nifi.web.security.x509.ocsp.CertificateStatusException ocsp validation issue
*/
public void validateClientCertificate(final HttpServletRequest request, final X509Certificate certificate)
public void validateClientCertificate(final X509Certificate[] certificates)
throws CertificateExpiredException, CertificateNotYetValidException, CertificateStatusException {
// ensure the cert is valid
certificate.checkValidity();
certificates[0].checkValidity();
// perform ocsp validator if necessary
ocspValidator.validate(request);
ocspValidator.validate(certificates);
}
public void setOcspValidator(OcspCertificateValidator ocspValidator) {

View File

@ -0,0 +1,92 @@
/*
* 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 java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
* Identity provider for extract the authenticating a ServletRequest with a X509Certificate.
*/
public class X509IdentityProvider {
private static final Logger logger = LoggerFactory.getLogger(X509IdentityProvider.class);
private X509CertificateValidator certificateValidator;
private X509PrincipalExtractor principalExtractor;
/**
* Authenticates the specified request by checking certificate validity.
*
* @param certificates the client certificates
* @return an authentication response
* @throws IllegalArgumentException the request did not contain a valid certificate (or no certificate)
*/
public AuthenticationResponse authenticate(final X509Certificate[] certificates) throws IllegalArgumentException {
// ensure the cert was found
if (certificates == null || certificates.length == 0) {
throw new IllegalArgumentException("The specified request does not contain a client certificate.");
}
// extract the principal
final Object certificatePrincipal = principalExtractor.extractPrincipal(certificates[0]);
final String principal = certificatePrincipal.toString();
try {
certificateValidator.validateClientCertificate(certificates);
} catch (CertificateExpiredException cee) {
final String message = String.format("Client certificate for (%s) is expired.", principal);
logger.info(message, cee);
if (logger.isDebugEnabled()) {
logger.debug("", cee);
}
throw new IllegalArgumentException(message, cee);
} catch (CertificateNotYetValidException cnyve) {
final String message = String.format("Client certificate for (%s) is not yet valid.", principal);
logger.info(message, cnyve);
if (logger.isDebugEnabled()) {
logger.debug("", cnyve);
}
throw new IllegalArgumentException(message, cnyve);
} catch (final Exception e) {
logger.info(e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("", e);
}
throw new IllegalArgumentException(e.getMessage(), e);
}
// build the authentication response
return new AuthenticationResponse(principal, principal, TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS));
}
/* setters */
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
this.principalExtractor = principalExtractor;
}
}

View File

@ -42,7 +42,6 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.x500.X500Principal;
import javax.servlet.http.HttpServletRequest;
import org.apache.nifi.framework.security.util.SslContextFactory;
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.ValidationStatus;
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.VerificationStatus;
@ -158,8 +157,7 @@ public class OcspCertificateValidator {
}
/**
* Loads the trusted certificate authorities according to the specified
* properties.
* Loads the trusted certificate authorities according to the specified properties.
*
* @param properties properties
* @return map of certificate authorities
@ -208,12 +206,10 @@ public class OcspCertificateValidator {
/**
* Validates the specified certificate using OCSP if configured.
*
* @param request http request
* @param certificates the client certificates
* @throws CertificateStatusException ex
*/
public void validate(final HttpServletRequest request) throws CertificateStatusException {
final X509Certificate[] certificates = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
public void validate(final X509Certificate[] certificates) throws CertificateStatusException {
// only validate if configured to do so
if (client != null && certificates != null && certificates.length > 0) {
final X509Certificate subjectCertificate = getSubjectCertificate(certificates);
@ -395,13 +391,9 @@ public class OcspCertificateValidator {
}
/**
* Gets the trusted responder certificate. The response contains the
* responder certificate, however we cannot blindly trust it. Instead, we
* use a configured trusted CA. If the responder certificate is a trusted
* CA, then we can use it. If the responder certificate is not directly
* trusted, we still may be able to trust it if it was issued by the same CA
* that issued the subject certificate. Other various checks may be required
* (this portion is currently not implemented).
* Gets the trusted responder certificate. The response contains the responder certificate, however we cannot blindly trust it. Instead, we use a configured trusted CA. If the responder
* certificate is a trusted CA, then we can use it. If the responder certificate is not directly trusted, we still may be able to trust it if it was issued by the same CA that issued the subject
* certificate. Other various checks may be required (this portion is currently not implemented).
*
* @param responderCertificate cert
* @param issuerCertificate cert

View File

@ -33,6 +33,12 @@
<property name="ocspValidator" ref="ocspValidator"/>
</bean>
<!-- x509 identity provider -->
<bean id="certificateIdentityProvider" class="org.apache.nifi.web.security.x509.X509IdentityProvider">
<property name="principalExtractor" ref="principalExtractor"/>
<property name="certificateValidator" ref="certificateValidator"/>
</bean>
<!-- user details service -->
<bean id="userDetailsService" class="org.apache.nifi.web.security.authorization.NiFiAuthorizationService">
<property name="userService" ref="userService"/>

View File

@ -187,7 +187,7 @@ public class NiFiAuthorizationServiceTest {
*
* @throws Exception ex
*/
@Test(expected = UsernameNotFoundException.class)
@Test(expected = UntrustedProxyException.class)
public void testProxyNotFound() throws Exception {
try {
authorizationService.loadUserDetails(createRequestAuthentication(USER, PROXY_NOT_FOUND));

View File

@ -27,8 +27,6 @@ nf.Login = (function () {
var config = {
urls: {
registrationStatus: '../nifi-api/registration/status',
registration: '../nifi-api/registration',
identity: '../nifi-api/controller/identity',
users: '../nifi-api/controller/users',
token: '../nifi-api/access/token',

View File

@ -84,9 +84,9 @@ public abstract class AbstractLdapProvider implements LoginIdentityProvider {
// attempt to get the ldap user details to get the DN
if (authentication.getPrincipal() instanceof LdapUserDetails) {
final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername());
return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration);
} else {
return new AuthenticationResponse(authentication.getName(), credentials.getUsername());
return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration);
}
} catch (final CommunicationException | AuthenticationServiceException e) {
logger.error(e.getMessage());
@ -99,11 +99,6 @@ public abstract class AbstractLdapProvider implements LoginIdentityProvider {
}
}
@Override
public long getExpiration() {
return expiration;
}
@Override
public final void preDestruction() throws ProviderDestructionException {
}