NIFI-655:

- Added an endpoint for access details including configuration, creating tokens, and checking status.
- Updated DTOs and client side to utilize new endpoints.
This commit is contained in:
Matt Gilman 2015-11-16 21:18:04 -05:00
parent 9ccf61aff1
commit 7529694f23
17 changed files with 667 additions and 792 deletions

View File

@ -20,12 +20,13 @@ import com.wordnik.swagger.annotations.ApiModelProperty;
import javax.xml.bind.annotation.XmlType;
/**
* Details for the login configuration.
* Details for the access configuration.
*/
@XmlType(name = "loginConfig")
public class LoginConfigurationDTO {
@XmlType(name = "accessConfig")
public class AccessConfigurationDTO {
private Boolean supportsLogin;
private Boolean supportsAnonymous;
/**
* @return Indicates whether or not this NiFi supports user login.
@ -41,4 +42,20 @@ public class LoginConfigurationDTO {
public void setSupportsLogin(Boolean supportsLogin) {
this.supportsLogin = supportsLogin;
}
/**
* @return Indicates whether or not this NiFi supports anonymous access.
*/
@ApiModelProperty(
value = "Indicates whether or not this NiFi supports anonymous.",
readOnly = true
)
public Boolean getSupportsAnonymous() {
return supportsAnonymous;
}
public void setSupportsAnonymous(Boolean supportsAnonymous) {
this.supportsAnonymous = supportsAnonymous;
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.api.dto;
import com.wordnik.swagger.annotations.ApiModelProperty;
import javax.xml.bind.annotation.XmlRootElement;
/**
* A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds the users access status.
*/
@XmlRootElement(name = "accessStatus")
public class AccessStatusDTO {
public static enum Status {
UNKNOWN,
UNREGISTERED,
NOT_ACTIVE,
ACTIVE
}
private String identity;
private String username;
private String status;
private String message;
/**
* @return the user identity
*/
@ApiModelProperty(
value = "The user identity.",
readOnly = true
)
public String getIdentity() {
return identity;
}
public void setIdentity(String identity) {
this.identity = identity;
}
/**
* @return the username
*/
@ApiModelProperty(
value = "The username.",
readOnly = true
)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
/**
* @return the user access status
*/
@ApiModelProperty(
value = "The user access status.",
readOnly = true
)
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
/**
* @return additional details about the user access status
*/
@ApiModelProperty(
value = "Additional details about the user access status.",
readOnly = true
)
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@ -17,26 +17,26 @@
package org.apache.nifi.web.api.entity;
import javax.xml.bind.annotation.XmlRootElement;
import org.apache.nifi.web.api.dto.LoginConfigurationDTO;
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
/**
* A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a LoginConfigurationDTO.
* A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a AccessConfigurationDTO.
*/
@XmlRootElement(name = "loginConfigurationEntity")
public class LoginConfigurationEntity extends Entity {
@XmlRootElement(name = "accessConfigurationEntity")
public class AccessConfigurationEntity extends Entity {
private LoginConfigurationDTO config;
private AccessConfigurationDTO config;
/**
* The LoginConfigurationDTO that is being serialized.
* The AccessConfigurationDTO that is being serialized.
*
* @return The LoginConfigurationDTO object
* @return The AccessConfigurationDTO object
*/
public LoginConfigurationDTO getConfig() {
public AccessConfigurationDTO getConfig() {
return config;
}
public void setConfig(LoginConfigurationDTO config) {
public void setConfig(AccessConfigurationDTO config) {
this.config = config;
}

View File

@ -0,0 +1,43 @@
/*
* 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.api.entity;
import javax.xml.bind.annotation.XmlRootElement;
import org.apache.nifi.web.api.dto.AccessStatusDTO;
/**
* A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a AccessStatusDTO.
*/
@XmlRootElement(name = "accessStatusEntity")
public class AccessStatusEntity extends Entity {
private AccessStatusDTO accessStatus;
/**
* The AccessStatusDTO that is being serialized.
*
* @return The AccessStatusDTO object
*/
public AccessStatusDTO getAccessStatus() {
return accessStatus;
}
public void setAccessStatus(AccessStatusDTO accessStatus) {
this.accessStatus = accessStatus;
}
}

View File

@ -43,7 +43,6 @@ import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.apache.nifi.web.api.dto.ProcessorDTO;
import org.apache.nifi.web.api.dto.ComponentHistoryDTO;
import org.apache.nifi.web.api.dto.ControllerServiceReferencingComponentDTO;
import org.apache.nifi.web.api.dto.LoginConfigurationDTO;
import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
@ -178,13 +177,6 @@ public interface NiFiServiceFacade {
*/
ControllerConfigurationDTO getControllerConfiguration();
/**
* Gets the login configuration for this controller.
*
* @return The login configuration
*/
LoginConfigurationDTO getLoginConfiguration();
/**
* Updates the configuration for this controller.
*

View File

@ -16,15 +16,12 @@
*/
package org.apache.nifi.web;
import javax.servlet.Filter;
import org.apache.nifi.admin.service.UserService;
import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.NiFiAuthenticationProvider;
import org.apache.nifi.web.security.anonymous.NiFiAnonymousUserFilter;
import org.apache.nifi.web.security.NiFiAuthenticationEntryPoint;
import org.apache.nifi.web.security.RegistrationStatusFilter;
import org.apache.nifi.web.security.login.LoginAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtService;
import org.apache.nifi.web.security.node.NodeAuthorizedUserFilter;
@ -35,7 +32,6 @@ import org.apache.nifi.web.security.x509.X509CertificateValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@ -46,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.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
@ -72,7 +67,9 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
@Override
public void configure(WebSecurity webSecurity) throws Exception {
webSecurity.ignoring().antMatchers(HttpMethod.GET, "/controller/login/config");
webSecurity
.ignoring()
.antMatchers("/access/**");
}
@Override
@ -88,12 +85,6 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// login authentication for /token - exchanges for JWT for subsequent API usage
http.addFilterBefore(buildLoginFilter("/token"), UsernamePasswordAuthenticationFilter.class);
// registration status - will check the status of a user's account registration (regardless if its based on login or not)
http.addFilterBefore(buildRegistrationStatusFilter("/registration/status"), UsernamePasswordAuthenticationFilter.class);
// cluster authorized user
http.addFilterBefore(buildNodeAuthorizedUserFilter(), AnonymousAuthenticationFilter.class);
@ -121,28 +112,6 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
auth.authenticationProvider(new NiFiAuthenticationProvider(userDetailsService));
}
private LoginAuthenticationFilter buildLoginFilter(final String url) {
final LoginAuthenticationFilter loginFilter = new LoginAuthenticationFilter(url);
loginFilter.setJwtService(jwtService);
loginFilter.setLoginIdentityProvider(loginIdentityProvider);
loginFilter.setUserDetailsService(userDetailsService);
loginFilter.setCertificateExtractor(certificateExtractor);
loginFilter.setPrincipalExtractor(principalExtractor);
loginFilter.setCertificateValidator(certificateValidator);
return loginFilter;
}
private Filter buildRegistrationStatusFilter(final String url) {
final RegistrationStatusFilter registrationStatusFilter = new RegistrationStatusFilter(url);
registrationStatusFilter.setCertificateExtractor(certificateExtractor);
registrationStatusFilter.setPrincipalExtractor(principalExtractor);
registrationStatusFilter.setCertificateValidator(certificateValidator);
registrationStatusFilter.setProperties(properties);
registrationStatusFilter.setJwtService(jwtService);
registrationStatusFilter.setUserDetailsService(userDetailsService);
return registrationStatusFilter;
}
private NodeAuthorizedUserFilter buildNodeAuthorizedUserFilter() {
return new NodeAuthorizedUserFilter(properties);
}

View File

@ -153,7 +153,6 @@ import org.apache.nifi.web.util.SnippetUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.Validator;
import org.apache.nifi.controller.ReportingTaskNode;
@ -164,7 +163,6 @@ import org.apache.nifi.controller.service.ControllerServiceState;
import org.apache.nifi.reporting.ComponentType;
import org.apache.nifi.web.api.dto.ControllerServiceDTO;
import org.apache.nifi.web.api.dto.ControllerServiceReferencingComponentDTO;
import org.apache.nifi.web.api.dto.LoginConfigurationDTO;
import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
import org.apache.nifi.web.api.dto.ReportingTaskDTO;
import org.apache.nifi.web.api.dto.status.ClusterProcessGroupStatusDTO;
@ -207,7 +205,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
// administrative services
private AuditService auditService;
private UserService userService;
private LoginIdentityProvider loginIdentityProvider;
// cluster manager
private WebClusterManager clusterManager;
@ -2350,16 +2347,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
return controllerFacade.getInstanceId();
}
@Override
public LoginConfigurationDTO getLoginConfiguration() {
final LoginConfigurationDTO loginConfiguration = new LoginConfigurationDTO();
// specify whether login should be supported
loginConfiguration.setSupportsLogin(loginIdentityProvider != null);
return loginConfiguration;
}
@Override
public ControllerConfigurationDTO getControllerConfiguration() {
ControllerConfigurationDTO controllerConfig = new ControllerConfigurationDTO();
@ -3420,10 +3407,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
this.snippetUtils = snippetUtils;
}
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
this.loginIdentityProvider = loginIdentityProvider;
}
private boolean isPrimaryNode(String nodeId) {
final Node primaryNode = clusterManager.getPrimaryNode();
return (primaryNode != null && primaryNode.getNodeId().getId().equals(nodeId));

View File

@ -0,0 +1,412 @@
/*
* 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.api;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.nifi.util.NiFiProperties;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
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;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
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;
import org.apache.nifi.web.api.entity.AccessStatusEntity;
import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
import org.apache.nifi.web.api.request.ClientIdParameter;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.UntrustedProxyException;
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.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.
*/
@Path("/access")
@Api(
value = "/access",
description = "Endpoints for obtaining an access token or checking access status"
)
public class AccessResource extends ApplicationResource {
private NiFiProperties properties;
private X509CertificateValidator certificateValidator;
private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor;
private LoginIdentityProvider loginIdentityProvider;
private JwtService jwtService;
private AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService;
/**
* Retrieves the access configuration for this NiFi.
*
* @param httpServletRequest the servlet request
* @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
* @return A accessConfigurationEntity
*/
@GET
@Consumes(MediaType.WILDCARD)
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Path("/config")
@ApiOperation(
value = "Retrieves the access configuration for this NiFi",
response = AccessConfigurationEntity.class
)
public Response getLoginConfig(
@Context HttpServletRequest httpServletRequest,
@ApiParam(
value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
required = false
)
@QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId) {
final AccessConfigurationDTO accessConfiguration = new AccessConfigurationDTO();
// specify whether login should be supported and only support for secure requests
accessConfiguration.setSupportsLogin(loginIdentityProvider != null && httpServletRequest.isSecure());
accessConfiguration.setSupportsAnonymous(!properties.getAnonymousAuthorities().isEmpty() || !httpServletRequest.isSecure());
// create the revision
final RevisionDTO revision = new RevisionDTO();
revision.setClientId(clientId.getClientId());
// create the response entity
final AccessConfigurationEntity entity = new AccessConfigurationEntity();
entity.setRevision(revision);
entity.setConfig(accessConfiguration);
// generate the response
return clusterContext(generateOkResponse(entity)).build();
}
/**
* Gets the status the client's access.
*
* @param httpServletRequest the servlet request
* @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
* @return A accessStatusEntity
*/
@GET
@Consumes(MediaType.WILDCARD)
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Path("")
@ApiOperation(
value = "Gets the status the client's access",
response = AccessStatusEntity.class
)
@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 = 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.")
}
)
public Response getAccessStatus(
@Context HttpServletRequest httpServletRequest,
@ApiParam(
value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
required = false
)
@QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId) {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS.");
}
final AccessStatusDTO accessStatus = new AccessStatusDTO();
try {
// look for a certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate(httpServletRequest);
// if no certificate, just check the credentials
if (certificate == null) {
final String principal = jwtService.getAuthentication(httpServletRequest);
// ensure we have something we can work with (certificate or crendentials)
if (principal == 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));
// 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.setStatus("Account is active and authorized");
}
} else {
// we have a certificate so let's consider a proxy chain
final String principal = principalExtractor.extractPrincipal(certificate).toString();
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);
}
// set the user identity
accessStatus.setIdentity(principal);
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
// ensure the proxy chain is authorized
checkAuthorization(ProxiedEntitiesUtils.buildProxyChain(httpServletRequest, principal));
// no issues with authorization
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setStatus("Account is active and authorized");
}
} catch (final UsernameNotFoundException unfe) {
accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name());
accessStatus.setMessage(String.format("Unregistered user %s", accessStatus.getIdentity()));
} catch (final AccountStatusException ase) {
accessStatus.setStatus(AccessStatusDTO.Status.NOT_ACTIVE.name());
accessStatus.setMessage(ase.getMessage());
} catch (final UntrustedProxyException upe) {
throw new AccessDeniedException(upe.getMessage(), upe);
} catch (final AuthenticationServiceException ase) {
throw new AdministrationException(ase.getMessage(), ase);
}
// create the revision
final RevisionDTO revision = new RevisionDTO();
revision.setClientId(clientId.getClientId());
// create the entity
final AccessStatusEntity entity = new AccessStatusEntity();
entity.setRevision(revision);
entity.setAccessStatus(accessStatus);
return generateOkResponse(entity).build();
}
/**
* Checks the status of the proxy.
*
* @param proxyChain the proxy chain
* @throws AuthenticationException if the proxy chain is not authorized
*/
private void checkAuthorization(final List<String> proxyChain) throws AuthenticationException {
userDetailsService.loadUserDetails(new NiFiAuthenticationRequestToken(proxyChain));
}
/**
* Creates a token for accessing the REST API via username/password.
*
* @param httpServletRequest the servlet request
* @param username the username
* @param password the password
* @return A JWT (string)
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
@Path("/token")
@ApiOperation(
value = "Creates a token for accessing the REST API via username/password",
response = String.class
)
@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 = 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.")
}
)
public Response createAccessToken(
@Context HttpServletRequest httpServletRequest,
@FormParam("username") String username,
@FormParam("password") String password) {
// only support access tokens when communicating over HTTPS
if (!httpServletRequest.isSecure()) {
throw new IllegalStateException("Access tokens are only issued over HTTPS.");
}
// if not configuration for login, don't consider credentials
if (loginIdentityProvider == null) {
throw new IllegalStateException("Username/Password login not supported by this NiFi.");
}
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);
// 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());
}
} 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());
} 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);
}
}
// generate JWT for response
final String token = jwtService.generateSignedToken(loginAuthenticationToken);
// build the response
final URI uri = URI.create(generateResourceUri("access", "token"));
return generateCreatedResponse(uri, token).build();
}
/**
* Ensures the proxyChain is authorized before allowing the user to be authenticated.
*
* @param proxyChain the proxy chain
* @throws AuthenticationException if the proxy chain is not authorized
*/
private void authorizeProxyIfNecessary(final List<String> proxyChain) throws AuthenticationException {
if (proxyChain.size() > 1) {
try {
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
} 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) {
};
}
}
}
// setters
public void setProperties(NiFiProperties properties) {
this.properties = properties;
}
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
this.loginIdentityProvider = loginIdentityProvider;
}
public void setJwtService(JwtService jwtService) {
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 setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService) {
this.userDetailsService = userDetailsService;
}
}

