NIFI-655:

- Ensuring we know the necessary state before we attempt to render the login page.
- Building the proxy chain in the JWT authentication filter.
- Only rendering the login when appropriate.
This commit is contained in:
Matt Gilman 2015-11-03 12:45:37 -05:00
parent 71d84117e4
commit ed27ed0449
7 changed files with 171 additions and 108 deletions

View File

@ -156,6 +156,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
final JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter();
jwtFilter.setProperties(properties);
jwtFilter.setJwtService(jwtService);
jwtFilter.setCertificateExtractor(certificateExtractor);
jwtFilter.setPrincipalExtractor(principalExtractor);
jwtFilter.setAuthenticationManager(authenticationManager());
return jwtFilter;
}

View File

@ -19,6 +19,8 @@ package org.apache.nifi.web.security;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@ -75,17 +77,27 @@ public class RegistrationStatusFilter extends AbstractAuthenticationProcessingFi
if (certificate == null) {
final LoginCredentials credentials = getLoginCredentials(request);
// ensure we have something we can work with (certificate or crendentials)
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()));
// without a certificate, this is not a proxied request
final List<String> chain = Arrays.asList(credentials.getUsername());
// check authorization for this user
checkAuthorization(chain);
// no issues with authorization
return new RegistrationStatusAuthenticationToken(credentials);
} else {
// we have a certificate so let's use that
// TODO - certificate validation
// we have a certificate so let's consider a proxy chain
final String principal = extractPrincipal(certificate);
checkAuthorization(ProxiedEntitiesUtils.buildProxyChain(request, principal));
// no issues with authorization
final LoginCredentials preAuthenticatedCredentials = new LoginCredentials(principal, null);
return new RegistrationStatusAuthenticationToken(preAuthenticatedCredentials);
}

View File

@ -83,6 +83,8 @@ public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingF
if (certificate == null) {
throw new PreAuthenticatedCredentialsNotFoundException("Unable to extract client certificate after processing request with no login credentials specified.");
}
// TODO - certificate validation
// authorize the proxy if necessary
final String principal = extractPrincipal(certificate);
@ -96,6 +98,8 @@ public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingF
// if there was a certificate with this request see if it was proxying an end user request
if (certificate != null) {
// TODO - certificate validation
// authorize the proxy if necessary
final String principal = extractPrincipal(certificate);
authorizeProxyIfNecessary(ProxiedEntitiesUtils.buildProxyChain(request, principal));

View File

@ -16,6 +16,8 @@
*/
package org.apache.nifi.web.security.jwt;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -24,9 +26,12 @@ import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken;
import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
import org.apache.nifi.web.security.user.NewAccountRequest;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
*/
@ -34,6 +39,8 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private X509CertificateExtractor certificateExtractor;
private X509PrincipalExtractor principalExtractor;
private JwtService jwtService;
@Override
@ -43,16 +50,39 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
return null;
}
final String principal = jwtService.getAuthentication(request);
if (principal == null) {
// get the principal out of the user token
final String jwtPrincipal = jwtService.getAuthentication(request);
if (jwtPrincipal == null) {
return null;
}
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxyChain(request, principal);
if (isNewAccountRequest(request)) {
return new NewAccountAuthenticationRequestToken(new NewAccountRequest(proxyChain, getJustification(request)));
// look for a certificate
final X509Certificate certificate = certificateExtractor.extractClientCertificate(request);
final List<String> chain;
if (certificate == null) {
// without a certificate, this is not a proxied request
chain = Arrays.asList(jwtPrincipal);
} else {
return new NiFiAuthenticationRequestToken(proxyChain);
// TODO - certificate validation
// extract the principal
Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
final String principal = ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
// get the proxy chain and verify the principal is found
chain = ProxiedEntitiesUtils.buildProxyChain(request, principal);
// ensure the chain contains the jwt principal
if (!chain.contains(jwtPrincipal)) {
throw new BadCredentialsException("Principal in user token not found in the proxy chain.");
}
}
if (isNewAccountRequest(request)) {
return new NewAccountAuthenticationRequestToken(new NewAccountRequest(chain, getJustification(request)));
} else {
return new NiFiAuthenticationRequestToken(chain);
}
}
@ -60,4 +90,12 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
this.jwtService = jwtService;
}
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor;
}
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
this.principalExtractor = principalExtractor;
}
}

