diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java index d6feef0a34..649f412525 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java @@ -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; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/RegistrationStatusFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/RegistrationStatusFilter.java index 073de67e2f..d2ffdc206b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/RegistrationStatusFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/RegistrationStatusFilter.java @@ -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 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); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/form/LoginAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/form/LoginAuthenticationFilter.java index c8556d0b5f..fcff545c70 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/form/LoginAuthenticationFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/form/LoginAuthenticationFilter.java @@ -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)); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java index 380289db89..b02a2a351e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java @@ -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 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 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; + } + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js index 551f33ee66..9ae3f4e192 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js @@ -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); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js index 28359d71be..1906620475 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js @@ -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); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js index 45ef691ad0..9bc3264865 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js @@ -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) {