View File

@ -78,10 +78,8 @@ import org.apache.nifi.web.api.request.ClientIdParameter;
import org.apache.nifi.web.api.request.IntegerParameter;
import org.apache.nifi.web.api.request.LongParameter;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.web.api.dto.LoginConfigurationDTO;
import org.apache.nifi.web.api.entity.ControllerServiceTypesEntity;
import org.apache.nifi.web.api.entity.IdentityEntity;
import org.apache.nifi.web.api.entity.LoginConfigurationEntity;
import org.apache.nifi.web.api.entity.ReportingTaskTypesEntity;
import org.springframework.security.access.prepost.PreAuthorize;
@ -656,52 +654,6 @@ public class ControllerResource extends ApplicationResource {
return clusterContext(generateOkResponse(entity)).build();
}
/**
* Retrieves the login configuration for this NiFi.
*
* @param httpServletRequest the servlet request
* @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
* @return A loginConfigurationEntity
*/
@GET
@Consumes(MediaType.WILDCARD)
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Path("/login/config")
@ApiOperation(
value = "Retrieves the login configuration for this NiFi",
response = LoginConfigurationEntity.class
)
public Response getLoginConfig(
@Context HttpServletRequest httpServletRequest,
@ApiParam(
value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
required = false
)
@QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId) {
// replicate if cluster manager
if (properties.isClusterManager()) {
return clusterManager.applyRequest(HttpMethod.GET, getAbsolutePath(), getRequestParameters(true), getHeaders()).getResponse();
}
final LoginConfigurationDTO loginConfig = serviceFacade.getLoginConfiguration();
// only support login/registration when running securely
loginConfig.setSupportsLogin(loginConfig.getSupportsLogin() && httpServletRequest.isSecure());
// create the revision
final RevisionDTO revision = new RevisionDTO();
revision.setClientId(clientId.getClientId());
// create the response entity
final LoginConfigurationEntity entity = new LoginConfigurationEntity();
entity.setRevision(revision);
entity.setConfig(loginConfig);
// generate the response
return clusterContext(generateOkResponse(entity)).build();
}
/**
* Retrieves the configuration for this NiFi.
*

View File

@ -122,7 +122,6 @@
<property name="optimisticLockingManager" ref="webOptimisticLockingManager"/>
<property name="dtoFactory" ref="dtoFactory"/>
<property name="clusterManager" ref="clusterManager"/>
<property name="loginIdentityProvider" ref="loginIdentityProvider"/>
</bean>
<!-- depecrated -->
@ -242,6 +241,15 @@
<bean id="systemDiagnosticsResource" class="org.apache.nifi.web.api.SystemDiagnosticsResource" scope="singleton">
<property name="serviceFacade" ref="serviceFacade"/>
</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="loginIdentityProvider" ref="loginIdentityProvider"/>
<property name="jwtService" ref="jwtService"/>
<property name="userDetailsService" ref="userDetailsService"/>
</bean>
<!-- configuration for jaxb serialization -->
<bean class="org.apache.nifi.web.util.ObjectMapperResolver" scope="singleton"/>

View File

@ -1,251 +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;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.nifi.authentication.LoginCredentials;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.util.StringUtils;
import org.apache.nifi.web.security.jwt.JwtService;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
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.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
* Exchanges a successful login with the configured provider for a ID token for accessing the API.
*/
public class RegistrationStatusFilter extends AbstractAuthenticationProcessingFilter {
private static final Logger logger = LoggerFactory.getLogger(RegistrationStatusFilter.class);
private NiFiProperties properties;
private JwtService jwtService;
private AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService;
private X509CertificateValidator certificateValidator;
private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor;
public RegistrationStatusFilter(final String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
// do not continue filter chain... simply exchaning authentication for token
setContinueChainBeforeSuccessfulAuthentication(false);
}
@Override
public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// only suppport login when running securely
if (!request.isSecure()) {
return null;
}
// look for a certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate(request);
// if no certificate, just check the credentials
if (certificate == null) {
final String principal = jwtService.getAuthentication(request);
// ensure we have something we can work with (certificate or crendentials)
if (principal == null) {
throw new BadCredentialsException("Unable to check registration status as no credentials were included with the request.");
}
// 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
final LoginCredentials tokenCredentials = new LoginCredentials(principal, null);
return new RegistrationStatusAuthenticationToken(tokenCredentials);
} else {
// we have a certificate so let's consider a proxy chain
final String principal = principalExtractor.extractPrincipal(certificate).toString();
try {
// validate the certificate
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;
}
// ensure the proxy chain is authorized
checkAuthorization(ProxiedEntitiesUtils.buildProxyChain(request, principal));
// no issues with authorization
final LoginCredentials preAuthenticatedCredentials = new LoginCredentials(principal, null);
return new RegistrationStatusAuthenticationToken(preAuthenticatedCredentials);
}
}
/**
* Checks the status of the proxy.
*
* @param proxyChain the proxy chain
* @throws AuthenticationException if the proxy chain is not authorized
*/
private void checkAuthorization(final List<String> proxyChain) throws AuthenticationException {
userDetailsService.loadUserDetails(new NiFiAuthenticationRequestToken(proxyChain));
}
@Override
protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authentication)
throws IOException, ServletException {
// mark as successful
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/plain");
response.setContentLength(0);
}
@Override
protected void unsuccessfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException ae) throws IOException, ServletException {
// 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);
}
}
/**
* This is an Authentication Token for logging in. Once a user is authenticated, they can be issues an ID token.
*/
public static class RegistrationStatusAuthenticationToken extends AbstractAuthenticationToken {
final LoginCredentials credentials;
public RegistrationStatusAuthenticationToken(final LoginCredentials credentials) {
super(null);
setAuthenticated(true);
this.credentials = credentials;
}
public LoginCredentials getLoginCredentials() {
return credentials;
}
@Override
public Object getCredentials() {
return credentials.getPassword();
}
@Override
public Object getPrincipal() {
return credentials.getUsername();
}
}
public void setJwtService(JwtService jwtService) {
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 setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService) {
this.userDetailsService = userDetailsService;
}
public void setProperties(NiFiProperties properties) {
this.properties = properties;
}
}

