mirror of https://github.com/apache/nifi.git
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:
parent
7529694f23
commit
a196207725
|
@ -23,16 +23,19 @@ public class AuthenticationResponse {
|
|||
|
||||
private final String identity;
|
||||
private final String username;
|
||||
private final long expiration;
|
||||
|
||||
/**
|
||||
* Creates an authentication response. The username and how long the authentication is valid in milliseconds
|
||||
*
|
||||
* @param identity The user identity
|
||||
* @param username The username
|
||||
* @param expiration The expiration in milliseconds
|
||||
*/
|
||||
public AuthenticationResponse(final String identity, final String username) {
|
||||
public AuthenticationResponse(final String identity, final String username, final long expiration) {
|
||||
this.identity = identity;
|
||||
this.username = username;
|
||||
this.expiration = expiration;
|
||||
}
|
||||
|
||||
public String getIdentity() {
|
||||
|
@ -43,4 +46,13 @@ public class AuthenticationResponse {
|
|||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expiration of a given authentication in milliseconds.
|
||||
*
|
||||
* @return The expiration in milliseconds
|
||||
*/
|
||||
public long getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,13 +36,6 @@ public interface LoginIdentityProvider {
|
|||
*/
|
||||
AuthenticationResponse authenticate(LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException;
|
||||
|
||||
/**
|
||||
* Returns the expiration of a given authentication in milliseconds.
|
||||
*
|
||||
* @return The expiration in milliseconds
|
||||
*/
|
||||
long getExpiration();
|
||||
|
||||
/**
|
||||
* Called immediately after instance creation for implementers to perform additional setup
|
||||
*
|
||||
|
|
|
@ -28,7 +28,7 @@ import org.apache.nifi.web.security.node.NodeAuthorizedUserFilter;
|
|||
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
|
||||
import org.apache.nifi.web.security.x509.X509AuthenticationFilter;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateValidator;
|
||||
import org.apache.nifi.web.security.x509.X509IdentityProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -42,7 +42,6 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
|
|||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
|
||||
/**
|
||||
* NiFi Web Api Spring security
|
||||
|
@ -56,9 +55,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
private UserService userService;
|
||||
private AuthenticationUserDetailsService userDetailsService;
|
||||
private JwtService jwtService;
|
||||
private X509CertificateValidator certificateValidator;
|
||||
private X509CertificateExtractor certificateExtractor;
|
||||
private X509PrincipalExtractor principalExtractor;
|
||||
private X509IdentityProvider certificateIdentityProvider;
|
||||
private LoginIdentityProvider loginIdentityProvider;
|
||||
|
||||
public NiFiWebApiSecurityConfiguration() {
|
||||
|
@ -113,7 +111,11 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
}
|
||||
|
||||
private NodeAuthorizedUserFilter buildNodeAuthorizedUserFilter() {
|
||||
return new NodeAuthorizedUserFilter(properties);
|
||||
final NodeAuthorizedUserFilter nodeFilter = new NodeAuthorizedUserFilter();
|
||||
nodeFilter.setProperties(properties);
|
||||
nodeFilter.setCertificateExtractor(certificateExtractor);
|
||||
nodeFilter.setCertificateIdentityProvider(certificateIdentityProvider);
|
||||
return nodeFilter;
|
||||
}
|
||||
|
||||
private JwtAuthenticationFilter buildJwtFilter() throws Exception {
|
||||
|
@ -127,9 +129,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
private X509AuthenticationFilter buildX509Filter() throws Exception {
|
||||
final X509AuthenticationFilter x509Filter = new X509AuthenticationFilter();
|
||||
x509Filter.setProperties(properties);
|
||||
x509Filter.setPrincipalExtractor(principalExtractor);
|
||||
x509Filter.setCertificateExtractor(certificateExtractor);
|
||||
x509Filter.setCertificateValidator(certificateValidator);
|
||||
x509Filter.setCertificateIdentityProvider(certificateIdentityProvider);
|
||||
x509Filter.setAuthenticationManager(authenticationManager());
|
||||
return x509Filter;
|
||||
}
|
||||
|
@ -165,18 +166,14 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
this.loginIdentityProvider = loginIdentityProvider;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
|
||||
this.certificateValidator = certificateValidator;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
|
||||
this.certificateExtractor = certificateExtractor;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
|
||||
this.principalExtractor = principalExtractor;
|
||||
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
|
||||
this.certificateIdentityProvider = certificateIdentityProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,8 +30,6 @@ import com.wordnik.swagger.annotations.ApiParam;
|
|||
import com.wordnik.swagger.annotations.ApiResponse;
|
||||
import com.wordnik.swagger.annotations.ApiResponses;
|
||||
import java.net.URI;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
@ -41,6 +39,7 @@ import javax.ws.rs.FormParam;
|
|||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.admin.service.AdministrationException;
|
||||
import org.apache.nifi.authentication.AuthenticationResponse;
|
||||
import org.apache.nifi.authentication.LoginCredentials;
|
||||
|
@ -48,8 +47,6 @@ import org.apache.nifi.authentication.LoginIdentityProvider;
|
|||
import org.apache.nifi.authentication.exception.IdentityAccessException;
|
||||
import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
|
||||
import org.apache.nifi.security.util.CertificateUtils;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import static org.apache.nifi.web.api.ApplicationResource.CLIENT_ID;
|
||||
import org.apache.nifi.web.api.dto.AccessStatusDTO;
|
||||
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
|
||||
import org.apache.nifi.web.api.dto.RevisionDTO;
|
||||
|
@ -62,15 +59,15 @@ import org.apache.nifi.web.security.jwt.JwtService;
|
|||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateValidator;
|
||||
import org.apache.nifi.web.security.x509.X509IdentityProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AccountStatusException;
|
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
|
||||
/**
|
||||
* RESTful endpoint for managing a cluster.
|
||||
|
@ -82,13 +79,15 @@ import org.springframework.security.web.authentication.preauth.x509.X509Principa
|
|||
)
|
||||
public class AccessResource extends ApplicationResource {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AccessResource.class);
|
||||
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
|
||||
private NiFiProperties properties;
|
||||
|
||||
private X509CertificateValidator certificateValidator;
|
||||
private X509CertificateExtractor certificateExtractor;
|
||||
private X509PrincipalExtractor principalExtractor;
|
||||
|
||||
private LoginIdentityProvider loginIdentityProvider;
|
||||
private X509CertificateExtractor certificateExtractor;
|
||||
private X509IdentityProvider certificateIdentityProvider;
|
||||
private JwtService jwtService;
|
||||
|
||||
private AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService;
|
||||
|
@ -172,57 +171,64 @@ public class AccessResource extends ApplicationResource {
|
|||
final AccessStatusDTO accessStatus = new AccessStatusDTO();
|
||||
|
||||
try {
|
||||
// look for a certificate
|
||||
final X509Certificate certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
|
||||
final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest);
|
||||
|
||||
// if no certificate, just check the credentials
|
||||
if (certificate == null) {
|
||||
final String principal = jwtService.getAuthentication(httpServletRequest);
|
||||
// if there is not certificate, consider a token
|
||||
if (certificates == null) {
|
||||
// look for an authorization token
|
||||
final String authorization = httpServletRequest.getHeader(AUTHORIZATION);
|
||||
|
||||
// ensure we have something we can work with (certificate or crendentials)
|
||||
if (principal == null) {
|
||||
// if there is no authorization header, we don't know the user
|
||||
if (authorization == null) {
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
|
||||
accessStatus.setMessage("No credentials supplied, unknown user.");
|
||||
} else {
|
||||
// set the user identity
|
||||
accessStatus.setIdentity(principal);
|
||||
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
|
||||
// TODO - use this token with the JWT service
|
||||
final String token = StringUtils.substringAfterLast(authorization, " ");
|
||||
|
||||
// without a certificate, this is not a proxied request
|
||||
final List<String> chain = Arrays.asList(principal);
|
||||
// TODO - do not call this method of the jwt service
|
||||
final String principal = jwtService.getAuthentication(httpServletRequest);
|
||||
|
||||
// check authorization for this user
|
||||
checkAuthorization(chain);
|
||||
// TODO - catch jwt exception?
|
||||
// ensure we have something we can work with (certificate or crendentials)
|
||||
if (principal == null) {
|
||||
throw new IllegalArgumentException("The specific token is not valid.");
|
||||
} else {
|
||||
// set the user identity
|
||||
accessStatus.setIdentity(principal);
|
||||
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
|
||||
|
||||
// no issues with authorization
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
|
||||
accessStatus.setStatus("Account is active and authorized");
|
||||
// without a certificate, this is not a proxied request
|
||||
final List<String> chain = Arrays.asList(principal);
|
||||
|
||||
// check authorization for this user
|
||||
checkAuthorization(chain);
|
||||
|
||||
// no issues with authorization
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
|
||||
accessStatus.setMessage("Account is active and authorized");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we have a certificate so let's consider a proxy chain
|
||||
final String principal = principalExtractor.extractPrincipal(certificate).toString();
|
||||
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
|
||||
|
||||
try {
|
||||
// validate the certificate
|
||||
certificateValidator.validateClientCertificate(httpServletRequest, certificate);
|
||||
} catch (CertificateExpiredException cee) {
|
||||
throw new IllegalArgumentException(String.format("Client certificate for (%s) is expired.", principal), cee);
|
||||
} catch (CertificateNotYetValidException cnyve) {
|
||||
throw new IllegalArgumentException(String.format("Client certificate for (%s) is not yet valid.", principal), cnyve);
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalArgumentException(e.getMessage(), e);
|
||||
// get the proxy chain and ensure its populated
|
||||
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(httpServletRequest, authenticationResponse.getIdentity());
|
||||
if (proxyChain.isEmpty()) {
|
||||
logger.error(String.format("Unable to parse the proxy chain %s from the incoming request.", authenticationResponse.getIdentity()));
|
||||
throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
|
||||
}
|
||||
|
||||
// set the user identity
|
||||
accessStatus.setIdentity(principal);
|
||||
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
|
||||
|
||||
// ensure the proxy chain is authorized
|
||||
checkAuthorization(ProxiedEntitiesUtils.buildProxyChain(httpServletRequest, principal));
|
||||
checkAuthorization(proxyChain);
|
||||
|
||||
// set the user identity
|
||||
accessStatus.setIdentity(proxyChain.get(0));
|
||||
accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0)));
|
||||
|
||||
// no issues with authorization
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
|
||||
accessStatus.setStatus("Account is active and authorized");
|
||||
accessStatus.setMessage("Account is active and authorized");
|
||||
}
|
||||
} catch (final UsernameNotFoundException unfe) {
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name());
|
||||
|
@ -277,6 +283,7 @@ public class AccessResource extends ApplicationResource {
|
|||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
|
||||
@ApiResponse(code = 403, message = "Client is not authorized to make this request."),
|
||||
@ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
|
||||
}
|
||||
)
|
||||
|
@ -297,58 +304,42 @@ public class AccessResource extends ApplicationResource {
|
|||
|
||||
final LoginAuthenticationToken loginAuthenticationToken;
|
||||
|
||||
// if we don't have username/password, consider JWT or x509
|
||||
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
|
||||
// look for a certificate
|
||||
final X509Certificate certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
|
||||
final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest);
|
||||
|
||||
// if there is no certificate, look for an existing token
|
||||
if (certificate == null) {
|
||||
// if not configured for login, don't consider existing tokens
|
||||
if (loginIdentityProvider == null) {
|
||||
throw new IllegalStateException("Login not supported.");
|
||||
}
|
||||
|
||||
// look for the principal
|
||||
final String principal = jwtService.getAuthentication(httpServletRequest);
|
||||
if (principal == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException("Unable to issue token as issue token as no credentials were found in the request.");
|
||||
}
|
||||
|
||||
// create the authentication token
|
||||
loginAuthenticationToken = new LoginAuthenticationToken(principal, loginIdentityProvider.getExpiration());
|
||||
} else {
|
||||
// extract the principal
|
||||
final String principal = principalExtractor.extractPrincipal(certificate).toString();
|
||||
|
||||
try {
|
||||
certificateValidator.validateClientCertificate(httpServletRequest, certificate);
|
||||
} catch (CertificateExpiredException cee) {
|
||||
throw new IllegalArgumentException(String.format("Client certificate for (%s) is expired.", principal), cee);
|
||||
} catch (CertificateNotYetValidException cnyve) {
|
||||
throw new IllegalArgumentException(String.format("Client certificate for (%s) is not yet valid.", principal), cnyve);
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalArgumentException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
// authorize the proxy if necessary
|
||||
authorizeProxyIfNecessary(ProxiedEntitiesUtils.buildProxyChain(httpServletRequest, principal));
|
||||
|
||||
// create the authentication token
|
||||
loginAuthenticationToken = new LoginAuthenticationToken(principal, loginIdentityProvider.getExpiration());
|
||||
// if there is not certificate, consider login credentials
|
||||
if (certificates == null) {
|
||||
// ensure we have login credentials
|
||||
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
|
||||
throw new IllegalArgumentException("The username and password must be specified.");
|
||||
}
|
||||
} else {
|
||||
|
||||
try {
|
||||
// attempt to authenticate
|
||||
final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password));
|
||||
|
||||
// create the authentication token
|
||||
loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getUsername(), loginIdentityProvider.getExpiration());
|
||||
loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getUsername(), authenticationResponse.getExpiration());
|
||||
} catch (final InvalidLoginCredentialsException ilce) {
|
||||
throw new IllegalArgumentException("The supplied username and password are not valid.", ilce);
|
||||
} catch (final IdentityAccessException iae) {
|
||||
throw new AdministrationException(iae.getMessage(), iae);
|
||||
}
|
||||
} else {
|
||||
// consider a certificate
|
||||
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
|
||||
|
||||
// get the proxy chain and ensure its populated
|
||||
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(httpServletRequest, authenticationResponse.getIdentity());
|
||||
if (proxyChain.isEmpty()) {
|
||||
logger.error(String.format("Unable to parse the proxy chain %s from the incoming request.", authenticationResponse.getIdentity()));
|
||||
throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
|
||||
}
|
||||
|
||||
// authorize the proxy if necessary
|
||||
authorizeProxyIfNecessary(proxyChain);
|
||||
|
||||
// create the authentication token
|
||||
loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration());
|
||||
}
|
||||
|
||||
// generate JWT for response
|
||||
|
@ -371,11 +362,14 @@ public class AccessResource extends ApplicationResource {
|
|||
userDetailsService.loadUserDetails(new NiFiAuthenticationRequestToken(proxyChain));
|
||||
} catch (final UsernameNotFoundException unfe) {
|
||||
// if a username not found exception was thrown, the proxies were authorized and now
|
||||
// we can issue a new ID token to the end user
|
||||
// we can issue a new token to the end user which they will use to identify themselves
|
||||
// when they enter a new account request
|
||||
} catch (final AuthenticationServiceException ase) {
|
||||
// throw an administration exception which will return a 500
|
||||
throw new AdministrationException(ase.getMessage(), ase);
|
||||
} catch (final Exception e) {
|
||||
// any other issue we're going to treat as an authentication exception which will return 401
|
||||
throw new AdministrationException(e.getMessage(), e) {
|
||||
};
|
||||
// any other issue we're going to treat as access denied exception which will return 403
|
||||
throw new AccessDeniedException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -393,16 +387,12 @@ public class AccessResource extends ApplicationResource {
|
|||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
|
||||
this.certificateValidator = certificateValidator;
|
||||
}
|
||||
|
||||
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
|
||||
this.certificateExtractor = certificateExtractor;
|
||||
}
|
||||
|
||||
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
|
||||
this.principalExtractor = principalExtractor;
|
||||
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
|
||||
this.certificateIdentityProvider = certificateIdentityProvider;
|
||||
}
|
||||
|
||||
public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService) {
|
||||
|
|
|
@ -53,6 +53,8 @@ import org.apache.nifi.web.util.WebUtils;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.apache.nifi.user.NiFiUser;
|
||||
import org.apache.nifi.web.security.user.NiFiUserUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
@ -363,9 +365,9 @@ public abstract class ApplicationResource {
|
|||
if (httpServletRequest.isSecure()) {
|
||||
|
||||
// add the certificate DN to the proxy chain
|
||||
final String xProxiedEntitiesChain = ProxiedEntitiesUtils.getXProxiedEntitiesChain(httpServletRequest);
|
||||
if (StringUtils.isNotBlank(xProxiedEntitiesChain)) {
|
||||
result.put(PROXIED_ENTITIES_CHAIN_HTTP_HEADER, xProxiedEntitiesChain);
|
||||
final NiFiUser user = NiFiUserUtils.getNiFiUser();
|
||||
if (user != null) {
|
||||
result.put(PROXIED_ENTITIES_CHAIN_HTTP_HEADER, ProxiedEntitiesUtils.buildProxiedEntitiesChainString(user));
|
||||
}
|
||||
|
||||
// add the user's authorities (if any) to the headers
|
||||
|
|
|
@ -823,7 +823,7 @@ public class ControllerFacade {
|
|||
final Map<String, String> attributes = event.getAttributes();
|
||||
|
||||
// calculate the dn chain
|
||||
final List<String> dnChain = ProxiedEntitiesUtils.getXProxiedEntitiesChain(user);
|
||||
final List<String> dnChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(user);
|
||||
|
||||
// ensure the users in this chain are allowed to download this content
|
||||
final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes);
|
||||
|
|
|
@ -243,9 +243,8 @@
|
|||
</bean>
|
||||
<bean id="accessResource" class="org.apache.nifi.web.api.AccessResource" scope="singleton">
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
<property name="certificateValidator" ref="certificateValidator"/>
|
||||
<property name="certificateExtractor" ref="certificateExtractor"/>
|
||||
<property name="principalExtractor" ref="principalExtractor"/>
|
||||
<property name="certificateIdentityProvider" ref="certificateIdentityProvider"/>
|
||||
<property name="loginIdentityProvider" ref="loginIdentityProvider"/>
|
||||
<property name="jwtService" ref="jwtService"/>
|
||||
<property name="userDetailsService" ref="userDetailsService"/>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ import org.apache.nifi.authorization.DownloadAuthorization;
|
|||
*/
|
||||
public class NiFiTestAuthorizationProvider implements AuthorityProvider {
|
||||
|
||||
private Map<String, Set<Authority>> users;
|
||||
private final Map<String, Set<Authority>> users;
|
||||
|
||||
/**
|
||||
* Creates a new FileAuthorizationProvider.
|
||||
|
@ -48,6 +48,7 @@ public class NiFiTestAuthorizationProvider implements AuthorityProvider {
|
|||
users.put("CN=Lastname Firstname Middlename monitor, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown", EnumSet.of(Authority.ROLE_MONITOR));
|
||||
users.put("CN=Lastname Firstname Middlename dfm, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown", EnumSet.of(Authority.ROLE_DFM));
|
||||
users.put("CN=Lastname Firstname Middlename admin, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown", EnumSet.of(Authority.ROLE_ADMIN));
|
||||
users.put("user@nifi", EnumSet.of(Authority.ROLE_DFM));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -78,8 +78,12 @@ public class NiFiTestServer {
|
|||
private void createSecureConnector() {
|
||||
org.eclipse.jetty.util.ssl.SslContextFactory contextFactory = new org.eclipse.jetty.util.ssl.SslContextFactory();
|
||||
|
||||
// need client auth
|
||||
contextFactory.setNeedClientAuth(properties.getNeedClientAuth());
|
||||
// require client auth when not supporting login or anonymous access
|
||||
if (StringUtils.isBlank(properties.getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER)) && properties.getAnonymousAuthorities().isEmpty()) {
|
||||
contextFactory.setNeedClientAuth(true);
|
||||
} else {
|
||||
contextFactory.setWantClientAuth(true);
|
||||
}
|
||||
|
||||
/* below code sets JSSE system properties when values are provided */
|
||||
// keystore properties
|
||||
|
@ -163,7 +167,6 @@ public class NiFiTestServer {
|
|||
}
|
||||
|
||||
public Client getClient() {
|
||||
// create the client
|
||||
return WebUtils.createClient(null, SslContextFactory.createSslContext(properties));
|
||||
}
|
||||
|
||||
|
|
|
@ -34,9 +34,27 @@ public class NiFiTestUser {
|
|||
private final Client client;
|
||||
private final String proxyDn;
|
||||
|
||||
public NiFiTestUser(Client client, String dn) {
|
||||
public NiFiTestUser(Client client, String proxyDn) {
|
||||
this.client = client;
|
||||
this.proxyDn = ProxiedEntitiesUtils.formatProxyDn(dn);
|
||||
if (proxyDn != null) {
|
||||
this.proxyDn = ProxiedEntitiesUtils.formatProxyDn(proxyDn);
|
||||
} else {
|
||||
this.proxyDn = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally adds the proxied entities chain.
|
||||
*
|
||||
* @param builder the resource builder
|
||||
* @return the resource builder
|
||||
*/
|
||||
private WebResource.Builder addProxiedEntities(final WebResource.Builder builder) {
|
||||
if (proxyDn == null) {
|
||||
return builder;
|
||||
} else {
|
||||
return builder.header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,6 +76,18 @@ public class NiFiTestUser {
|
|||
* @return response
|
||||
*/
|
||||
public ClientResponse testGet(String url, Map<String, String> queryParams) {
|
||||
return testGetWithHeaders(url, queryParams, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET using the specified url and query parameters.
|
||||
*
|
||||
* @param url url
|
||||
* @param queryParams params
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
*/
|
||||
public ClientResponse testGetWithHeaders(String url, Map<String, String> queryParams, Map<String, String> headers) {
|
||||
// get the resource
|
||||
WebResource resource = client.resource(url);
|
||||
|
||||
|
@ -68,8 +98,18 @@ public class NiFiTestUser {
|
|||
}
|
||||
}
|
||||
|
||||
// get the builder
|
||||
WebResource.Builder builder = addProxiedEntities(resource.accept(MediaType.APPLICATION_JSON));
|
||||
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
builder = builder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the query
|
||||
return resource.accept(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn).get(ClientResponse.class);
|
||||
return builder.get(ClientResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,14 +132,34 @@ public class NiFiTestUser {
|
|||
* @throws Exception ex
|
||||
*/
|
||||
public ClientResponse testPost(String url, Object entity) throws Exception {
|
||||
return testPostWithHeaders(url, entity, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a POST using the specified url and entity body.
|
||||
*
|
||||
* @param url url
|
||||
* @param entity entity
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
* @throws Exception ex
|
||||
*/
|
||||
public ClientResponse testPostWithHeaders(String url, Object entity, Map<String, String> headers) throws Exception {
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON));
|
||||
|
||||
// include the request entity
|
||||
if (entity != null) {
|
||||
resourceBuilder = resourceBuilder.entity(entity);
|
||||
}
|
||||
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
resourceBuilder = resourceBuilder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the request
|
||||
return resourceBuilder.post(ClientResponse.class);
|
||||
}
|
||||
|
@ -109,18 +169,38 @@ public class NiFiTestUser {
|
|||
*
|
||||
* @param url url
|
||||
* @param entity entity
|
||||
* @return repsonse
|
||||
* @return response
|
||||
* @throws Exception ex
|
||||
*/
|
||||
public ClientResponse testPostMultiPart(String url, Object entity) throws Exception {
|
||||
return testPostMultiPartWithHeaders(url, entity, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a POST using the specified url and entity body.
|
||||
*
|
||||
* @param url url
|
||||
* @param entity entity
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
* @throws Exception ex
|
||||
*/
|
||||
public ClientResponse testPostMultiPartWithHeaders(String url, Object entity, Map<String, String> headers) throws Exception {
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_XML).type(MediaType.MULTIPART_FORM_DATA).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_XML).type(MediaType.MULTIPART_FORM_DATA));
|
||||
|
||||
// include the request entity
|
||||
if (entity != null) {
|
||||
resourceBuilder = resourceBuilder.entity(entity);
|
||||
}
|
||||
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
resourceBuilder = resourceBuilder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the request
|
||||
return resourceBuilder.post(ClientResponse.class);
|
||||
}
|
||||
|
@ -134,6 +214,19 @@ public class NiFiTestUser {
|
|||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testPost(String url, Map<String, String> formData) throws Exception {
|
||||
return testPostWithHeaders(url, formData, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a POST using the specified url and form data.
|
||||
*
|
||||
* @param url url
|
||||
* @param formData form data
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testPostWithHeaders(String url, Map<String, String> formData, Map<String, String> headers) throws Exception {
|
||||
// convert the form data
|
||||
MultivaluedMapImpl entity = new MultivaluedMapImpl();
|
||||
for (String key : formData.keySet()) {
|
||||
|
@ -141,14 +234,20 @@ public class NiFiTestUser {
|
|||
}
|
||||
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder
|
||||
= client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED));
|
||||
|
||||
// add the form data if necessary
|
||||
if (!entity.isEmpty()) {
|
||||
resourceBuilder = resourceBuilder.entity(entity);
|
||||
}
|
||||
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
resourceBuilder = resourceBuilder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the request
|
||||
return resourceBuilder.post(ClientResponse.class);
|
||||
}
|
||||
|
@ -162,14 +261,34 @@ public class NiFiTestUser {
|
|||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testPut(String url, Object entity) throws Exception {
|
||||
return testPutWithHeaders(url, entity, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a PUT using the specified url and entity body.
|
||||
*
|
||||
* @param url url
|
||||
* @param entity entity
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testPutWithHeaders(String url, Object entity, Map<String, String> headers) throws Exception {
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON));
|
||||
|
||||
// include the request entity
|
||||
if (entity != null) {
|
||||
resourceBuilder = resourceBuilder.entity(entity);
|
||||
}
|
||||
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
resourceBuilder = resourceBuilder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the request
|
||||
return resourceBuilder.put(ClientResponse.class);
|
||||
}
|
||||
|
@ -183,6 +302,19 @@ public class NiFiTestUser {
|
|||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testPut(String url, Map<String, String> formData) throws Exception {
|
||||
return testPutWithHeaders(url, formData, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a PUT using the specified url and form data.
|
||||
*
|
||||
* @param url url
|
||||
* @param formData form data
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testPutWithHeaders(String url, Map<String, String> formData, Map<String, String> headers) throws Exception {
|
||||
// convert the form data
|
||||
MultivaluedMapImpl entity = new MultivaluedMapImpl();
|
||||
for (String key : formData.keySet()) {
|
||||
|
@ -190,14 +322,20 @@ public class NiFiTestUser {
|
|||
}
|
||||
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder
|
||||
= client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED));
|
||||
|
||||
// add the form data if necessary
|
||||
if (!entity.isEmpty()) {
|
||||
resourceBuilder = resourceBuilder.entity(entity);
|
||||
}
|
||||
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
resourceBuilder = resourceBuilder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the request
|
||||
return resourceBuilder.put(ClientResponse.class);
|
||||
}
|
||||
|
@ -210,24 +348,26 @@ public class NiFiTestUser {
|
|||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testDelete(String url) throws Exception {
|
||||
return testDelete(url, (Object) null);
|
||||
return testDelete(url, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a DELETE using the specified url and entity.
|
||||
*
|
||||
* @param url url
|
||||
* @param entity entity
|
||||
* @return repsonse
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
* @throws java.lang.Exception ex
|
||||
*/
|
||||
public ClientResponse testDelete(String url, Object entity) throws Exception {
|
||||
public ClientResponse testDeleteWithHeaders(String url, Map<String, String> headers) throws Exception {
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder = client.resource(url).accept(MediaType.APPLICATION_JSON).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn);
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.APPLICATION_JSON));
|
||||
|
||||
// append any query parameters
|
||||
if (entity != null) {
|
||||
resourceBuilder = resourceBuilder.entity(entity);
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
resourceBuilder = resourceBuilder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the query
|
||||
|
@ -254,7 +394,56 @@ public class NiFiTestUser {
|
|||
}
|
||||
|
||||
// perform the request
|
||||
return resource.accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED).header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxyDn).delete(ClientResponse.class);
|
||||
return addProxiedEntities(resource.accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_FORM_URLENCODED)).delete(ClientResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a token with the specified username and password.
|
||||
*
|
||||
* @param url the url
|
||||
* @param username the username
|
||||
* @param password the password
|
||||
* @return response
|
||||
* @throws Exception ex
|
||||
*/
|
||||
public ClientResponse testCreateToken(String url, String username, String password) throws Exception {
|
||||
// convert the form data
|
||||
MultivaluedMapImpl entity = new MultivaluedMapImpl();
|
||||
entity.add("username", username);
|
||||
entity.add("password", password);
|
||||
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.TEXT_PLAIN).type(MediaType.APPLICATION_FORM_URLENCODED)).entity(entity);
|
||||
|
||||
// perform the request
|
||||
return resourceBuilder.post(ClientResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a token with the specified username and password.
|
||||
*
|
||||
* @param url the url
|
||||
* @param justification justification
|
||||
* @param headers http headers
|
||||
* @return response
|
||||
* @throws Exception ex
|
||||
*/
|
||||
public ClientResponse testRegisterUser(String url, String justification, Map<String, String> headers) throws Exception {
|
||||
// convert the form data
|
||||
MultivaluedMapImpl entity = new MultivaluedMapImpl();
|
||||
entity.add("justification", justification);
|
||||
|
||||
// get the resource
|
||||
WebResource.Builder resourceBuilder = addProxiedEntities(client.resource(url).accept(MediaType.TEXT_PLAIN).type(MediaType.APPLICATION_FORM_URLENCODED)).entity(entity);
|
||||
|
||||
// append any headers
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (String key : headers.keySet()) {
|
||||
resourceBuilder = resourceBuilder.header(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
// perform the request
|
||||
return resourceBuilder.post(ClientResponse.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
|
@ -13,6 +13,12 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<services>
|
||||
|
||||
</services>
|
||||
<!--
|
||||
This file lists all authority providers to use when running securely.
|
||||
-->
|
||||
<loginIdentityProviders>
|
||||
<provider>
|
||||
<identifier>test-provider</identifier>
|
||||
<class>org.apache.nifi.integration.util.NiFiTestLoginIdentityProvider</class>
|
||||
</provider>
|
||||
</loginIdentityProviders>
|
|
@ -14,16 +14,15 @@
|
|||
# limitations under the License.
|
||||
|
||||
# Core Properties #
|
||||
nifi.version=nifi 0.2.1-SNAPSHOT
|
||||
nifi.version=nifi version
|
||||
nifi.flow.configuration.file=
|
||||
nifi.flow.configuration.archive.dir=target/archive
|
||||
nifi.flowcontroller.autoResumeState=true
|
||||
nifi.flowcontroller.graceful.shutdown.period=10 sec
|
||||
nifi.flowservice.writedelay.interval=2 sec
|
||||
|
||||
nifi.reporting.task.configuration.file=target/test-classes/access-control/reporting-tasks.xml
|
||||
nifi.controller.service.configuration.file=target/test-classes/access-control/controller-services.xml
|
||||
nifi.authority.provider.configuration.file=target/test-classes/access-control/authority-providers.xml
|
||||
nifi.login.identity.provider.configuration.file=target/test-classes/access-control/login-identity-providers.xml
|
||||
nifi.templates.directory=target/test-classes/access-control/templates
|
||||
nifi.ui.banner.text=TEST BANNER
|
||||
nifi.ui.autorefresh.interval=30 sec
|
||||
|
@ -93,11 +92,11 @@ nifi.security.truststoreType=JKS
|
|||
nifi.security.truststorePasswd=localtest
|
||||
nifi.security.needClientAuth=true
|
||||
nifi.security.user.authority.provider=test-provider
|
||||
nifi.security.user.login.identity.provider=
|
||||
nifi.security.user.login.identity.provider=test-provider
|
||||
nifi.security.authorizedUsers.file=target/test-classes/access-control/users.xml
|
||||
nifi.security.user.credential.cache.duration=1 hr
|
||||
nifi.security.support.new.account.requests=
|
||||
nifi.security.default.user.roles=
|
||||
nifi.security.anonymous.authorities=
|
||||
|
||||
# cluster common properties (cluster manager and nodes must have same values) #
|
||||
nifi.cluster.protocol.heartbeat.interval=5 sec
|
||||
|
|
|
@ -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>
|
|
@ -29,12 +29,14 @@ import javax.servlet.http.HttpServletResponse;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.user.NiFiUser;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
|
||||
import org.apache.nifi.web.security.user.NiFiUserUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.authentication.AccountStatusException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
@ -85,8 +87,13 @@ public abstract class NiFiAuthenticationFilter implements Filter {
|
|||
|
||||
private void authenticate(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
|
||||
try {
|
||||
final Authentication authenticated = attemptAuthentication(request, response);
|
||||
final NiFiAuthenticationRequestToken authenticated = attemptAuthentication(request, response);
|
||||
if (authenticated != null) {
|
||||
// log the request attempt - response details will be logged later
|
||||
logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)",
|
||||
ProxiedEntitiesUtils.formatProxyDn(StringUtils.join(authenticated.getChain(), "><")), request.getMethod(),
|
||||
request.getRequestURL().toString(), request.getRemoteAddr()));
|
||||
|
||||
final Authentication authorized = authenticationManager.authenticate(authenticated);
|
||||
successfulAuthorization(request, response, authorized);
|
||||
}
|
||||
|
@ -97,7 +104,7 @@ public abstract class NiFiAuthenticationFilter implements Filter {
|
|||
}
|
||||
}
|
||||
|
||||
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response);
|
||||
public abstract NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response);
|
||||
|
||||
protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
@ -127,6 +134,9 @@ public abstract class NiFiAuthenticationFilter implements Filter {
|
|||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
out.println("Access is denied.");
|
||||
}
|
||||
} else if (ae instanceof BadCredentialsException) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
out.println(ae.getMessage());
|
||||
} else if (ae instanceof AccountStatusException) {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
out.println(ae.getMessage());
|
||||
|
|
|
@ -47,10 +47,12 @@ public class NiFiAuthenticationProvider implements AuthenticationProvider {
|
|||
final UserDetails userDetails = userDetailsService.loadUserDetails(request);
|
||||
|
||||
// build an authentication for accesing nifi
|
||||
return new NiFiAuthorizationToken(userDetails);
|
||||
final NiFiAuthorizationToken result = new NiFiAuthorizationToken(userDetails);
|
||||
result.setDetails(request.getDetails());
|
||||
return result;
|
||||
} catch (final UsernameNotFoundException unfe) {
|
||||
// if the result was an authenticated new account request and it could not be authorized because the user was not found,
|
||||
// return the token so the new account could be created. this must go here to ensure that any proxies have been authorized
|
||||
// if the authentication request is for a new account and it could not be authorized because the user was not found,
|
||||
// return the token so the new account could be created. this must go here toe nsure that any proxies have been authorized
|
||||
if (isNewAccountAuthenticationToken(request)) {
|
||||
return new NewAccountAuthenticationToken(((NewAccountAuthenticationRequestToken) authentication).getNewAccountRequest());
|
||||
} else {
|
||||
|
|
|
@ -16,15 +16,12 @@
|
|||
*/
|
||||
package org.apache.nifi.web.security;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.nifi.web.security.x509.SubjectDnX509PrincipalExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.user.NiFiUser;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
@ -42,33 +39,50 @@ public class ProxiedEntitiesUtils {
|
|||
private static final Pattern proxyChainPattern = Pattern.compile("<(.*?)>");
|
||||
|
||||
/**
|
||||
* @param request http request
|
||||
* @return the X-ProxiedEntitiesChain from the specified request
|
||||
* Formats the specified DN to be set as a HTTP header using well known conventions.
|
||||
*
|
||||
* @param dn raw dn
|
||||
* @return the dn formatted as an HTTP header
|
||||
*/
|
||||
public static String getXProxiedEntitiesChain(final HttpServletRequest request) {
|
||||
String xProxiedEntitiesChain = request.getHeader("X-ProxiedEntitiesChain");
|
||||
final X509Certificate cert = new X509CertificateExtractor().extractClientCertificate(request);
|
||||
if (cert != null) {
|
||||
final SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
|
||||
final String extractedPrincipal = principalExtractor.extractPrincipal(cert).toString();
|
||||
final String formattedPrincipal = formatProxyDn(extractedPrincipal);
|
||||
if (StringUtils.isBlank(xProxiedEntitiesChain)) {
|
||||
xProxiedEntitiesChain = formattedPrincipal;
|
||||
} else {
|
||||
xProxiedEntitiesChain += formattedPrincipal;
|
||||
}
|
||||
}
|
||||
|
||||
return xProxiedEntitiesChain;
|
||||
public static String formatProxyDn(String dn) {
|
||||
return "<" + dn + ">";
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the dn chain for the specified user.
|
||||
* Tokenizes the specified proxy chain.
|
||||
*
|
||||
* @param rawProxyChain raw chain
|
||||
* @return tokenized proxy chain
|
||||
*/
|
||||
public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) {
|
||||
final List<String> proxyChain = new ArrayList<>();
|
||||
final Matcher rawProxyChainMatcher = proxyChainPattern.matcher(rawProxyChain);
|
||||
while (rawProxyChainMatcher.find()) {
|
||||
proxyChain.add(rawProxyChainMatcher.group(1));
|
||||
}
|
||||
|
||||
return proxyChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the proxy chain for the specified user.
|
||||
*
|
||||
* @param user The current user
|
||||
* @return The dn chain for that user
|
||||
* @return The proxy chain for that user in String form
|
||||
*/
|
||||
public static List<String> getXProxiedEntitiesChain(final NiFiUser user) {
|
||||
public static String buildProxiedEntitiesChainString(final NiFiUser user) {
|
||||
// calculate the dn chain
|
||||
final List<String> proxyChain = buildProxiedEntitiesChain(user);
|
||||
return formatProxyDn(StringUtils.join(proxyChain, "><"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the proxy chain for the specified user.
|
||||
*
|
||||
* @param user The current user
|
||||
* @return The proxy chain for that user in List form
|
||||
*/
|
||||
public static List<String> buildProxiedEntitiesChain(final NiFiUser user) {
|
||||
// calculate the dn chain
|
||||
final List<String> proxyChain = new ArrayList<>();
|
||||
|
||||
|
@ -86,56 +100,25 @@ public class ProxiedEntitiesUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Formats the specified DN to be set as a HTTP header using well known conventions.
|
||||
* Builds the proxy chain from the specified request and user.
|
||||
*
|
||||
* @param dn raw dn
|
||||
* @return the dn formatted as an HTTP header
|
||||
* @param request the request
|
||||
* @param username the username
|
||||
* @return the proxy chain in list form
|
||||
*/
|
||||
public static String formatProxyDn(String dn) {
|
||||
return "<" + dn + ">";
|
||||
public static List<String> buildProxiedEntitiesChain(final HttpServletRequest request, final String username) {
|
||||
final String chain = buildProxiedEntitiesChainString(request, username);
|
||||
return tokenizeProxiedEntitiesChain(chain);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Tokenizes the specified proxy chain.
|
||||
// *
|
||||
// * @param rawProxyChain raw chain
|
||||
// * @return tokenized proxy chain
|
||||
// */
|
||||
// public static Deque<String> tokenizeProxyChain(String rawProxyChain) {
|
||||
// final Deque<String> dnList = new ArrayDeque<>();
|
||||
//
|
||||
// // parse the proxy chain
|
||||
// final Matcher rawProxyChainMatcher = proxyChainPattern.matcher(rawProxyChain);
|
||||
// while (rawProxyChainMatcher.find()) {
|
||||
// dnList.push(rawProxyChainMatcher.group(1));
|
||||
// }
|
||||
//
|
||||
// return dnList;
|
||||
// }
|
||||
public static List<String> buildProxyChain(final HttpServletRequest request, final String username) {
|
||||
String principal;
|
||||
if (username.startsWith("<") && username.endsWith(">")) {
|
||||
principal = username;
|
||||
} else {
|
||||
principal = formatProxyDn(username);
|
||||
}
|
||||
|
||||
// look for a proxied user
|
||||
if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
|
||||
principal = request.getHeader(PROXY_ENTITIES_CHAIN) + principal;
|
||||
}
|
||||
|
||||
// parse the proxy chain
|
||||
final List<String> proxyChain = new ArrayList<>();
|
||||
final Matcher rawProxyChainMatcher = proxyChainPattern.matcher(principal);
|
||||
while (rawProxyChainMatcher.find()) {
|
||||
proxyChain.add(rawProxyChainMatcher.group(1));
|
||||
}
|
||||
|
||||
return proxyChain;
|
||||
}
|
||||
|
||||
public static String extractProxiedEntitiesChain(final HttpServletRequest request, final String username) {
|
||||
/**
|
||||
* Builds the dn chain from the specified request and user.
|
||||
*
|
||||
* @param request the request
|
||||
* @param username the username
|
||||
* @return the dn chain in string form
|
||||
*/
|
||||
public static String buildProxiedEntitiesChainString(final HttpServletRequest request, final String username) {
|
||||
String principal;
|
||||
if (username.startsWith("<") && username.endsWith(">")) {
|
||||
principal = username;
|
||||
|
|
|
@ -54,10 +54,8 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
|
|||
/**
|
||||
* Loads the user details for the specified dn.
|
||||
*
|
||||
* Synchronizing because we want each request to be authorized atomically
|
||||
* since each may contain any number of DNs. We wanted an access decision
|
||||
* made for each individual request as a whole (without other request
|
||||
* potentially impacting it).
|
||||
* Synchronizing because we want each request to be authorized atomically since each may contain any number of DNs. We wanted an access decision made for each individual request as a whole
|
||||
* (without other request potentially impacting it).
|
||||
*
|
||||
* @param request request
|
||||
* @return user details
|
||||
|
@ -109,9 +107,6 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
|
|||
|
||||
// attempt to create a new user account for the proxying client
|
||||
userService.createPendingUserAccount(dn, "Automatic account request generated for unknown proxy.");
|
||||
|
||||
// propagate the exception to return the appropriate response
|
||||
throw new UsernameNotFoundException(String.format("An account request was generated for the proxy '%s'.", dn));
|
||||
} catch (AdministrationException ae) {
|
||||
throw new AuthenticationServiceException(String.format("Unable to create an account request for '%s': %s", dn, ae.getMessage()), ae);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
|
@ -122,10 +117,10 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
|
|||
throw new AccountStatusException(message) {
|
||||
};
|
||||
}
|
||||
} else {
|
||||
logger.warn(String.format("Untrusted proxy '%s' must be authorized with '%s' authority: %s", dn, Authority.ROLE_PROXY.toString(), unfe.getMessage()));
|
||||
throw new UntrustedProxyException(String.format("Untrusted proxy '%s' must be authorized with '%s'.", dn, Authority.ROLE_PROXY.toString()));
|
||||
}
|
||||
|
||||
logger.warn(String.format("Untrusted proxy '%s' must be authorized with '%s' authority: %s", dn, Authority.ROLE_PROXY.toString(), unfe.getMessage()));
|
||||
throw new UntrustedProxyException(String.format("Untrusted proxy '%s' must be authorized with '%s'.", dn, Authority.ROLE_PROXY.toString()));
|
||||
} catch (AuthenticationException ae) {
|
||||
logger.warn(String.format("Untrusted proxy '%s' must be authorized with '%s' authority: %s", dn, Authority.ROLE_PROXY.toString(), ae.getMessage()));
|
||||
throw new UntrustedProxyException(String.format("Untrusted proxy '%s' must be authorized with '%s'.", dn, Authority.ROLE_PROXY.toString()));
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
|
|||
import org.apache.nifi.web.security.user.NewAccountRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
*/
|
||||
|
@ -36,7 +35,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
|
|||
private JwtService jwtService;
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
|
||||
public NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
|
||||
// only suppport jwt login when running securely
|
||||
if (!request.isSecure()) {
|
||||
return null;
|
||||
|
|
|
@ -82,7 +82,7 @@ public class JwtService {
|
|||
/**
|
||||
* Generates a signed JWT token from the provided (Spring Security) login authentication token.
|
||||
*
|
||||
* @param authenticationToken
|
||||
* @param authenticationToken the authentication token
|
||||
* @return a signed JWT containing the user identity and the identity provider, Base64-encoded
|
||||
*/
|
||||
public String generateSignedToken(final LoginAuthenticationToken authenticationToken) {
|
||||
|
|
|
@ -26,34 +26,26 @@ import javax.servlet.ServletResponse;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.apache.nifi.controller.FlowController;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.authentication.AuthenticationResponse;
|
||||
import org.apache.nifi.web.security.user.NiFiUserDetails;
|
||||
import org.apache.nifi.web.security.x509.SubjectDnX509PrincipalExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
import org.apache.nifi.user.NiFiUser;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.token.NiFiAuthorizationToken;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509IdentityProvider;
|
||||
import org.apache.nifi.web.util.WebUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.web.context.support.WebApplicationContextUtils;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
|
||||
/**
|
||||
* Custom filter to extract a user's authorities from the request where the user
|
||||
* was authenticated by the cluster manager and populate the threadlocal with
|
||||
* the authorized user. If the request contains the appropriate header with
|
||||
* authorities and the application instance is a node connected to the cluster,
|
||||
* then the authentication/authorization steps remaining in the filter chain are
|
||||
* skipped.
|
||||
* Custom filter to extract a user's authorities from the request where the user was authenticated by the cluster manager and populate the threadlocal with the authorized user. If the request contains
|
||||
* the appropriate header with authorities and the application instance is a node connected to the cluster, then the authentication/authorization steps remaining in the filter chain are skipped.
|
||||
*
|
||||
* Checking if the application instance is a connected node is important because
|
||||
* it prevents external clients from faking the request headers and bypassing
|
||||
* the authentication processing chain.
|
||||
* Checking if the application instance is a connected node is important because it prevents external clients from faking the request headers and bypassing the authentication processing chain.
|
||||
*/
|
||||
public class NodeAuthorizedUserFilter extends GenericFilterBean {
|
||||
|
||||
|
@ -61,14 +53,9 @@ public class NodeAuthorizedUserFilter extends GenericFilterBean {
|
|||
|
||||
public static final String PROXY_USER_DETAILS = "X-ProxiedEntityUserDetails";
|
||||
|
||||
private final NiFiProperties properties;
|
||||
private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
|
||||
private final X509CertificateExtractor certificateExtractor = new X509CertificateExtractor();
|
||||
private final X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
|
||||
|
||||
public NodeAuthorizedUserFilter(NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
private NiFiProperties properties;
|
||||
private X509CertificateExtractor certificateExtractor;
|
||||
private X509IdentityProvider certificateIdentityProvider;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
|
@ -87,16 +74,15 @@ public class NodeAuthorizedUserFilter extends GenericFilterBean {
|
|||
// check that we are connected to the cluster
|
||||
if (flowController.getNodeId() != null) {
|
||||
try {
|
||||
// get the DN from the cert in the request
|
||||
final X509Certificate certificate = certificateExtractor.extractClientCertificate((HttpServletRequest) request);
|
||||
// attempt to extract the client certificate
|
||||
final X509Certificate[] certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
|
||||
if (certificate != null) {
|
||||
// extract the principal from the certificate
|
||||
final Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
|
||||
final String dn = certificatePrincipal.toString();
|
||||
// authenticate the certificate
|
||||
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificate);
|
||||
|
||||
// only consider the pre-authorized user when the request came from the NCM according to the DN in the certificate
|
||||
final String clusterManagerDN = flowController.getClusterManagerDN();
|
||||
if (clusterManagerDN != null && clusterManagerDN.equals(dn)) {
|
||||
// only consider the pre-authorized user when the request came directly from the NCM according to the DN in the certificate
|
||||
final String clusterManagerIdentity = flowController.getClusterManagerDN();
|
||||
if (clusterManagerIdentity != null && clusterManagerIdentity.equals(authenticationResponse.getIdentity())) {
|
||||
// deserialize hex encoded object
|
||||
final Serializable userDetailsObj = WebUtils.deserializeHexToObject(hexEncodedUserDetails);
|
||||
|
||||
|
@ -109,19 +95,33 @@ public class NodeAuthorizedUserFilter extends GenericFilterBean {
|
|||
logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", user.getIdentity(), httpServletRequest.getMethod(),
|
||||
httpServletRequest.getRequestURL().toString(), request.getRemoteAddr()));
|
||||
|
||||
// we do not create the authentication token with the X509 certificate because the certificate is from the sending system, not the proxied user
|
||||
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
token.setDetails(authenticationDetailsSource.buildDetails(request));
|
||||
// create the authorized nifi token
|
||||
final NiFiAuthorizationToken token = new NiFiAuthorizationToken(userDetails);
|
||||
SecurityContextHolder.getContext().setAuthentication(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (final ClassNotFoundException cnfe) {
|
||||
LOGGER.warn("Classpath issue detected because failed to deserialize authorized user in request header due to: " + cnfe, cnfe);
|
||||
} catch (final IllegalArgumentException iae) {
|
||||
// unable to authenticate a serialized user from the incoming request
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
public void setProperties(NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
|
||||
this.certificateIdentityProvider = certificateIdentityProvider;
|
||||
}
|
||||
|
||||
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
|
||||
this.certificateExtractor = certificateExtractor;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -266,13 +266,6 @@ public class LoginIdentityProviderFactoryBean implements FactoryBean, Disposable
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getExpiration() {
|
||||
try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) {
|
||||
return baseProvider.getExpiration();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
|
||||
try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) {
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
*/
|
||||
package org.apache.nifi.web.security.x509;
|
||||
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.nifi.authentication.AuthenticationResponse;
|
||||
import org.apache.nifi.web.security.NiFiAuthenticationFilter;
|
||||
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
|
||||
import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken;
|
||||
|
@ -29,8 +28,7 @@ import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
|
|||
import org.apache.nifi.web.security.user.NewAccountRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
|
||||
/**
|
||||
* Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate.
|
||||
|
@ -39,54 +37,31 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
|
|||
|
||||
private static final Logger logger = LoggerFactory.getLogger(X509AuthenticationFilter.class);
|
||||
|
||||
private X509PrincipalExtractor principalExtractor;
|
||||
private X509CertificateExtractor certificateExtractor;
|
||||
private X509CertificateValidator certificateValidator;
|
||||
private X509IdentityProvider certificateIdentityProvider;
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
|
||||
public NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
|
||||
// only suppport x509 login when running securely
|
||||
if (!request.isSecure()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract the cert
|
||||
X509Certificate certificate = certificateExtractor.extractClientCertificate(request);
|
||||
|
||||
// ensure the cert was found
|
||||
if (certificate == null) {
|
||||
// look for a client certificate
|
||||
final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(request);
|
||||
if (certificates == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract the principal
|
||||
Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
|
||||
final String principal = ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
|
||||
|
||||
// attempt to authenticate if certificates were found
|
||||
final AuthenticationResponse authenticationResponse;
|
||||
try {
|
||||
certificateValidator.validateClientCertificate(request, certificate);
|
||||
} catch (CertificateExpiredException cee) {
|
||||
final String message = String.format("Client certificate for (%s) is expired.", principal);
|
||||
logger.info(message, cee);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("", cee);
|
||||
}
|
||||
return null;
|
||||
} catch (CertificateNotYetValidException cnyve) {
|
||||
final String message = String.format("Client certificate for (%s) is not yet valid.", principal);
|
||||
logger.info(message, cnyve);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("", cnyve);
|
||||
}
|
||||
return null;
|
||||
} catch (final Exception e) {
|
||||
logger.info(e.getMessage());
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("", e);
|
||||
}
|
||||
return null;
|
||||
authenticationResponse = certificateIdentityProvider.authenticate(certificates);
|
||||
} catch (final IllegalArgumentException iae) {
|
||||
throw new BadCredentialsException(iae.getMessage(), iae);
|
||||
}
|
||||
|
||||
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxyChain(request, principal);
|
||||
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(request, authenticationResponse.getIdentity());
|
||||
if (isNewAccountRequest(request)) {
|
||||
return new NewAccountAuthenticationRequestToken(new NewAccountRequest(proxyChain, getJustification(request)));
|
||||
} else {
|
||||
|
@ -95,16 +70,12 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
|
|||
}
|
||||
|
||||
/* setters */
|
||||
public void setCertificateValidator(X509CertificateValidator certificateValidator) {
|
||||
this.certificateValidator = certificateValidator;
|
||||
}
|
||||
|
||||
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
|
||||
this.principalExtractor = principalExtractor;
|
||||
}
|
||||
|
||||
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
|
||||
this.certificateExtractor = certificateExtractor;
|
||||
}
|
||||
|
||||
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
|
||||
this.certificateIdentityProvider = certificateIdentityProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -35,11 +35,11 @@ public class X509CertificateExtractor {
|
|||
* @param request http request
|
||||
* @return cert
|
||||
*/
|
||||
public X509Certificate extractClientCertificate(HttpServletRequest request) {
|
||||
public X509Certificate[] extractClientCertificate(HttpServletRequest request) {
|
||||
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
|
||||
|
||||
if (certs != null && certs.length > 0) {
|
||||
return certs[0];
|
||||
return certs;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
|
|
@ -19,7 +19,6 @@ package org.apache.nifi.web.security.x509;
|
|||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.apache.nifi.web.security.x509.ocsp.CertificateStatusException;
|
||||
import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -37,20 +36,19 @@ public class X509CertificateValidator {
|
|||
/**
|
||||
* Extract the client certificate from the specified HttpServletRequest or null if none is specified.
|
||||
*
|
||||
* @param request the request
|
||||
* @param certificate the certificate
|
||||
* @param certificates the client certificates
|
||||
* @throws java.security.cert.CertificateExpiredException cert is expired
|
||||
* @throws java.security.cert.CertificateNotYetValidException cert is not yet valid
|
||||
* @throws org.apache.nifi.web.security.x509.ocsp.CertificateStatusException ocsp validation issue
|
||||
*/
|
||||
public void validateClientCertificate(final HttpServletRequest request, final X509Certificate certificate)
|
||||
public void validateClientCertificate(final X509Certificate[] certificates)
|
||||
throws CertificateExpiredException, CertificateNotYetValidException, CertificateStatusException {
|
||||
|
||||
// ensure the cert is valid
|
||||
certificate.checkValidity();
|
||||
certificates[0].checkValidity();
|
||||
|
||||
// perform ocsp validator if necessary
|
||||
ocspValidator.validate(request);
|
||||
ocspValidator.validate(certificates);
|
||||
}
|
||||
|
||||
public void setOcspValidator(OcspCertificateValidator ocspValidator) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -42,7 +42,6 @@ import javax.net.ssl.TrustManager;
|
|||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.apache.nifi.framework.security.util.SslContextFactory;
|
||||
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.ValidationStatus;
|
||||
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.VerificationStatus;
|
||||
|
@ -158,8 +157,7 @@ public class OcspCertificateValidator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Loads the trusted certificate authorities according to the specified
|
||||
* properties.
|
||||
* Loads the trusted certificate authorities according to the specified properties.
|
||||
*
|
||||
* @param properties properties
|
||||
* @return map of certificate authorities
|
||||
|
@ -208,12 +206,10 @@ public class OcspCertificateValidator {
|
|||
/**
|
||||
* Validates the specified certificate using OCSP if configured.
|
||||
*
|
||||
* @param request http request
|
||||
* @param certificates the client certificates
|
||||
* @throws CertificateStatusException ex
|
||||
*/
|
||||
public void validate(final HttpServletRequest request) throws CertificateStatusException {
|
||||
final X509Certificate[] certificates = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
|
||||
|
||||
public void validate(final X509Certificate[] certificates) throws CertificateStatusException {
|
||||
// only validate if configured to do so
|
||||
if (client != null && certificates != null && certificates.length > 0) {
|
||||
final X509Certificate subjectCertificate = getSubjectCertificate(certificates);
|
||||
|
@ -395,13 +391,9 @@ public class OcspCertificateValidator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the trusted responder certificate. The response contains the
|
||||
* responder certificate, however we cannot blindly trust it. Instead, we
|
||||
* use a configured trusted CA. If the responder certificate is a trusted
|
||||
* CA, then we can use it. If the responder certificate is not directly
|
||||
* trusted, we still may be able to trust it if it was issued by the same CA
|
||||
* that issued the subject certificate. Other various checks may be required
|
||||
* (this portion is currently not implemented).
|
||||
* Gets the trusted responder certificate. The response contains the responder certificate, however we cannot blindly trust it. Instead, we use a configured trusted CA. If the responder
|
||||
* certificate is a trusted CA, then we can use it. If the responder certificate is not directly trusted, we still may be able to trust it if it was issued by the same CA that issued the subject
|
||||
* certificate. Other various checks may be required (this portion is currently not implemented).
|
||||
*
|
||||
* @param responderCertificate cert
|
||||
* @param issuerCertificate cert
|
||||
|
|
|
@ -33,6 +33,12 @@
|
|||
<property name="ocspValidator" ref="ocspValidator"/>
|
||||
</bean>
|
||||
|
||||
<!-- x509 identity provider -->
|
||||
<bean id="certificateIdentityProvider" class="org.apache.nifi.web.security.x509.X509IdentityProvider">
|
||||
<property name="principalExtractor" ref="principalExtractor"/>
|
||||
<property name="certificateValidator" ref="certificateValidator"/>
|
||||
</bean>
|
||||
|
||||
<!-- user details service -->
|
||||
<bean id="userDetailsService" class="org.apache.nifi.web.security.authorization.NiFiAuthorizationService">
|
||||
<property name="userService" ref="userService"/>
|
||||
|
|
|
@ -187,7 +187,7 @@ public class NiFiAuthorizationServiceTest {
|
|||
*
|
||||
* @throws Exception ex
|
||||
*/
|
||||
@Test(expected = UsernameNotFoundException.class)
|
||||
@Test(expected = UntrustedProxyException.class)
|
||||
public void testProxyNotFound() throws Exception {
|
||||
try {
|
||||
authorizationService.loadUserDetails(createRequestAuthentication(USER, PROXY_NOT_FOUND));
|
||||
|
|
|
@ -27,8 +27,6 @@ nf.Login = (function () {
|
|||
|
||||
var config = {
|
||||
urls: {
|
||||
registrationStatus: '../nifi-api/registration/status',
|
||||
registration: '../nifi-api/registration',
|
||||
identity: '../nifi-api/controller/identity',
|
||||
users: '../nifi-api/controller/users',
|
||||
token: '../nifi-api/access/token',
|
||||
|
|
|
@ -84,9 +84,9 @@ public abstract class AbstractLdapProvider implements LoginIdentityProvider {
|
|||
// attempt to get the ldap user details to get the DN
|
||||
if (authentication.getPrincipal() instanceof LdapUserDetails) {
|
||||
final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
|
||||
return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername());
|
||||
return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration);
|
||||
} else {
|
||||
return new AuthenticationResponse(authentication.getName(), credentials.getUsername());
|
||||
return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration);
|
||||
}
|
||||
} catch (final CommunicationException | AuthenticationServiceException e) {
|
||||
logger.error(e.getMessage());
|
||||
|
@ -99,11 +99,6 @@ public abstract class AbstractLdapProvider implements LoginIdentityProvider {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void preDestruction() throws ProviderDestructionException {
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue