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 identity;
private final String username; private final String username;
private final long expiration;
/** /**
* Creates an authentication response. The username and how long the authentication is valid in milliseconds * Creates an authentication response. The username and how long the authentication is valid in milliseconds
* *
* @param identity The user identity * @param identity The user identity
* @param username The username * @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.identity = identity;
this.username = username; this.username = username;
this.expiration = expiration;
} }
public String getIdentity() { public String getIdentity() {
@ -43,4 +46,13 @@ public class AuthenticationResponse {
return username; 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; 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 * 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.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.x509.X509AuthenticationFilter; import org.apache.nifi.web.security.x509.X509AuthenticationFilter;
import org.apache.nifi.web.security.x509.X509CertificateExtractor; 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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/** /**
* NiFi Web Api Spring security * NiFi Web Api Spring security
@ -56,9 +55,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
private UserService userService; private UserService userService;
private AuthenticationUserDetailsService userDetailsService; private AuthenticationUserDetailsService userDetailsService;
private JwtService jwtService; private JwtService jwtService;
private X509CertificateValidator certificateValidator;
private X509CertificateExtractor certificateExtractor; private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor; private X509IdentityProvider certificateIdentityProvider;
private LoginIdentityProvider loginIdentityProvider; private LoginIdentityProvider loginIdentityProvider;
public NiFiWebApiSecurityConfiguration() { public NiFiWebApiSecurityConfiguration() {
@ -113,7 +111,11 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
} }
private NodeAuthorizedUserFilter buildNodeAuthorizedUserFilter() { 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 { private JwtAuthenticationFilter buildJwtFilter() throws Exception {
@ -127,9 +129,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
private X509AuthenticationFilter buildX509Filter() throws Exception { private X509AuthenticationFilter buildX509Filter() throws Exception {
final X509AuthenticationFilter x509Filter = new X509AuthenticationFilter(); final X509AuthenticationFilter x509Filter = new X509AuthenticationFilter();
x509Filter.setProperties(properties); x509Filter.setProperties(properties);
x509Filter.setPrincipalExtractor(principalExtractor);
x509Filter.setCertificateExtractor(certificateExtractor); x509Filter.setCertificateExtractor(certificateExtractor);
x509Filter.setCertificateValidator(certificateValidator); x509Filter.setCertificateIdentityProvider(certificateIdentityProvider);
x509Filter.setAuthenticationManager(authenticationManager()); x509Filter.setAuthenticationManager(authenticationManager());
return x509Filter; return x509Filter;
} }
@ -165,18 +166,14 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
this.loginIdentityProvider = loginIdentityProvider; this.loginIdentityProvider = loginIdentityProvider;
} }
@Autowired
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
@Autowired @Autowired
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor; this.certificateExtractor = certificateExtractor;
} }
@Autowired @Autowired
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
this.principalExtractor = principalExtractor; 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.ApiResponse;
import com.wordnik.swagger.annotations.ApiResponses; import com.wordnik.swagger.annotations.ApiResponses;
import java.net.URI; import java.net.URI;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -41,6 +39,7 @@ import javax.ws.rs.FormParam;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.admin.service.AdministrationException; import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.authentication.AuthenticationResponse; import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials; 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.IdentityAccessException;
import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException; import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
import org.apache.nifi.security.util.CertificateUtils; 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.AccessStatusDTO;
import org.apache.nifi.web.api.dto.AccessConfigurationDTO; import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
import org.apache.nifi.web.api.dto.RevisionDTO; 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.LoginAuthenticationToken;
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.x509.X509CertificateExtractor; 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.access.AccessDeniedException;
import org.springframework.security.authentication.AccountStatusException; import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/** /**
* RESTful endpoint for managing a cluster. * RESTful endpoint for managing a cluster.
@ -82,13 +79,15 @@ import org.springframework.security.web.authentication.preauth.x509.X509Principa
) )
public class AccessResource extends ApplicationResource { public class AccessResource extends ApplicationResource {
private static final Logger logger = LoggerFactory.getLogger(AccessResource.class);
private static final String AUTHORIZATION = "Authorization";
private NiFiProperties properties; private NiFiProperties properties;
private X509CertificateValidator certificateValidator;
private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor;
private LoginIdentityProvider loginIdentityProvider; private LoginIdentityProvider loginIdentityProvider;
private X509CertificateExtractor certificateExtractor;
private X509IdentityProvider certificateIdentityProvider;
private JwtService jwtService; private JwtService jwtService;
private AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService; private AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService;
@ -172,57 +171,64 @@ public class AccessResource extends ApplicationResource {
final AccessStatusDTO accessStatus = new AccessStatusDTO(); final AccessStatusDTO accessStatus = new AccessStatusDTO();
try { try {
// look for a certificate final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest);
final X509Certificate certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
// if no certificate, just check the credentials // if there is not certificate, consider a token
if (certificate == null) { if (certificates == null) {
final String principal = jwtService.getAuthentication(httpServletRequest); // look for an authorization token
final String authorization = httpServletRequest.getHeader(AUTHORIZATION);
// ensure we have something we can work with (certificate or crendentials) // if there is no authorization header, we don't know the user
if (principal == null) { if (authorization == null) {
accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name()); accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
accessStatus.setMessage("No credentials supplied, unknown user."); accessStatus.setMessage("No credentials supplied, unknown user.");
} else { } else {
// set the user identity // TODO - use this token with the JWT service
accessStatus.setIdentity(principal); final String token = StringUtils.substringAfterLast(authorization, " ");
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
// without a certificate, this is not a proxied request // TODO - do not call this method of the jwt service
final List<String> chain = Arrays.asList(principal); final String principal = jwtService.getAuthentication(httpServletRequest);
// check authorization for this user // TODO - catch jwt exception?
checkAuthorization(chain); // 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 // without a certificate, this is not a proxied request
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name()); final List<String> chain = Arrays.asList(principal);
accessStatus.setStatus("Account is active and authorized");
// 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 { } else {
// we have a certificate so let's consider a proxy chain final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
final String principal = principalExtractor.extractPrincipal(certificate).toString();
try { // get the proxy chain and ensure its populated
// validate the certificate final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(httpServletRequest, authenticationResponse.getIdentity());
certificateValidator.validateClientCertificate(httpServletRequest, certificate); if (proxyChain.isEmpty()) {
} catch (CertificateExpiredException cee) { logger.error(String.format("Unable to parse the proxy chain %s from the incoming request.", authenticationResponse.getIdentity()));
throw new IllegalArgumentException(String.format("Client certificate for (%s) is expired.", principal), cee); throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
} 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);
} }
// set the user identity
accessStatus.setIdentity(principal);
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
// ensure the proxy chain is authorized // 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 // no issues with authorization
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name()); accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setStatus("Account is active and authorized"); accessStatus.setMessage("Account is active and authorized");
} }
} catch (final UsernameNotFoundException unfe) { } catch (final UsernameNotFoundException unfe) {
accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name()); accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name());
@ -277,6 +283,7 @@ public class AccessResource extends ApplicationResource {
@ApiResponses( @ApiResponses(
value = { 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 = 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.") @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; final LoginAuthenticationToken loginAuthenticationToken;
// if we don't have username/password, consider JWT or x509 final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest);
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
// look for a certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
// if there is no certificate, look for an existing token // if there is not certificate, consider login credentials
if (certificate == null) { if (certificates == null) {
// if not configured for login, don't consider existing tokens // ensure we have login credentials
if (loginIdentityProvider == null) { if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
throw new IllegalStateException("Login not supported."); throw new IllegalArgumentException("The username and password must be specified.");
}
// 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());
} }
} else {
try { try {
// attempt to authenticate // attempt to authenticate
final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password)); final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password));
// create the authentication token // create the authentication token
loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getUsername(), loginIdentityProvider.getExpiration()); loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getUsername(), authenticationResponse.getExpiration());
} catch (final InvalidLoginCredentialsException ilce) { } catch (final InvalidLoginCredentialsException ilce) {
throw new IllegalArgumentException("The supplied username and password are not valid.", ilce); throw new IllegalArgumentException("The supplied username and password are not valid.", ilce);
} catch (final IdentityAccessException iae) { } catch (final IdentityAccessException iae) {
throw new AdministrationException(iae.getMessage(), 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 // generate JWT for response
@ -371,11 +362,14 @@ public class AccessResource extends ApplicationResource {
userDetailsService.loadUserDetails(new NiFiAuthenticationRequestToken(proxyChain)); userDetailsService.loadUserDetails(new NiFiAuthenticationRequestToken(proxyChain));
} catch (final UsernameNotFoundException unfe) { } catch (final UsernameNotFoundException unfe) {
// if a username not found exception was thrown, the proxies were authorized and now // 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) { } catch (final Exception e) {
// any other issue we're going to treat as an authentication exception which will return 401 // any other issue we're going to treat as access denied exception which will return 403
throw new AdministrationException(e.getMessage(), e) { throw new AccessDeniedException(e.getMessage(), e);
};
} }
} }
} }
@ -393,16 +387,12 @@ public class AccessResource extends ApplicationResource {
this.jwtService = jwtService; this.jwtService = jwtService;
} }
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor; this.certificateExtractor = certificateExtractor;
} }
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
this.principalExtractor = principalExtractor; this.certificateIdentityProvider = certificateIdentityProvider;
} }
public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService) { 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.StringUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder; import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -363,9 +365,9 @@ public abstract class ApplicationResource {
if (httpServletRequest.isSecure()) { if (httpServletRequest.isSecure()) {
// add the certificate DN to the proxy chain // add the certificate DN to the proxy chain
final String xProxiedEntitiesChain = ProxiedEntitiesUtils.getXProxiedEntitiesChain(httpServletRequest); final NiFiUser user = NiFiUserUtils.getNiFiUser();
if (StringUtils.isNotBlank(xProxiedEntitiesChain)) { if (user != null) {
result.put(PROXIED_ENTITIES_CHAIN_HTTP_HEADER, xProxiedEntitiesChain); result.put(PROXIED_ENTITIES_CHAIN_HTTP_HEADER, ProxiedEntitiesUtils.buildProxiedEntitiesChainString(user));
} }
// add the user's authorities (if any) to the headers // 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(); final Map<String, String> attributes = event.getAttributes();
// calculate the dn chain // 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 // ensure the users in this chain are allowed to download this content
final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes); final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes);

View File

@ -243,9 +243,8 @@
</bean> </bean>
<bean id="accessResource" class="org.apache.nifi.web.api.AccessResource" scope="singleton"> <bean id="accessResource" class="org.apache.nifi.web.api.AccessResource" scope="singleton">
<property name="properties" ref="nifiProperties"/> <property name="properties" ref="nifiProperties"/>
<property name="certificateValidator" ref="certificateValidator"/>
<property name="certificateExtractor" ref="certificateExtractor"/> <property name="certificateExtractor" ref="certificateExtractor"/>
<property name="principalExtractor" ref="principalExtractor"/> <property name="certificateIdentityProvider" ref="certificateIdentityProvider"/>
<property name="loginIdentityProvider" ref="loginIdentityProvider"/> <property name="loginIdentityProvider" ref="loginIdentityProvider"/>
<property name="jwtService" ref="jwtService"/> <property name="jwtService" ref="jwtService"/>
<property name="userDetailsService" ref="userDetailsService"/> <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 { public class NiFiTestAuthorizationProvider implements AuthorityProvider {
private Map<String, Set<Authority>> users; private final Map<String, Set<Authority>> users;
/** /**
* Creates a new FileAuthorizationProvider. * 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 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 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("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 @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() { private void createSecureConnector() {
org.eclipse.jetty.util.ssl.SslContextFactory contextFactory = new org.eclipse.jetty.util.ssl.SslContextFactory(); org.eclipse.jetty.util.ssl.SslContextFactory contextFactory = new org.eclipse.jetty.util.ssl.SslContextFactory();
// need client auth // require client auth when not supporting login or anonymous access
contextFactory.setNeedClientAuth(properties.getNeedClientAuth()); 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 */ /* below code sets JSSE system properties when values are provided */
// keystore properties // keystore properties
@ -163,7 +167,6 @@ public class NiFiTestServer {
} }
public Client getClient() { public Client getClient() {
// create the client
return WebUtils.createClient(null, SslContextFactory.createSslContext(properties)); return WebUtils.createClient(null, SslContextFactory.createSslContext(properties));
} }