View File

@ -26,12 +26,9 @@ import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.UnsupportedJwtException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.admin.service.KeyService;
@ -83,35 +80,25 @@ public class JwtService {
}
/**
* Adds a token for the specified authentication in the specified response.
* Generates a signed JWT token from the provided (Spring Security) login authentication token.
*
* @param response The response to add the token to
* @param authentication The authentication to generate a token for
* @throws java.io.IOException if an io exception occurs
* @param authenticationToken
* @return a signed JWT containing the user identity and the identity provider, Base64-encoded
*/
public void addToken(final HttpServletResponse response, final LoginAuthenticationToken authentication) throws IOException {
public String generateSignedToken(final LoginAuthenticationToken authenticationToken) {
// set expiration to one day from now
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(calendar.getTimeInMillis() + authentication.getExpiration());
calendar.setTimeInMillis(calendar.getTimeInMillis() + authenticationToken.getExpiration());
// create a token the specified authentication
final String identity = authentication.getPrincipal().toString();
final String username = authentication.getName();
final String identity = authenticationToken.getPrincipal().toString();
final String username = authenticationToken.getName();
// get/create the key for this user
final String key = keyService.getOrCreateKey(identity);
final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
// build the token
final String token = Jwts.builder().setSubject(identity).claim("preferred_username", username).setExpiration(calendar.getTime()).signWith(SignatureAlgorithm.HS512, keyBytes).compact();
// add the token as a response header
final PrintWriter out = response.getWriter();
out.print(token);
// mark the response as successful
response.setStatus(HttpServletResponse.SC_CREATED);
response.setContentType("text/plain");
return Jwts.builder().setSubject(identity).claim("preferred_username", username).setExpiration(calendar.getTime()).signWith(SignatureAlgorithm.HS512, keyBytes).compact();
}
}

