NIFI-655:

- Adding a new endpoint to obtain the status of a user registration.
- Updated the login page loading to ensure all possible states work.
This commit is contained in:
Matt Gilman 2015-11-03 11:10:32 -05:00
parent 7f9807f461
commit 71d84117e4
12 changed files with 382 additions and 35 deletions

View File

@ -23,6 +23,7 @@ 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.form.LoginAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtService;
@ -90,11 +91,14 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
// login authentication for /token - exchanges for JWT for subsequent API usage
http.addFilterBefore(buildLoginFilter("/token"), UsernamePasswordAuthenticationFilter.class);
// verify the configured login authenticator supports registration
// verify the configured login authenticator supports user login registration
if (loginIdentityProvider.supportsRegistration()) {
http.addFilterBefore(buildRegistrationFilter("/registration"), 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);
@ -134,6 +138,15 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
private Filter buildRegistrationFilter(final String url) {
return null;
}
private Filter buildRegistrationStatusFilter(final String url) {
final RegistrationStatusFilter registrationFilter = new RegistrationStatusFilter(url);
registrationFilter.setCertificateExtractor(certificateExtractor);
registrationFilter.setPrincipalExtractor(principalExtractor);
registrationFilter.setProperties(properties);
registrationFilter.setUserDetailsService(userDetailsService);
return registrationFilter;
}
private NodeAuthorizedUserFilter buildNodeAuthorizedUserFilter() {
return new NodeAuthorizedUserFilter(properties);

View File

@ -929,7 +929,7 @@ public class ControllerResource extends ApplicationResource {
IdentityEntity entity = new IdentityEntity();
entity.setRevision(revision);
entity.setUserId(user.getId());
entity.setIdentity(user.getDn());
entity.setIdentity(user.getUserName());
// generate the response
return clusterContext(generateOkResponse(entity)).build();
@ -945,14 +945,17 @@ public class ControllerResource extends ApplicationResource {
@Consumes(MediaType.WILDCARD)
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Path("/authorities")
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN', 'ROLE_PROXY', 'ROLE_NIFI', 'ROLE_PROVENANCE')")
@ApiOperation(
value = "Retrieves the user details, including the authorities, about the user making the request",
response = AuthorityEntity.class,
authorizations = {
@Authorization(value = "Read Only", type = "ROLE_MONITOR"),
@Authorization(value = "Data Flow Manager", type = "ROLE_DFM"),
@Authorization(value = "Administrator", type = "ROLE_ADMIN")
@Authorization(value = "Administrator", type = "ROLE_ADMIN"),
@Authorization(value = "Proxy", type = "ROLE_PROXY"),
@Authorization(value = "NiFi", type = "ROLE_NIFI"),
@Authorization(value = "Provenance", type = "ROLE_PROVENANCE")
}
)
@ApiResponses(

View File

@ -0,0 +1,217 @@
/*
* 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.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.authentication.LoginCredentials;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.util.StringUtils;
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
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 AuthenticationUserDetailsService<NiFiAuthenticationRequestToken> userDetailsService;
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 LoginCredentials credentials = getLoginCredentials(request);
if (credentials == null) {
throw new BadCredentialsException("Unable to check registration status as no credentials were included with the request.");
}
checkAuthorization(ProxiedEntitiesUtils.buildProxyChain(request, credentials.getUsername()));
return new RegistrationStatusAuthenticationToken(credentials);
} else {
// we have a certificate so let's use that
final String principal = extractPrincipal(certificate);
checkAuthorization(ProxiedEntitiesUtils.buildProxyChain(request, principal));
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));
}
private String extractPrincipal(final X509Certificate certificate) {
// extract the principal
final Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
return ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
}
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 {
// 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 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

@ -35,6 +35,7 @@
${nf.login.script.tags}
</head>
<body>
<jsp:include page="/WEB-INF/partials/login/login-message.jsp"/>
<jsp:include page="/WEB-INF/partials/login/login-form.jsp"/>
<jsp:include page="/WEB-INF/partials/login/user-registration-form.jsp"/>
<jsp:include page="/WEB-INF/partials/login/nifi-registration-form.jsp"/>

View File

@ -48,6 +48,9 @@
<span id="current-user" class="hidden"></span>
<span id="login-link" class="link">login</span>
</li>
<li id="logout-link-container" style="display: none;">
<span id="logout-link" class="link">logout</span>
</li>
<li>
<span id="help-link" class="link">help</span>
</li>

View File

@ -0,0 +1,20 @@
<%--
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.
--%>
<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<div id="login-message-container" class="hidden">
<div id="login-message"></div>
</div>

View File

@ -15,6 +15,6 @@
limitations under the License.
--%>
<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<div id="login-submission-container">
<div id="login-submission-container" class="hidden">
<button id="login-submission-button" type="submit" class="btn">Log in</button>
</div>

View File

@ -506,6 +506,10 @@ div.search-glass-pane {
/* styles for the status link */
#current-user {
margin-right: 8px;
}
#utilities-container {
float: right;
}

View File

@ -147,6 +147,11 @@ nf.CanvasHeader = (function () {
} else {
$('#login-link-container').css('display', 'none');
}
// logout link
$('#logout-link').click(function () {
nf.Storage.removeItem("jwt");
});
// initialize the new template dialog
$('#new-template-dialog').modal({

View File

@ -1054,6 +1054,7 @@ nf.Canvas = (function () {
nf.Common.setAuthorities(authoritiesResponse.authorities);
// at this point the user may be themselves or anonymous
$('#current-user').text(identityResponse.identity).show();
// if the user is logged, we want to determine if they were logged in using a certificate
if (identityResponse.identity !== 'anonymous') {
@ -1064,7 +1065,7 @@ nf.Canvas = (function () {
}).fail(function () {
// if this request succeeds, it means the user is logged in using their certificate.
// if this request fails, it means the user is logged in with login credentials so we want to render a logout button.
// TODO - render logout button
$('#logout-link-container').show();
}).always(function () {
deferred.resolve();
});

View File

@ -25,6 +25,8 @@ 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/token',
@ -32,6 +34,10 @@ nf.Login = (function () {
}
};
var initializeMessage = function () {
$('#login-message-container').show();
};
var initializeLogin = function () {
return $.ajax({
type: 'GET',
@ -45,8 +51,29 @@ nf.Login = (function () {
// handle login click
$('#login-button').on('click', function () {
login().done(function (response) {
login().done(function (response, status, xhr) {
var authorization = xhr.getResponseHeader('Authorization');
var badToken = false;
// ensure there was a token in the response
if (authorization) {
var tokens = authorization.split(/ /);
// ensure the token is the appropriate length
if (tokens.length === 2) {
// store the jwt and reload the page
nf.Storage.setItem('jwt', tokens[1]);
window.location = '/nifi';
} else {
badToken = true;
}
} else {
badToken = true;
}
if (badToken === true) {
// TODO - show unable to parse response token
}
});
});
@ -112,17 +139,19 @@ nf.Login = (function () {
'justification': justification
}
}).done(function (response) {
// TODO
// // hide the registration pane
// $('#registration-pane').hide();
//
// // show the message pane
// $('#message-pane').show();
// $('#message-title').text('Thanks');
// $('#message-content').text('Your request will be processed shortly.');
}).fail(nf.Common.handleAjaxError);
$('#login-message').text('Thanks! Your request will be processed shortly.');
}).fail(function (xhr, status, error) {
$('#login-message').text(xhr.responseText);
}).always(function () {
// update form visibility
$('#nifi-registration-container').hide();
$('#login-submission-container').hide();
$('#login-message-container').show();
});
}
});
$('#login-submission-container').show();
};
return {
@ -132,6 +161,7 @@ nf.Login = (function () {
init: function () {
nf.Storage.init();
var showMessage = false;
var needsLogin = false;
var needsNiFiRegistration = false;
@ -140,47 +170,87 @@ nf.Login = (function () {
url: config.urls.token
});
var identity = $.ajax({
type: 'GET',
url: config.urls.identity,
dataType: 'json'
});
var pageStateInit = $.Deferred(function(deferred) {
// get the current user's identity
$.ajax({
type: 'GET',
url: config.urls.identity,
dataType: 'json'
}).done(function (response) {
var identity = response.identity;
// if the user is anonymous they need to login
if (identity === 'anonymous') {
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') {
// request a token without including credentials, if successful then the user is using a certificate
token.done(function () {
// anonymous user and 200 from token means they have a certificate but have not yet requested an account
needsNiFiRegistration = true;
}).fail(function (xhr, status, error) {
// no token granted, user needs to login with their credentials
// the user is using a certificate, see if their account is active/pending/revoked/etc
$.ajax({
type: 'GET',
url: config.urls.registrationStatus
}).done(function () {
showMessage = true;
// account is active and good
$('#login-message').text('Your account is active and you are already logged in.');
deferred.resolve();
}).fail(function (xhr, status, error) {
if (xhr.status === 401) {
// 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
if ($.trim(xhr.responseText) === '') {
$('#login-message').text('Unable to check registration status.');
} else {
$('#login-message').text(xhr.responseText);
}
}
deferred.resolve();
});
}).fail(function () {
// 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').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 () {
// 401 from identity request and 200 from token means they have a certificate but have not yet requested an account
needsNiFiRegistration = true;
}).fail(function (xhr, status, error) {
}).fail(function () {
// no token granted, user needs to login with their credentials
needsLogin = true;
});
} else if (xhr.status === 403) {
// the user is logged in with certificate or credentials but their account is still pending. error message should indicate
// TODO - show error
} else {
showMessage = true;
// the user is logged in with certificate or credentials but their account is pending/revoked. error message should indicate
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);
}
}
}).always(function () {
deferred.resolve();
});
}).promise();
// render the page accordingly
$.when(pageStateInit).done(function () {
if (needsLogin === true) {
if (showMessage === true) {
initializeMessage();
} else if (needsLogin === true) {
initializeLogin();
} else if (needsNiFiRegistration === true) {
initializeNiFiRegistration();

View File

@ -50,6 +50,16 @@ $(document).ready(function () {
// hide the loading indicator
$('div.loading-container').removeClass('ajax-loading');
});
// include jwt when possible
$.ajaxSetup({
'beforeSend': function(xhr) {
var token = nf.Storage.getItem('jwt');
if (token) {
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
}
}
});
// initialize the tooltips
$('img.setting-icon').qtip(nf.Common.config.tooltipConfig);