View File

@ -34,9 +34,27 @@ public class NiFiTestUser {
private final Client client; private final Client client;
private final String proxyDn; private final String proxyDn;
public NiFiTestUser(Client client, String dn) { public NiFiTestUser(Client client, String proxyDn) {
this.client = client; 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 * @return response
*/ */
public ClientResponse testGet(String url, Map<String, String> queryParams) { 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 // get the resource
WebResource resource = client.resource(url); 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 // 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 * @throws Exception ex
*/ */
public ClientResponse testPost(String url, Object entity) throws Exception { 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 // 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 // include the request entity
if (entity != null) { if (entity != null) {
resourceBuilder = resourceBuilder.entity(entity); 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 // perform the request
return resourceBuilder.post(ClientResponse.class); return resourceBuilder.post(ClientResponse.class);
} }
@ -109,18 +169,38 @@ public class NiFiTestUser {
* *
* @param url url * @param url url
* @param entity entity * @param entity entity
* @return repsonse * @return response
* @throws Exception ex * @throws Exception ex
*/ */
public ClientResponse testPostMultiPart(String url, Object entity) throws Exception { 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 // 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 // include the request entity
if (entity != null) { if (entity != null) {
resourceBuilder = resourceBuilder.entity(entity); 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 // perform the request
return resourceBuilder.post(ClientResponse.class); return resourceBuilder.post(ClientResponse.class);
} }
@ -134,6 +214,19 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex * @throws java.lang.Exception ex
*/ */
public ClientResponse testPost(String url, Map<String, String> formData) throws Exception { 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 // convert the form data
MultivaluedMapImpl entity = new MultivaluedMapImpl(); MultivaluedMapImpl entity = new MultivaluedMapImpl();
for (String key : formData.keySet()) { for (String key : formData.keySet()) {
@ -141,14 +234,20 @@ public class NiFiTestUser {
} }
// get the resource // get the resource
WebResource.Builder resourceBuilder WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED));
= client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
// add the form data if necessary // add the form data if necessary
if (!entity.isEmpty()) { if (!entity.isEmpty()) {
resourceBuilder = resourceBuilder.entity(entity); 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 // perform the request
return resourceBuilder.post(ClientResponse.class); return resourceBuilder.post(ClientResponse.class);
} }
@ -162,14 +261,34 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex * @throws java.lang.Exception ex
*/ */
public ClientResponse testPut(String url, Object entity) throws Exception { 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 // 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 // include the request entity
if (entity != null) { if (entity != null) {
resourceBuilder = resourceBuilder.entity(entity); 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 // perform the request
return resourceBuilder.put(ClientResponse.class); return resourceBuilder.put(ClientResponse.class);
} }
@ -183,6 +302,19 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex * @throws java.lang.Exception ex
*/ */
public ClientResponse testPut(String url, Map<String, String> formData) throws Exception { 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 // convert the form data
MultivaluedMapImpl entity = new MultivaluedMapImpl(); MultivaluedMapImpl entity = new MultivaluedMapImpl();
for (String key : formData.keySet()) { for (String key : formData.keySet()) {
@ -190,14 +322,20 @@ public class NiFiTestUser {
} }
// get the resource // get the resource
WebResource.Builder resourceBuilder WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED));
= client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
// add the form data if necessary // add the form data if necessary
if (!entity.isEmpty()) { if (!entity.isEmpty()) {
resourceBuilder = resourceBuilder.entity(entity); 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 // perform the request
return resourceBuilder.put(ClientResponse.class); return resourceBuilder.put(ClientResponse.class);
} }
@ -210,24 +348,26 @@ public class NiFiTestUser {
* @throws java.lang.Exception ex * @throws java.lang.Exception ex
*/ */
public ClientResponse testDelete(String url) throws Exception { 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. * Performs a DELETE using the specified url and entity.
* *
* @param url url * @param url url
* @param entity entity * @param headers http headers
* @return repsonse * @return response
* @throws java.lang.Exception ex * @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 // 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 // append any headers
if (entity != null) { if (headers != null && !headers.isEmpty()) {
resourceBuilder = resourceBuilder.entity(entity); for (String key : headers.keySet()) {
resourceBuilder = resourceBuilder.header(key, headers.get(key));
}
} }
// perform the query // perform the query
@ -254,7 +394,56 @@ public class NiFiTestUser {
} }
// perform the request // 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 Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with contributor license agreements. See the NOTICE file distributed with
@ -13,6 +13,12 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<services> <!--
This file lists all authority providers to use when running securely.
</services> -->
<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. # limitations under the License.
# Core Properties # # Core Properties #
nifi.version=nifi 0.2.1-SNAPSHOT nifi.version=nifi version
nifi.flow.configuration.file= nifi.flow.configuration.file=
nifi.flow.configuration.archive.dir=target/archive nifi.flow.configuration.archive.dir=target/archive
nifi.flowcontroller.autoResumeState=true nifi.flowcontroller.autoResumeState=true
nifi.flowcontroller.graceful.shutdown.period=10 sec nifi.flowcontroller.graceful.shutdown.period=10 sec
nifi.flowservice.writedelay.interval=2 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.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.templates.directory=target/test-classes/access-control/templates
nifi.ui.banner.text=TEST BANNER nifi.ui.banner.text=TEST BANNER
nifi.ui.autorefresh.interval=30 sec nifi.ui.autorefresh.interval=30 sec
@ -93,11 +92,11 @@ nifi.security.truststoreType=JKS
nifi.security.truststorePasswd=localtest nifi.security.truststorePasswd=localtest
nifi.security.needClientAuth=true nifi.security.needClientAuth=true
nifi.security.user.authority.provider=test-provider 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.authorizedUsers.file=target/test-classes/access-control/users.xml
nifi.security.user.credential.cache.duration=1 hr nifi.security.user.credential.cache.duration=1 hr
nifi.security.support.new.account.requests= 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) # # cluster common properties (cluster manager and nodes must have same values) #
nifi.cluster.protocol.heartbeat.interval=5 sec 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.commons.lang3.StringUtils;
import org.apache.nifi.user.NiFiUser; import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.user.NiFiUserUtils; import org.apache.nifi.web.security.user.NiFiUserUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AccountStatusException; import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder; 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 { private void authenticate(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
try { try {
final Authentication authenticated = attemptAuthentication(request, response); final NiFiAuthenticationRequestToken authenticated = attemptAuthentication(request, response);
if (authenticated != null) { 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); final Authentication authorized = authenticationManager.authenticate(authenticated);
successfulAuthorization(request, response, authorized); 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) { protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
@ -127,6 +134,9 @@ public abstract class NiFiAuthenticationFilter implements Filter {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println("Access is denied."); 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) { } else if (ae instanceof AccountStatusException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println(ae.getMessage()); out.println(ae.getMessage());

View File

@ -47,10 +47,12 @@ public class NiFiAuthenticationProvider implements AuthenticationProvider {
final UserDetails userDetails = userDetailsService.loadUserDetails(request); final UserDetails userDetails = userDetailsService.loadUserDetails(request);
// build an authentication for accesing nifi // 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) { } 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, // 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 to ensure that any proxies have been authorized // 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)) { if (isNewAccountAuthenticationToken(request)) {
return new NewAccountAuthenticationToken(((NewAccountAuthenticationRequestToken) authentication).getNewAccountRequest()); return new NewAccountAuthenticationToken(((NewAccountAuthenticationRequestToken) authentication).getNewAccountRequest());
} else { } else {

View File

@ -16,15 +16,12 @@
*/ */
package org.apache.nifi.web.security; package org.apache.nifi.web.security;
import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; 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.commons.lang3.StringUtils;
import org.apache.nifi.user.NiFiUser; import org.apache.nifi.user.NiFiUser;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -42,33 +39,50 @@ public class ProxiedEntitiesUtils {
private static final Pattern proxyChainPattern = Pattern.compile("<(.*?)>"); private static final Pattern proxyChainPattern = Pattern.compile("<(.*?)>");
/** /**
* @param request http request * Formats the specified DN to be set as a HTTP header using well known conventions.
* @return the X-ProxiedEntitiesChain from the specified request *
* @param dn raw dn
* @return the dn formatted as an HTTP header
*/ */
public static String getXProxiedEntitiesChain(final HttpServletRequest request) { public static String formatProxyDn(String dn) {
String xProxiedEntitiesChain = request.getHeader("X-ProxiedEntitiesChain"); return "<" + dn + ">";
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;
} }
/** /**
* 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 * @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 // calculate the dn chain
final List<String> proxyChain = new ArrayList<>(); 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 * @param request the request
* @return the dn formatted as an HTTP header * @param username the username
* @return the proxy chain in list form
*/ */
public static String formatProxyDn(String dn) { public static List<String> buildProxiedEntitiesChain(final HttpServletRequest request, final String username) {
return "<" + dn + ">"; final String chain = buildProxiedEntitiesChainString(request, username);
return tokenizeProxiedEntitiesChain(chain);
} }
// /** /**
// * Tokenizes the specified proxy chain. * Builds the dn chain from the specified request and user.
// * *
// * @param rawProxyChain raw chain * @param request the request
// * @return tokenized proxy chain * @param username the username
// */ * @return the dn chain in string form
// public static Deque<String> tokenizeProxyChain(String rawProxyChain) { */
// final Deque<String> dnList = new ArrayDeque<>(); public static String buildProxiedEntitiesChainString(final HttpServletRequest request, final String username) {
//
// // 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) {
String principal; String principal;
if (username.startsWith("<") && username.endsWith(">")) { if (username.startsWith("<") && username.endsWith(">")) {
principal = username; principal = username;

View File

@ -54,10 +54,8 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
/** /**
* Loads the user details for the specified dn. * Loads the user details for the specified dn.
* *
* Synchronizing because we want each request to be authorized atomically * 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
* since each may contain any number of DNs. We wanted an access decision * (without other request potentially impacting it).
* made for each individual request as a whole (without other request
* potentially impacting it).
* *
* @param request request * @param request request
* @return user details * @return user details
@ -109,9 +107,6 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
// attempt to create a new user account for the proxying client // attempt to create a new user account for the proxying client
userService.createPendingUserAccount(dn, "Automatic account request generated for unknown proxy."); 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) { } catch (AdministrationException ae) {
throw new AuthenticationServiceException(String.format("Unable to create an account request for '%s': %s", dn, ae.getMessage()), ae); throw new AuthenticationServiceException(String.format("Unable to create an account request for '%s': %s", dn, ae.getMessage()), ae);
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
@ -122,10 +117,10 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
throw new AccountStatusException(message) { 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) { } catch (AuthenticationException ae) {
logger.warn(String.format("Untrusted proxy '%s' must be authorized with '%s' authority: %s", dn, Authority.ROLE_PROXY.toString(), ae.getMessage())); 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())); 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.apache.nifi.web.security.user.NewAccountRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
/** /**
*/ */
@ -36,7 +35,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
private JwtService jwtService; private JwtService jwtService;
@Override @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { public NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// only suppport jwt login when running securely // only suppport jwt login when running securely
if (!request.isSecure()) { if (!request.isSecure()) {
return null; return null;

View File

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

View File

@ -26,34 +26,26 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.nifi.controller.FlowController; import org.apache.nifi.controller.FlowController;
import org.apache.commons.lang3.StringUtils; 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.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.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties; 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.apache.nifi.web.util.WebUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.core.context.SecurityContextHolder; 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.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.GenericFilterBean;
/** /**
* Custom filter to extract a user's authorities from the request where the user * 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
* was authenticated by the cluster manager and populate the threadlocal with * 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.
* 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 * 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.
* it prevents external clients from faking the request headers and bypassing
* the authentication processing chain.
*/ */
public class NodeAuthorizedUserFilter extends GenericFilterBean { public class NodeAuthorizedUserFilter extends GenericFilterBean {
@ -61,14 +53,9 @@ public class NodeAuthorizedUserFilter extends GenericFilterBean {
public static final String PROXY_USER_DETAILS = "X-ProxiedEntityUserDetails"; public static final String PROXY_USER_DETAILS = "X-ProxiedEntityUserDetails";
private final NiFiProperties properties; private NiFiProperties properties;
private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); private X509CertificateExtractor certificateExtractor;
private final X509CertificateExtractor certificateExtractor = new X509CertificateExtractor(); private X509IdentityProvider certificateIdentityProvider;
private final X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
public NodeAuthorizedUserFilter(NiFiProperties properties) {
this.properties = properties;
}
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 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 // check that we are connected to the cluster
if (flowController.getNodeId() != null) { if (flowController.getNodeId() != null) {
try { try {
// get the DN from the cert in the request // attempt to extract the client certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate((HttpServletRequest) request); final X509Certificate[] certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
if (certificate != null) { if (certificate != null) {
// extract the principal from the certificate // authenticate the certificate
final Object certificatePrincipal = principalExtractor.extractPrincipal(certificate); final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificate);
final String dn = certificatePrincipal.toString();
// only consider the pre-authorized user when the request came from the NCM according to the DN in the certificate // only consider the pre-authorized user when the request came directly from the NCM according to the DN in the certificate
final String clusterManagerDN = flowController.getClusterManagerDN(); final String clusterManagerIdentity = flowController.getClusterManagerDN();
if (clusterManagerDN != null && clusterManagerDN.equals(dn)) { if (clusterManagerIdentity != null && clusterManagerIdentity.equals(authenticationResponse.getIdentity())) {
// deserialize hex encoded object // deserialize hex encoded object
final Serializable userDetailsObj = WebUtils.deserializeHexToObject(hexEncodedUserDetails); 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(), logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", user.getIdentity(), httpServletRequest.getMethod(),
httpServletRequest.getRequestURL().toString(), request.getRemoteAddr())); 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 // create the authorized nifi token
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities()); final NiFiAuthorizationToken token = new NiFiAuthorizationToken(userDetails);
token.setDetails(authenticationDetailsSource.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(token); SecurityContextHolder.getContext().setAuthentication(token);
} }
} }
} }
} catch (final ClassNotFoundException cnfe) { } catch (final ClassNotFoundException cnfe) {
LOGGER.warn("Classpath issue detected because failed to deserialize authorized user in request header due to: " + cnfe, 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); 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 @Override
public void initialize(LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException { public void initialize(LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) {

View File

@ -16,12 +16,11 @@
*/ */
package org.apache.nifi.web.security.x509; package org.apache.nifi.web.security.x509;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.web.security.NiFiAuthenticationFilter; import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.apache.nifi.web.security.ProxiedEntitiesUtils; import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken; 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.apache.nifi.web.security.user.NewAccountRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication; import org.springframework.security.authentication.BadCredentialsException;
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. * 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 static final Logger logger = LoggerFactory.getLogger(X509AuthenticationFilter.class);
private X509PrincipalExtractor principalExtractor;
private X509CertificateExtractor certificateExtractor; private X509CertificateExtractor certificateExtractor;
private X509CertificateValidator certificateValidator; private X509IdentityProvider certificateIdentityProvider;
@Override @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { public NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// only suppport x509 login when running securely // only suppport x509 login when running securely
if (!request.isSecure()) { if (!request.isSecure()) {
return null; return null;
} }
// extract the cert // look for a client certificate
X509Certificate certificate = certificateExtractor.extractClientCertificate(request); final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(request);
if (certificates == null) {
// ensure the cert was found
if (certificate == null) {
return null; return null;
} }
// extract the principal // attempt to authenticate if certificates were found
Object certificatePrincipal = principalExtractor.extractPrincipal(certificate); final AuthenticationResponse authenticationResponse;
final String principal = ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
try { try {
certificateValidator.validateClientCertificate(request, certificate); authenticationResponse = certificateIdentityProvider.authenticate(certificates);
} catch (CertificateExpiredException cee) { } catch (final IllegalArgumentException iae) {
final String message = String.format("Client certificate for (%s) is expired.", principal); throw new BadCredentialsException(iae.getMessage(), iae);
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;
} }
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxyChain(request, principal); final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(request, authenticationResponse.getIdentity());
if (isNewAccountRequest(request)) { if (isNewAccountRequest(request)) {
return new NewAccountAuthenticationRequestToken(new NewAccountRequest(proxyChain, getJustification(request))); return new NewAccountAuthenticationRequestToken(new NewAccountRequest(proxyChain, getJustification(request)));
} else { } else {
@ -95,16 +70,12 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
} }
/* setters */ /* setters */
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
this.certificateValidator = certificateValidator;
}
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
this.principalExtractor = principalExtractor;
}
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = 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 * @param request http request
* @return cert * @return cert
*/ */
public X509Certificate extractClientCertificate(HttpServletRequest request) { public X509Certificate[] extractClientCertificate(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) { if (certs != null && certs.length > 0) {
return certs[0]; return certs;
} }
if (logger.isDebugEnabled()) { 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.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException; import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate; 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.CertificateStatusException;
import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator; import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator;
import org.slf4j.Logger; 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. * Extract the client certificate from the specified HttpServletRequest or null if none is specified.
* *
* @param request the request * @param certificates the client certificates
* @param certificate the certificate
* @throws java.security.cert.CertificateExpiredException cert is expired * @throws java.security.cert.CertificateExpiredException cert is expired
* @throws java.security.cert.CertificateNotYetValidException cert is not yet valid * @throws java.security.cert.CertificateNotYetValidException cert is not yet valid
* @throws org.apache.nifi.web.security.x509.ocsp.CertificateStatusException ocsp validation issue * @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 { throws CertificateExpiredException, CertificateNotYetValidException, CertificateStatusException {
// ensure the cert is valid // ensure the cert is valid
certificate.checkValidity(); certificates[0].checkValidity();
// perform ocsp validator if necessary // perform ocsp validator if necessary
ocspValidator.validate(request); ocspValidator.validate(certificates);
} }
public void setOcspValidator(OcspCertificateValidator ocspValidator) { 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.TrustManagerFactory;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import javax.security.auth.x500.X500Principal; import javax.security.auth.x500.X500Principal;
import javax.servlet.http.HttpServletRequest;
import org.apache.nifi.framework.security.util.SslContextFactory; 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.ValidationStatus;
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.VerificationStatus; 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 * Loads the trusted certificate authorities according to the specified properties.
* properties.
* *
* @param properties properties * @param properties properties
* @return map of certificate authorities * @return map of certificate authorities
@ -208,12 +206,10 @@ public class OcspCertificateValidator {
/** /**
* Validates the specified certificate using OCSP if configured. * Validates the specified certificate using OCSP if configured.
* *
* @param request http request * @param certificates the client certificates
* @throws CertificateStatusException ex * @throws CertificateStatusException ex
*/ */
public void validate(final HttpServletRequest request) throws CertificateStatusException { public void validate(final X509Certificate[] certificates) throws CertificateStatusException {
final X509Certificate[] certificates = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
// only validate if configured to do so // only validate if configured to do so
if (client != null && certificates != null && certificates.length > 0) { if (client != null && certificates != null && certificates.length > 0) {
final X509Certificate subjectCertificate = getSubjectCertificate(certificates); final X509Certificate subjectCertificate = getSubjectCertificate(certificates);
@ -395,13 +391,9 @@ public class OcspCertificateValidator {
} }
/** /**
* Gets the trusted responder certificate. The response contains the * 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
* responder certificate, however we cannot blindly trust it. Instead, we * 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
* use a configured trusted CA. If the responder certificate is a trusted * certificate. Other various checks may be required (this portion is currently not implemented).
* 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 responderCertificate cert
* @param issuerCertificate cert * @param issuerCertificate cert

View File

@ -33,6 +33,12 @@
<property name="ocspValidator" ref="ocspValidator"/> <property name="ocspValidator" ref="ocspValidator"/>
</bean> </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 --> <!-- user details service -->
<bean id="userDetailsService" class="org.apache.nifi.web.security.authorization.NiFiAuthorizationService"> <bean id="userDetailsService" class="org.apache.nifi.web.security.authorization.NiFiAuthorizationService">
<property name="userService" ref="userService"/> <property name="userService" ref="userService"/>

View File

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

View File

@ -27,8 +27,6 @@ nf.Login = (function () {
var config = { var config = {
urls: { urls: {
registrationStatus: '../nifi-api/registration/status',
registration: '../nifi-api/registration',
identity: '../nifi-api/controller/identity', identity: '../nifi-api/controller/identity',
users: '../nifi-api/controller/users', users: '../nifi-api/controller/users',
token: '../nifi-api/access/token', 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 // attempt to get the ldap user details to get the DN
if (authentication.getPrincipal() instanceof LdapUserDetails) { if (authentication.getPrincipal() instanceof LdapUserDetails) {
final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal(); final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername()); return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration);
} else { } else {
return new AuthenticationResponse(authentication.getName(), credentials.getUsername()); return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration);
} }
} catch (final CommunicationException | AuthenticationServiceException e) { } catch (final CommunicationException | AuthenticationServiceException e) {
logger.error(e.getMessage()); logger.error(e.getMessage());
@ -99,11 +99,6 @@ public abstract class AbstractLdapProvider implements LoginIdentityProvider {
} }
} }
@Override
public long getExpiration() {
return expiration;
}
@Override @Override
public final void preDestruction() throws ProviderDestructionException { public final void preDestruction() throws ProviderDestructionException {
} }