View File

@ -1,243 +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.login;
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.authentication.exception.IdentityAccessException;
import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
import org.apache.nifi.util.StringUtils;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.jwt.JwtService;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
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.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
* Exchanges a successful login with the configured provider for a ID token for accessing the API.
*/
public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final Logger logger = LoggerFactory.getLogger(LoginAuthenticationFilter.class);
private AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService;
private X509CertificateValidator certificateValidator;
private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor;
private LoginIdentityProvider loginIdentityProvider;
private JwtService jwtService;
public LoginAuthenticationFilter(final String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
// do not continue filter chain... simply exchanging authentication for token
setContinueChainBeforeSuccessfulAuthentication(false);
}
@Override
public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// only suppport login when running securely
if (!request.isSecure()) {
return null;
}
// look for the credentials in the request
final LoginCredentials credentials = getLoginCredentials(request);
// if the credentials were not part of the request, attempt to log in with the certificate in the request
if (credentials == null) {
// look for a certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate(request);
// 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 BadCredentialsException("Login not supported.");
}
final String principal = jwtService.getAuthentication(request);
if (principal == null) {
throw new AuthenticationCredentialsNotFoundException("Unable to issue token as issue token as no credentials were found in the request.");
}
return new LoginAuthenticationToken(principal, loginIdentityProvider.getExpiration());
} else {
// extract the principal
final String principal = principalExtractor.extractPrincipal(certificate).toString();
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;
}
// authorize the proxy if necessary
authorizeProxyIfNecessary(ProxiedEntitiesUtils.buildProxyChain(request, principal));
return new LoginAuthenticationToken(principal, loginIdentityProvider.getExpiration());
}
} else {
// if not configuration for login, don't consider credentials
if (loginIdentityProvider == null) {
throw new BadCredentialsException("Login not supported.");
}
try {
// attempt to authenticate
final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(credentials);
// create the authentication token
return new LoginAuthenticationToken(authenticationResponse.getUsername(), loginIdentityProvider.getExpiration());
} catch (final InvalidLoginCredentialsException ilce) {
throw new BadCredentialsException("The supplied username and password are not valid.", ilce);
} catch (final IdentityAccessException iae) {
throw new AuthenticationServiceException(iae.getMessage(), iae);
}
}
}
/**
* Ensures the proxyChain is authorized before allowing the user to be authenticated.
*
* @param proxyChain the proxy chain
* @throws AuthenticationException if the proxy chain is not authorized
*/
private void authorizeProxyIfNecessary(final List<String> proxyChain) throws AuthenticationException {
if (proxyChain.size() > 1) {
try {
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
} catch (final Exception e) {
// any other issue we're going to treat as an authentication exception which will return 401
throw new AuthenticationException(e.getMessage(), e) {
};
}
}
}
private LoginCredentials getLoginCredentials(HttpServletRequest request) {
final String username = request.getParameter("username");
final String password = request.getParameter("password");
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
return null;
} else {
return new LoginCredentials(username, password);
}
}
@Override
protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authentication)
throws IOException, ServletException {
try {
// generate JWT for response
jwtService.addToken(response, (LoginAuthenticationToken) authentication);
} catch (final AdministrationException ae) {
unsuccessfulAuthentication(request, response, new AuthenticationServiceException(ae.getMessage(), ae));
}
}
@Override
protected void unsuccessfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException failed) throws IOException, ServletException {
if (failed instanceof BadCredentialsException || failed instanceof AuthenticationCredentialsNotFoundException) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
} else if (failed instanceof AuthenticationServiceException) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
response.setContentType("text/plain");
final PrintWriter out = response.getWriter();
out.println(failed.getMessage());
}
public void setJwtService(JwtService jwtService) {
this.jwtService = jwtService;
}
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
this.loginIdentityProvider = loginIdentityProvider;
}
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 setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService) {
this.userDetailsService = userDetailsService;
}
}