View File

@ -139,7 +139,9 @@ nf.CanvasHeader = (function () {
nf.Shell.showPage(config.urls.helpDocument);
});
if (supportsLogin === true) {
// show the login link if supported and user is currently anonymous
var isAnonymous = $('#current-user').text() === 'anonymous';
if (supportsLogin === true && isAnonymous) {
// login link
$('#login-link').click(function () {
nf.Shell.showPage('login', false);

View File

@ -1076,121 +1076,123 @@ nf.Canvas = (function () {
// there is no anonymous access and we don't know this user - open the login page which handles login/registration/etc
if (xhr.status === 401) {
window.location = '/nifi/login';
}
deferred.reject(xhr, status, error);
});
}).promise();
// get the controller config to register the status poller
var configXhr = $.ajax({
type: 'GET',
url: config.urls.controllerConfig,
dataType: 'json'
});
// get the login config
var loginXhr = $.ajax({
type: 'GET',
url: config.urls.loginConfig,
dataType: 'json'
});
// create the deferred cluster request
var isClusteredRequest = $.Deferred(function (deferred) {
$.ajax({
type: 'HEAD',
url: config.urls.cluster
}).done(function (response, status, xhr) {
clustered = true;
deferred.resolve(response, status, xhr);
}).fail(function (xhr, status, error) {
if (xhr.status === 404) {
clustered = false;
deferred.resolve('', 'success', xhr);
} else {
deferred.reject(xhr, status, error);
}
});
}).promise();
userXhr.done(function () {
// get the controller config to register the status poller
var configXhr = $.ajax({
type: 'GET',
url: config.urls.controllerConfig,
dataType: 'json'
});
// ensure the config requests are loaded
$.when(configXhr, loginXhr, userXhr).done(function (configResult, loginResult) {
var configResponse = configResult[0];
var loginResponse = loginResult[0];
// get the login config
var loginXhr = $.ajax({
type: 'GET',
url: config.urls.loginConfig,
dataType: 'json'
});
// calculate the canvas offset
var canvasContainer = $('#canvas-container');
nf.Canvas.CANVAS_OFFSET = canvasContainer.offset().top;
// create the deferred cluster request
var isClusteredRequest = $.Deferred(function (deferred) {
$.ajax({
type: 'HEAD',
url: config.urls.cluster
}).done(function (response, status, xhr) {
clustered = true;
deferred.resolve(response, status, xhr);
}).fail(function (xhr, status, error) {
if (xhr.status === 404) {
clustered = false;
deferred.resolve('', 'success', xhr);
} else {
deferred.reject(xhr, status, error);
}
});
}).promise();
// ensure the config requests are loaded
$.when(configXhr, loginXhr, userXhr).done(function (configResult, loginResult) {
var configResponse = configResult[0];
var loginResponse = loginResult[0];
// get the config details
var configDetails = configResponse.config;
var loginDetails = loginResponse.config;
// calculate the canvas offset
var canvasContainer = $('#canvas-container');
nf.Canvas.CANVAS_OFFSET = canvasContainer.offset().top;
// when both request complete, load the application
isClusteredRequest.done(function () {
// get the auto refresh interval
var autoRefreshIntervalSeconds = parseInt(configDetails.autoRefreshIntervalSeconds, 10);
// get the config details
var configDetails = configResponse.config;
var loginDetails = loginResponse.config;
// initialize whether site to site is secure
secureSiteToSite = configDetails.siteToSiteSecure;
// when both request complete, load the application
isClusteredRequest.done(function () {
// get the auto refresh interval
var autoRefreshIntervalSeconds = parseInt(configDetails.autoRefreshIntervalSeconds, 10);
// load d3
loadD3().done(function () {
nf.Storage.init();
// initialize whether site to site is secure
secureSiteToSite = configDetails.siteToSiteSecure;
// initialize the application
initCanvas();
nf.Canvas.View.init();
nf.ContextMenu.init();
nf.CanvasToolbar.init();
nf.CanvasToolbox.init();
nf.CanvasHeader.init(loginDetails.supportsLogin);
nf.GraphControl.init();
nf.Search.init();
nf.Settings.init();
// load d3
loadD3().done(function () {
nf.Storage.init();
// initialize the component behaviors
nf.Draggable.init();
nf.Selectable.init();
nf.Connectable.init();
// initialize the application
initCanvas();
nf.Canvas.View.init();
nf.ContextMenu.init();
nf.CanvasToolbar.init();
nf.CanvasToolbox.init();
nf.CanvasHeader.init(loginDetails.supportsLogin);
nf.GraphControl.init();
nf.Search.init();
nf.Settings.init();
// initialize the chart
nf.StatusHistory.init(configDetails.timeOffset);
// initialize the component behaviors
nf.Draggable.init();
nf.Selectable.init();
nf.Connectable.init();
// initialize the birdseye
nf.Birdseye.init();
// initialize the chart
nf.StatusHistory.init(configDetails.timeOffset);
// initialize components
nf.ConnectionConfiguration.init();
nf.ControllerService.init();
nf.ReportingTask.init();
nf.ProcessorConfiguration.init();
nf.ProcessGroupConfiguration.init();
nf.RemoteProcessGroupConfiguration.init();
nf.RemoteProcessGroupPorts.init();
nf.PortConfiguration.init();
nf.SecurePortConfiguration.init();
nf.LabelConfiguration.init();
nf.ProcessorDetails.init();
nf.ProcessGroupDetails.init();
nf.PortDetails.init();
nf.SecurePortDetails.init();
nf.ConnectionDetails.init();
nf.RemoteProcessGroupDetails.init();
nf.GoTo.init();
nf.Graph.init().done(function () {
// determine the split between the polling
var pollingSplit = autoRefreshIntervalSeconds / 2;
// initialize the birdseye
nf.Birdseye.init();
// register the revision and status polling
startRevisionPolling(autoRefreshIntervalSeconds);
setTimeout(function () {
startStatusPolling(autoRefreshIntervalSeconds);
}, pollingSplit * 1000);
// initialize components
nf.ConnectionConfiguration.init();
nf.ControllerService.init();
nf.ReportingTask.init();
nf.ProcessorConfiguration.init();
nf.ProcessGroupConfiguration.init();
nf.RemoteProcessGroupConfiguration.init();
nf.RemoteProcessGroupPorts.init();
nf.PortConfiguration.init();
nf.SecurePortConfiguration.init();
nf.LabelConfiguration.init();
nf.ProcessorDetails.init();
nf.ProcessGroupDetails.init();
nf.PortDetails.init();
nf.SecurePortDetails.init();
nf.ConnectionDetails.init();
nf.RemoteProcessGroupDetails.init();
nf.GoTo.init();
nf.Graph.init().done(function () {
// determine the split between the polling
var pollingSplit = autoRefreshIntervalSeconds / 2;
// hide the splash screen
nf.Canvas.hideSplash();
// register the revision and status polling
startRevisionPolling(autoRefreshIntervalSeconds);
setTimeout(function () {
startStatusPolling(autoRefreshIntervalSeconds);
}, pollingSplit * 1000);
// hide the splash screen
nf.Canvas.hideSplash();
}).fail(nf.Common.handleAjaxError);
}).fail(nf.Common.handleAjaxError);
}).fail(nf.Common.handleAjaxError);
}).fail(nf.Common.handleAjaxError);

View File

@ -231,6 +231,8 @@ nf.Login = (function () {
}).fail(function () {
// no token granted, user needs to login with their credentials
needsLogin = true;
}).always(function () {
deferred.resolve();
});
} else {
showMessage = true;
@ -241,13 +243,14 @@ nf.Login = (function () {
} else {
$('#login-message').text(xhr.responseText);
}
deferred.resolve();
}
deferred.resolve();
});
}).promise();
// render the page accordingly
$.when(pageStateInit).done(function () {
pageStateInit.done(function () {
if (showMessage === true) {
initializeMessage();
} else if (needsLogin === true) {