View File

@ -29,7 +29,7 @@
</bean>
<!-- x509 validator -->
<bean id="x509Validator" class="org.apache.nifi.web.security.x509.X509CertificateValidator">
<bean id="certificateValidator" class="org.apache.nifi.web.security.x509.X509CertificateValidator">
<property name="ocspValidator" ref="ocspValidator"/>
</bean>

View File

@ -65,7 +65,7 @@ nf.Canvas = (function () {
banners: '../nifi-api/controller/banners',
controller: '../nifi-api/controller',
controllerConfig: '../nifi-api/controller/config',
loginConfig: '../nifi-api/controller/login/config',
accessConfig: '../nifi-api/access/config',
cluster: '../nifi-api/cluster',
d3Script: 'js/d3/d3.min.js'
}
@ -1090,7 +1090,7 @@ nf.Canvas = (function () {
// get the login config
var loginXhr = $.ajax({
type: 'GET',
url: config.urls.loginConfig,
url: config.urls.accessConfig,
dataType: 'json'
});

View File

@ -31,8 +31,9 @@ nf.Login = (function () {
registration: '../nifi-api/registration',
identity: '../nifi-api/controller/identity',
users: '../nifi-api/controller/users',
token: '../nifi-api/token',
loginConfig: '../nifi-api/controller/login/config'
token: '../nifi-api/access/token',
accessStatus: '../nifi-api/access',
accessConfig: '../nifi-api/access/config'
}
};
@ -210,167 +211,71 @@ nf.Login = (function () {
init: function () {
nf.Storage.init();
var showMessage = false;
var needsLogin = false;
var needsNiFiRegistration = false;
if (nf.Storage.getItem('jwt') !== null) {
showLogoutLink();
}
var token = $.ajax({
// access status
var accessStatus = $.ajax({
type: 'GET',
url: config.urls.token
url: config.urls.accessStatus,
dataType: 'json'
}).fail(function (xhr, status, error) {
$('#login-message-title').text('Unable to check Access Status');
$('#login-message').text(xhr.responseText);
initializeMessage();
});
var identity = $.ajax({
// access config
var accessConfigXhr = $.ajax({
type: 'GET',
url: config.urls.identity,
url: config.urls.accessConfig,
dataType: 'json'
});
var pageStateInit = $.Deferred(function (deferred) {
// get the current user's identity
identity.done(function (response) {
// if the user is anonymous see if they need to login or if they are working with a certificate
if (response.identity === 'anonymous') {
supportsAnonymous = true;
$.when(accessStatus, accessConfigXhr).done(function (accessStatusResult, accessConfigResult) {
var accessStatusResponse = accessStatusResult[0];
var accessStatus = accessStatusResponse.accessStatus;
// request a token without including credentials, if successful then the user is using a certificate
token.done(function (jwt) {
var accessConfigResponse = accessConfigResult[0];
var accessConfig = accessConfigResponse.config;
// the user is using a certificate/token, see if their account is active/pending/revoked/etc
$.ajax({
type: 'GET',
url: config.urls.registrationStatus
}).done(function () {
// record whether this NiFi supports anonymous access
supportsAnonymous = accessConfig.supportsAnonymous;
// possible login states
var needsLogin = false;
var needsNiFiRegistration = false;
var showMessage = false;
// handle the status appropriately
if (accessStatus.status === 'UNKNOWN') {
needsLogin = true;
} else if (accessStatus.status === 'UNREGISTERED') {
needsNiFiRegistration = true;
$('#nifi-user-submit-justification').text(accessStatus.username);
} else if (accessStatus.status === 'NOT_ACTIVE') {
showMessage = true;
$('#login-message-title').text('Access Denied');
$('#login-message').text(accessStatus.message);
} else if (accessStatus.status === 'ACTIVE') {
showMessage = true;
// account is active and good
$('#login-message-title').text('Success');
$('#login-message').text('Your account is active and you are already logged in.');
}).fail(function (xhr, status, error) {
if (xhr.status === 401) {
var user = nf.Common.getJwtSubject(jwt);
// show the user
$('#nifi-user-submit-justification').text(user);
// anonymous user and 401 means they need nifi registration
needsNiFiRegistration = true;
} else {
showMessage = true;
// anonymous user and non-401 means they already have an account and it's pending/revoked
$('#login-message-title').text('Access Denied');
if ($.trim(xhr.responseText) === '') {
$('#login-message').text('Unable to check registration status.');
} else {
$('#login-message').text(xhr.responseText);
}
}
}).always(function () {
deferred.resolve();
});
}).fail(function (tokenXhr) {
if (tokenXhr.status === 400) {
// no credentials supplied so 400 must be due to an invalid/expired token
logout();
}
// no token granted, user has no certificate and needs to login with their credentials
needsLogin = true;
deferred.resolve();
});
} else {
showMessage = true;
// the user is not anonymous and has an active account (though maybe role-less)
$('#login-message-title').text('Success');
$('#login-message').text('Your account is active and you are already logged in.');
deferred.resolve();
}
}).fail(function (xhr, status, error) {
// unable to get identity (and no anonymous user) see if we can offer login
if (xhr.status === 401) {
// attempt to get a token for the current user without passing login credentials
token.done(function (jwt) {
var user = nf.Common.getJwtSubject(jwt);
// show the user
$('#nifi-user-submit-justification').text(user);
// 401 from identity request and 200 from token means they have a certificate/token but have not yet requested an account
needsNiFiRegistration = true;
}).fail(function (tokenXhr) {
if (tokenXhr.status === 400) {
// no credentials supplied so 400 must be due to an invalid/expired token
logout();
}
// no token granted, user needs to login with their credentials
needsLogin = true;
}).always(function () {
deferred.resolve();
});
} else if (xhr.status === 403) {
// attempt to get a token for the current user without passing login credentials
token.done(function () {
showMessage = true;
// the user is logged in with certificate or credentials but their account is pending/revoked. error message should indicate
$('#login-message-title').text('Access Denied');
if ($.trim(xhr.responseText) === '') {
$('#login-message').text('Unable to authorize you to use this NiFi and anonymous access is disabled.');
} else {
$('#login-message').text(xhr.responseText);
}
}).fail(function (tokenXhr) {
if (tokenXhr.status === 400) {
// no credentials supplied so 400 must be due to an invalid/expired token
logout();
}
// no token granted, user needs to login with their credentials
needsLogin = true;
}).always(function () {
deferred.resolve();
});
} else {
showMessage = true;
// the user is logged in with certificate or credentials but their account is pending/revoked. error message should indicate
$('#login-message-title').text('Access Denied');
if ($.trim(xhr.responseText) === '') {
$('#login-message').text('Unable to authorize you to use this NiFi and anonymous access is disabled.');
} else {
$('#login-message').text(xhr.responseText);
}
deferred.resolve();
}
});
}).promise();
var loginConfigXhr = $.ajax({
type: 'GET',
url: config.urls.loginConfig,
dataType: 'json'
});
// render the page accordingly
$.when(loginConfigXhr, pageStateInit).done(function (loginResult) {
var loginResponse = loginResult[0];
var loginConfig = loginResponse.config;
// if login is required, verify its supported
if (loginConfig.supportsLogin === false && needsLogin === true) {
if (accessConfig.supportsLogin === false && needsLogin === true) {
$('#login-message-title').text('Access Denied');
$('#login-message').text('This NiFi is not configured to support login.');
showMessage = true;
needsLogin = false;
}
// initialize the page as appropriate
if (showMessage === true) {
initializeMessage();
} else if (needsLogin === true) {

View File

@ -102,7 +102,7 @@ nf.Common = (function () {
}
},
urls: {
token: '../nifi-api/token'
token: '../nifi-api/access/token'
}
},
@ -163,7 +163,7 @@ nf.Common = (function () {
if (timeRemaining < interval) {
// if the token will expire before the next interval minus some bonus time, refresh now
$.ajax({
type: 'GET',
type: 'POST',
url: nf.Common.config.urls.token
}).done(function (jwt) {
nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt));