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 1488aba57c..64ecc5ff40 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 @@ -24,7 +24,9 @@ import org.apache.nifi.web.security.anonymous.NiFiAnonymousUserFilter; import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter; import org.apache.nifi.web.security.jwt.JwtService; import org.apache.nifi.web.security.node.NodeAuthorizedUserFilter; -import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; +import org.apache.nifi.web.security.otp.OtpAuthenticationFilter; +import org.apache.nifi.web.security.otp.OtpService; +import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken; import org.apache.nifi.web.security.x509.X509AuthenticationFilter; import org.apache.nifi.web.security.x509.X509CertificateExtractor; import org.apache.nifi.web.security.x509.X509IdentityProvider; @@ -54,12 +56,14 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte private UserService userService; private AuthenticationUserDetailsService userDetailsService; private JwtService jwtService; + private OtpService otpService; private X509CertificateExtractor certificateExtractor; private X509IdentityProvider certificateIdentityProvider; private LoginIdentityProvider loginIdentityProvider; private NodeAuthorizedUserFilter nodeAuthorizedUserFilter; private JwtAuthenticationFilter jwtAuthenticationFilter; + private OtpAuthenticationFilter otpAuthenticationFilter; private X509AuthenticationFilter x509AuthenticationFilter; private NiFiAnonymousUserFilter anonymousAuthenticationFilter; @@ -69,9 +73,12 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte @Override public void configure(WebSecurity webSecurity) throws Exception { + // ignore the access endpoints for obtaining the access config, the access token + // granting, and access status for a given user (note: we are not ignoring the + // the /access/download-token and /access/ui-extension-token endpoints webSecurity .ignoring() - .antMatchers("/access/**"); + .antMatchers("/access", "/access/config", "/access/token"); } @Override @@ -95,6 +102,9 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte // jwt http.addFilterAfter(jwtFilterBean(), AnonymousAuthenticationFilter.class); + + // otp + http.addFilterAfter(otpFilterBean(), AnonymousAuthenticationFilter.class); } @Bean @@ -135,6 +145,20 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte return jwtAuthenticationFilter; } + public OtpAuthenticationFilter otpFilterBean() throws Exception { + if (otpAuthenticationFilter == null) { + otpAuthenticationFilter = new OtpAuthenticationFilter(); + otpAuthenticationFilter.setProperties(properties); + otpAuthenticationFilter.setAuthenticationManager(authenticationManager()); + + // only consider the tokens when configured for login + if (loginIdentityProvider != null) { + otpAuthenticationFilter.setOtpService(otpService); + } + } + return otpAuthenticationFilter; + } + @Bean public X509AuthenticationFilter x509FilterBean() throws Exception { if (x509AuthenticationFilter == null) { @@ -157,7 +181,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte } @Autowired - public void setUserDetailsService(AuthenticationUserDetailsService userDetailsService) { + public void setUserDetailsService(AuthenticationUserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } @@ -176,6 +200,11 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte this.jwtService = jwtService; } + @Autowired + public void setOtpService(OtpService otpService) { + this.otpService = otpService; + } + @Autowired public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) { this.loginIdentityProvider = loginIdentityProvider; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java index f994c52dfa..afaf3edcbf 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java @@ -17,7 +17,28 @@ package org.apache.nifi.web; import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.ClientResponse.Status; import com.sun.jersey.core.util.MultivaluedMapImpl; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.authorization.Authority; +import org.apache.nifi.cluster.manager.NodeResponse; +import org.apache.nifi.cluster.manager.exception.UnknownNodeException; +import org.apache.nifi.cluster.manager.impl.WebClusterManager; +import org.apache.nifi.cluster.node.Node; +import org.apache.nifi.cluster.protocol.NodeIdentifier; +import org.apache.nifi.controller.repository.claim.ContentDirection; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.user.NiFiUserDetails; +import org.apache.nifi.web.security.user.NiFiUserUtils; +import org.apache.nifi.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MultivaluedMap; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; @@ -27,23 +48,6 @@ import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.core.MultivaluedMap; -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.cluster.manager.NodeResponse; -import org.apache.nifi.cluster.manager.exception.UnknownNodeException; -import org.apache.nifi.cluster.manager.impl.WebClusterManager; -import org.apache.nifi.cluster.node.Node; -import org.apache.nifi.cluster.protocol.NodeIdentifier; -import org.apache.nifi.controller.repository.claim.ContentDirection; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.security.user.NiFiUserDetails; -import org.apache.nifi.web.util.WebUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; /** * @@ -54,17 +58,16 @@ public class StandardNiFiContentAccess implements ContentAccess { public static final String CLIENT_ID_PARAM = "clientId"; private static final Pattern FLOWFILE_CONTENT_URI_PATTERN = Pattern - .compile("/controller/process-groups/((?:root)|(?:[a-f0-9\\-]{36}))/connections/([a-f0-9\\-]{36})/flowfiles/([a-f0-9\\-]{36})/content"); + .compile("/controller/process-groups/((?:root)|(?:[a-f0-9\\-]{36}))/connections/([a-f0-9\\-]{36})/flowfiles/([a-f0-9\\-]{36})/content.*"); private static final Pattern PROVENANCE_CONTENT_URI_PATTERN = Pattern - .compile("/controller/provenance/events/([0-9]+)/content/((?:input)|(?:output))"); + .compile("/controller/provenance/events/([0-9]+)/content/((?:input)|(?:output)).*"); private NiFiProperties properties; private NiFiServiceFacade serviceFacade; private WebClusterManager clusterManager; @Override - @PreAuthorize("hasRole('ROLE_PROVENANCE')") public DownloadableContent getContent(final ContentRequestContext request) { // if clustered, send request to cluster manager if (properties.isClusterManager()) { @@ -99,6 +102,11 @@ public class StandardNiFiContentAccess implements ContentAccess { } } + // ensure we were able to detect the cluster node id + if (request.getClusterNodeId() == null) { + throw new IllegalArgumentException("Unable to determine the which node has the content."); + } + // get the target node and ensure it exists final Node targetNode = clusterManager.getNode(request.getClusterNodeId()); if (targetNode == null) { @@ -113,6 +121,16 @@ public class StandardNiFiContentAccess implements ContentAccess { final ClientResponse clientResponse = nodeResponse.getClientResponse(); final MultivaluedMap responseHeaders = clientResponse.getHeaders(); + // ensure an appropriate response + if (Status.NOT_FOUND.getStatusCode() == clientResponse.getStatusInfo().getStatusCode()) { + throw new ResourceNotFoundException(clientResponse.getEntity(String.class)); + } else if (Status.FORBIDDEN.getStatusCode() == clientResponse.getStatusInfo().getStatusCode() + || Status.UNAUTHORIZED.getStatusCode() == clientResponse.getStatusInfo().getStatusCode()) { + throw new AccessDeniedException(clientResponse.getEntity(String.class)); + } else if (Status.OK.getStatusCode() != clientResponse.getStatusInfo().getStatusCode()) { + throw new IllegalStateException(clientResponse.getEntity(String.class)); + } + // get the file name final String contentDisposition = responseHeaders.getFirst("Content-Disposition"); final String filename = StringUtils.substringBetween(contentDisposition, "filename=\"", "\""); @@ -140,7 +158,7 @@ public class StandardNiFiContentAccess implements ContentAccess { final String connectionId = flowFileMatcher.group(2); final String flowfileId = flowFileMatcher.group(3); - return serviceFacade.getContent(groupId, connectionId, flowfileId, dataUri); + return getFlowFileContent(groupId, connectionId, flowfileId, dataUri); } // provenance event content @@ -150,7 +168,7 @@ public class StandardNiFiContentAccess implements ContentAccess { final Long eventId = Long.parseLong(provenanceMatcher.group(1)); final ContentDirection direction = ContentDirection.valueOf(provenanceMatcher.group(2).toUpperCase()); - return serviceFacade.getContent(eventId, dataUri, direction); + return getProvenanceEventContent(eventId, dataUri, direction); } catch (final IllegalArgumentException iae) { throw new IllegalArgumentException("The specified data reference URI is not valid."); } @@ -161,6 +179,24 @@ public class StandardNiFiContentAccess implements ContentAccess { } } + private DownloadableContent getFlowFileContent(final String groupId, final String connectionId, final String flowfileId, final String dataUri) { + // ensure the user is authorized as DFM - not checking with @PreAuthorized annotation as aspect not trigger on call within a class + if (!NiFiUserUtils.getAuthorities().contains(Authority.ROLE_DFM.toString())) { + throw new AccessDeniedException("Access is denied."); + } + + return serviceFacade.getContent(groupId, connectionId, flowfileId, dataUri); + } + + private DownloadableContent getProvenanceEventContent(final Long eventId, final String dataUri, final ContentDirection direction) { + // ensure the user is authorized as Provenance - not checking with @PreAuthorized annotation as aspect not trigger on call within a class + if (!NiFiUserUtils.getAuthorities().contains(Authority.ROLE_PROVENANCE.toString())) { + throw new AccessDeniedException("Access is denied."); + } + + return serviceFacade.getContent(eventId, dataUri, direction); + } + public void setProperties(NiFiProperties properties) { this.properties = properties; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java index 9502bf2d81..cae1175482 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java @@ -86,6 +86,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration private AuditService auditService; @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public ControllerService getControllerService(String serviceIdentifier) { return controllerServiceLookup.getControllerService(serviceIdentifier); } @@ -157,6 +158,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration } @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public String getCurrentUserDn() { String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY; @@ -169,6 +171,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration } @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public String getCurrentUserName() { String userName = NiFiUser.ANONYMOUS_USER_IDENTITY; @@ -181,6 +184,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration } @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public ComponentDetails getComponentDetails(final NiFiWebRequestContext requestContext) throws ResourceNotFoundException, ClusterRequestException { final String id = requestContext.getId(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java index 7e9e77e08f..9667ad65ca 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java @@ -81,6 +81,7 @@ public class StandardNiFiWebContext implements NiFiWebContext { private AuditService auditService; @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public ControllerService getControllerService(String serviceIdentifier) { return controllerServiceLookup.getControllerService(serviceIdentifier); } @@ -128,6 +129,7 @@ public class StandardNiFiWebContext implements NiFiWebContext { } @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public String getCurrentUserDn() { String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY; @@ -140,6 +142,7 @@ public class StandardNiFiWebContext implements NiFiWebContext { } @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public String getCurrentUserName() { String userName = NiFiUser.ANONYMOUS_USER_IDENTITY; @@ -152,6 +155,7 @@ public class StandardNiFiWebContext implements NiFiWebContext { } @Override + @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')") public ProcessorInfo getProcessor(final NiFiWebContextConfig config) throws ResourceNotFoundException, ClusterRequestException { final Revision revision = config.getRevision(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java index 7bf9690e6b..24536fcb72 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import io.jsonwebtoken.JwtException; +import org.apache.nifi.user.NiFiUser; import org.apache.nifi.util.NiFiProperties; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; @@ -60,8 +61,11 @@ import org.apache.nifi.web.security.ProxiedEntitiesUtils; import org.apache.nifi.web.security.UntrustedProxyException; import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter; import org.apache.nifi.web.security.jwt.JwtService; +import org.apache.nifi.web.security.otp.OtpService; import org.apache.nifi.web.security.token.LoginAuthenticationToken; -import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; +import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken; +import org.apache.nifi.web.security.token.OtpAuthenticationToken; +import org.apache.nifi.web.security.user.NiFiUserUtils; import org.apache.nifi.web.security.x509.X509CertificateExtractor; import org.apache.nifi.web.security.x509.X509IdentityProvider; import org.slf4j.Logger; @@ -92,8 +96,9 @@ public class AccessResource extends ApplicationResource { private X509CertificateExtractor certificateExtractor; private X509IdentityProvider certificateIdentityProvider; private JwtService jwtService; + private OtpService otpService; - private AuthenticationUserDetailsService userDetailsService; + private AuthenticationUserDetailsService userDetailsService; /** * Retrieves the access configuration for this NiFi. @@ -285,7 +290,107 @@ public class AccessResource extends ApplicationResource { * @throws AuthenticationException if the proxy chain is not authorized */ private UserDetails checkAuthorization(final List proxyChain) throws AuthenticationException { - return userDetailsService.loadUserDetails(new NiFiAuthortizationRequestToken(proxyChain)); + return userDetailsService.loadUserDetails(new NiFiAuthorizationRequestToken(proxyChain)); + } + + /** + * Creates a single use access token for downloading FlowFile content. + * + * @param httpServletRequest the servlet request + * @return A token (string) + */ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + @Path("/download-token") + @ApiOperation( + value = "Creates a single use access token for downloading FlowFile content.", + notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " + + "It is used as a query parameter name 'access_token'.", + response = String.class + ) + @ApiResponses( + value = { + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " + + "(i.e. may not have any tokens to grant or be configured to support username/password login)"), + @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.") + } + ) + public Response createDownloadToken(@Context HttpServletRequest httpServletRequest) { + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Download tokens are only issued over HTTPS."); + } + + // if not configuration for login, don't consider credentials + if (loginIdentityProvider == null) { + throw new IllegalStateException("Download tokens not supported by this NiFi."); + } + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new AccessDeniedException("Unable to determine user details."); + } + + final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(user.getIdentity()); + + // generate otp for response + final String token = otpService.generateDownloadToken(authenticationToken); + + // build the response + final URI uri = URI.create(generateResourceUri("access", "download-token")); + return generateCreatedResponse(uri, token).build(); + } + + /** + * Creates a single use access token for accessing a NiFi UI extension. + * + * @param httpServletRequest the servlet request + * @return A token (string) + */ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + @Path("/ui-extension-token") + @ApiOperation( + value = "Creates a single use access token for accessing a NiFi UI extension.", + notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " + + "It is used as a query parameter name 'access_token'.", + response = String.class + ) + @ApiResponses( + value = { + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " + + "(i.e. may not have any tokens to grant or be configured to support username/password login)"), + @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.") + } + ) + public Response createUiExtensionToken(@Context HttpServletRequest httpServletRequest) { + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("UI extension access tokens are only issued over HTTPS."); + } + + // if not configuration for login, don't consider credentials + if (loginIdentityProvider == null) { + throw new IllegalStateException("UI extension access tokens not supported by this NiFi."); + } + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new AccessDeniedException("Unable to determine user details."); + } + + final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(user.getIdentity()); + + // generate otp for response + final String token = otpService.generateUiExtensionToken(authenticationToken); + + // build the response + final URI uri = URI.create(generateResourceUri("access", "ui-extension-token")); + return generateCreatedResponse(uri, token).build(); } /** @@ -302,6 +407,9 @@ public class AccessResource extends ApplicationResource { @Path("/token") @ApiOperation( value = "Creates a token for accessing the REST API via username/password", + notes = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + + "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + + "in the format 'Authorization: Bearer '.", response = String.class ) @ApiResponses( @@ -399,7 +507,7 @@ public class AccessResource extends ApplicationResource { private void authorizeProxyIfNecessary(final List proxyChain) throws AuthenticationException { if (proxyChain.size() > 1) { try { - userDetailsService.loadUserDetails(new NiFiAuthortizationRequestToken(proxyChain)); + userDetailsService.loadUserDetails(new NiFiAuthorizationRequestToken(proxyChain)); } catch (final UsernameNotFoundException unfe) { // if a username not found exception was thrown, the proxies were authorized and now // we can issue a new token to the end user which they will use to identify themselves @@ -427,6 +535,10 @@ public class AccessResource extends ApplicationResource { this.jwtService = jwtService; } + public void setOtpService(OtpService otpService) { + this.otpService = otpService; + } + public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { this.certificateExtractor = certificateExtractor; } @@ -435,7 +547,7 @@ public class AccessResource extends ApplicationResource { this.certificateIdentityProvider = certificateIdentityProvider; } - public void setUserDetailsService(AuthenticationUserDetailsService userDetailsService) { + public void setUserDetailsService(AuthenticationUserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml index b38bca5fdb..c9326b23bd 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml @@ -259,6 +259,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java index 82fe73a503..fe484901c1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java @@ -204,7 +204,7 @@ public class AccessTokenEndpointTest { * @throws Exception ex */ @Test - public void testUnkownUser() throws Exception { + public void testUnknownUser() throws Exception { String url = BASE_URL + "/access/token"; ClientResponse response = TOKEN_USER.testCreateToken(url, "not a real user", "not a real password"); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java index a6f6aec85c..9b053f081e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java @@ -21,6 +21,7 @@ import com.ibm.icu.text.CharsetMatch; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -71,7 +72,19 @@ public class ContentViewerController extends HttpServlet { final ServletContext servletContext = request.getServletContext(); final ContentAccess contentAccess = (ContentAccess) servletContext.getAttribute("nifi-content-access"); - final ContentRequestContext contentRequest = getContentRequest(request); + final ContentRequestContext contentRequest; + try { + contentRequest = getContentRequest(request); + } catch (final Exception e) { + request.setAttribute("title", "Error"); + request.setAttribute("messages", "Unable to interpret content request."); + + // forward to the error page + final ServletContext viewerContext = servletContext.getContext("/nifi"); + viewerContext.getRequestDispatcher("/message").forward(request, response); + return; + } + if (contentRequest.getDataUri() == null) { request.setAttribute("title", "Error"); request.setAttribute("messages", "The data reference must be specified."); @@ -274,26 +287,41 @@ public class ContentViewerController extends HttpServlet { * @return Get the content request context based on the specified request */ private ContentRequestContext getContentRequest(final HttpServletRequest request) { + final String ref = request.getParameter("ref"); + final String clientId = request.getParameter("clientId"); + final String proxiedEntitiesChain = request.getHeader("X-ProxiedEntitiesChain"); + + final URI refUri = URI.create(ref); + final String query = refUri.getQuery(); + final String[] queryParameters = query.split("&"); + + String rawClusterNodeId = null; + for (int i = 0; i < queryParameters.length; i++) { + if (queryParameters[0].startsWith("clusterNodeId=")) { + rawClusterNodeId = StringUtils.substringAfterLast(queryParameters[0], "clusterNodeId="); + } + } + final String clusterNodeId = rawClusterNodeId; + return new ContentRequestContext() { @Override public String getDataUri() { - return request.getParameter("ref"); + return ref; } @Override public String getClusterNodeId() { - final String ref = request.getParameter("ref"); - return StringUtils.substringAfterLast(ref, "clusterNodeId="); + return clusterNodeId; } @Override public String getClientId() { - return request.getParameter("clientId"); + return clientId; } @Override public String getProxiedEntitiesChain() { - return request.getHeader("X-ProxiedEntitiesChain"); + return proxiedEntitiesChain; } }; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp index 0f6fae3800..c0002844fe 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp @@ -25,14 +25,37 @@ + + + + + \n\ \n\ \n\ +\n\ nf.bulletin.board.style.tags=\n\ \n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties index e626859d9e..5443907b00 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties @@ -21,6 +21,7 @@ nf.canvas.script.tags=\n\ \n\ \n\ +\n\ \n\ \n\ \n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/cluster.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/cluster.properties index 183ec6a672..0fb872cd1f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/cluster.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/cluster.properties @@ -18,6 +18,7 @@ nf.cluster.script.tags=\n\ \n\ \n\ +\n\ \n\ nf.cluster.style.tags=\n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/counters.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/counters.properties index cfa7f361dd..4c947b4f4d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/counters.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/counters.properties @@ -18,6 +18,7 @@ nf.counters.script.tags=\n\ \n\ \n\ +\n\ \n\ nf.counters.style.tags=\n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/history.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/history.properties index 0850127586..6cee206ef8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/history.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/history.properties @@ -18,6 +18,7 @@ nf.history.script.tags=\n\ \n\ \n\ +\n\ \n\ \n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/login.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/login.properties index 7a65ac6540..7a1678ddcf 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/login.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/login.properties @@ -17,6 +17,7 @@ nf.login.script.tags=\n\ \n\ \n\ +\n\ nf.login.style.tags=\n\ \n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/provenance.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/provenance.properties index 8d825cb2e9..dee70b883c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/provenance.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/provenance.properties @@ -18,6 +18,7 @@ nf.provenance.script.tags=\n\ \n\ \n\ +\n\ \n\ nf.provenance.style.tags=\n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/summary.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/summary.properties index b7a26bae09..752ca68abd 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/summary.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/summary.properties @@ -18,6 +18,7 @@ nf.summary.script.tags=\n\ \n\ \n\ +\n\ \n\ \n\ \n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/templates.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/templates.properties index a9299fc935..c0cee4cff8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/templates.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/templates.properties @@ -18,6 +18,7 @@ nf.templates.script.tags=\n\ \n\ \n\ +\n\ \n\ nf.templates.style.tags=\n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/users.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/users.properties index 160cb49601..0b3736360a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/users.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/users.properties @@ -18,6 +18,7 @@ nf.users.script.tags=\n\ \n\ \n\ +\n\ \n\ nf.users.style.tags=\n\ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js index 15eda10366..0a9cf28eb9 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js @@ -125,7 +125,7 @@ nf.CanvasUtils = (function () { } } }, - + /** * Calculates the point on the specified bounding box that is closest to the * specified point. diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-custom-ui.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-custom-ui.js index 365831a615..469dcbae68 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-custom-ui.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-custom-ui.js @@ -26,22 +26,38 @@ nf.CustomUi = { * @argument {boolean} editable Whether the custom ui should support editing */ showCustomUi: function (id, uri, editable) { + return $.Deferred(function (deferred) { + nf.Common.getAccessToken('../nifi-api/access/ui-extension-token').done(function (uiExtensionToken) { + // record the processor id + $('#shell-close-button'); - // record the processor id - $('#shell-close-button'); + var revision = nf.Client.getRevision(); - var revision = nf.Client.getRevision(); + // build the customer ui params + var customUiParams = { + 'id': id, + 'processorId': id, // deprecated + 'revision': revision.version, + 'clientId': revision.clientId, + 'editable': editable + }; - // build the customer ui params - var customUiParams = { - 'id': id, - 'processorId': id, // deprecated - 'revision': revision.version, - 'clientId': revision.clientId, - 'editable': editable - }; + // conditionally include the ui extension token + if (!nf.Common.isBlank(uiExtensionToken)) { + customUiParams['access_token'] = uiExtensionToken; + } - // show the shell - return nf.Shell.showPage('..' + uri + '?' + $.param(customUiParams), false); + // show the shell + nf.Shell.showPage('..' + uri + '?' + $.param(customUiParams), false).done(function () { + deferred.resolve(); + }); + }).fail(function () { + nf.Dialog.showOkDialog({ + dialogContent: 'Unable to generate access token for accessing the advanced configuration dialog.', + overlayBackground: false + }); + deferred.resolve(); + }); + }).promise(); } }; \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-queue-listing.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-queue-listing.js index ef88c70036..76d368ee5c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-queue-listing.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-queue-listing.js @@ -22,6 +22,16 @@ */ nf.QueueListing = (function () { + /** + * Configuration object used to hold a number of configuration items. + */ + var config = { + urls: { + uiExtensionToken: '../nifi-api/access/ui-extension-token', + downloadToken: '../nifi-api/access/download-token' + } + }; + /** * Initializes the listing request status dialog. */ @@ -57,15 +67,33 @@ nf.QueueListing = (function () { var downloadContent = function () { var dataUri = $('#flowfile-uri').text() + '/content'; - // conditionally include the cluster node id - var clusterNodeId = $('#flowfile-cluster-node-id').text(); - if (!nf.Common.isBlank(clusterNodeId)) { - window.open(dataUri + '?' + $.param({ - 'clusterNodeId': clusterNodeId - })); - } else { - window.open(dataUri); - } + // perform the request once we've received a token + nf.Common.getAccessToken(config.urls.downloadToken).done(function (downloadToken) { + var parameters = {}; + + // conditionally include the ui extension token + if (!nf.Common.isBlank(downloadToken)) { + parameters['access_token'] = downloadToken; + } + + // conditionally include the cluster node id + var clusterNodeId = $('#flowfile-cluster-node-id').text(); + if (!nf.Common.isBlank(clusterNodeId)) { + parameters['clusterNodeId'] = clusterNodeId; + } + + // open the url + if ($.isEmptyObject(parameters)) { + window.open(dataUri); + } else { + window.open(dataUri + '?' + $.param(parameters)); + } + }).fail(function () { + nf.Dialog.showOkDialog({ + dialogContent: 'Unable to generate access token for downloading content.', + overlayBackground: false + }); + }); }; /** @@ -74,32 +102,80 @@ nf.QueueListing = (function () { var viewContent = function () { var dataUri = $('#flowfile-uri').text() + '/content'; - // conditionally include the cluster node id - var clusterNodeId = $('#flowfile-cluster-node-id').text(); - if (!nf.Common.isBlank(clusterNodeId)) { - var parameters = { - 'clusterNodeId': clusterNodeId + // generate tokens as necessary + var getAccessTokens = $.Deferred(function (deferred) { + if (nf.Storage.hasItem('jwt')) { + // generate a token for the ui extension and another for the callback + var uiExtensionToken = $.ajax({ + type: 'POST', + url: config.urls.uiExtensionToken + }); + var downloadToken = $.ajax({ + type: 'POST', + url: config.urls.downloadToken + }); + + // wait for each token + $.when(uiExtensionToken, downloadToken).done(function (uiExtensionTokenResult, downloadTokenResult) { + var uiExtensionToken = uiExtensionTokenResult[0]; + var downloadToken = downloadTokenResult[0]; + deferred.resolve(uiExtensionToken, downloadToken); + }).fail(function () { + nf.Dialog.showOkDialog({ + dialogContent: 'Unable to generate access token for viewing content.', + overlayBackground: false + }); + deferred.reject(); + }); + } else { + deferred.resolve('', ''); + } + }).promise(); + + // perform the request after we've received the tokens + getAccessTokens.done(function (uiExtensionToken, downloadToken) { + var dataUriParameters = {}; + + // conditionally include the cluster node id + var clusterNodeId = $('#flowfile-cluster-node-id').text(); + if (!nf.Common.isBlank(clusterNodeId)) { + dataUriParameters['clusterNodeId'] = clusterNodeId; + } + + // include the download token if applicable + if (!nf.Common.isBlank(downloadToken)) { + dataUriParameters['access_token'] = downloadToken; + } + + // include parameters if necessary + if ($.isEmptyObject(dataUriParameters) === false) { + dataUri = dataUri + '?' + $.param(dataUriParameters); + } + + // open the content viewer + var contentViewerUrl = $('#nifi-content-viewer-url').text(); + + // if there's already a query string don't add another ?... this assumes valid + // input meaning that if the url has already included a ? it also contains at + // least one query parameter + if (contentViewerUrl.indexOf('?') === -1) { + contentViewerUrl += '?'; + } else { + contentViewerUrl += '&'; + } + + var contentViewerParameters = { + 'ref': dataUri }; - dataUri = dataUri + '?' + $.param(parameters); - } + // include the download token if applicable + if (!nf.Common.isBlank(uiExtensionToken)) { + contentViewerParameters['access_token'] = uiExtensionToken; + } - // open the content viewer - var contentViewerUrl = $('#nifi-content-viewer-url').text(); - - // if there's already a query string don't add another ?... this assumes valid - // input meaning that if the url has already included a ? it also contains at - // least one query parameter - if (contentViewerUrl.indexOf('?') === -1) { - contentViewerUrl += '?'; - } else { - contentViewerUrl += '&'; - } - - // open the content viewer - window.open(contentViewerUrl + $.param({ - 'ref': dataUri - })); + // open the content viewer + window.open(contentViewerUrl + $.param(contentViewerParameters)); + }); }; /** diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-ajax-setup.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-ajax-setup.js new file mode 100644 index 0000000000..a04120797c --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-ajax-setup.js @@ -0,0 +1,39 @@ +/* + * 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. + */ + +/** + * Performs ajax setup for use within NiFi. + */ +$(document).ready(function () { + // include jwt when possible + $.ajaxSetup({ + 'beforeSend': function(xhr) { + var hadToken = nf.Storage.hasItem('jwt'); + + // get the token to include in all requests + var token = nf.Storage.getItem('jwt'); + if (token !== null) { + xhr.setRequestHeader('Authorization', 'Bearer ' + token); + } else { + // if the current user was logged in with a token and the token just expired, cancel the request + if (hadToken === true) { + return false; + } + } + } + }); +}); \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js index bf88a09a21..7bf401e330 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js @@ -51,24 +51,6 @@ $(document).ready(function () { $('div.loading-container').removeClass('ajax-loading'); }); - // include jwt when possible - $.ajaxSetup({ - 'beforeSend': function(xhr) { - var hadToken = nf.Storage.hasItem('jwt'); - - // get the token to include in all requests - var token = nf.Storage.getItem('jwt'); - if (token !== null) { - xhr.setRequestHeader('Authorization', 'Bearer ' + token); - } else { - // if the current user was logged in with a token and the token just expired, reload - if (hadToken === true) { - return false; - } - } - } - }); - // initialize the tooltips $('img.setting-icon').qtip(nf.Common.config.tooltipConfig); @@ -767,6 +749,29 @@ nf.Common = (function () { } }, + /** + * Gets an access token from the specified url. + * + * @param accessTokenUrl The access token + * @returns the access token as a deferred + */ + getAccessToken: function (accessTokenUrl) { + return $.Deferred(function (deferred) { + if (nf.Storage.hasItem('jwt')) { + $.ajax({ + type: 'POST', + url: accessTokenUrl + }).done(function (token) { + deferred.resolve(token); + }).fail(function () { + deferred.reject(); + }) + } else { + deferred.resolve(''); + } + }).promise(); + }, + /** * Constants for time duration formatting. */ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js index 7b0c3f61d6..efb90fe9ee 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js @@ -20,7 +20,20 @@ nf.Storage = (function () { // Store items for two days before being eligible for removal. - var TWO_DAYS = nf.Common.MILLIS_PER_DAY * 2; + var MILLIS_PER_DAY = 86400000; + var TWO_DAYS = MILLIS_PER_DAY * 2; + + var isUndefined = function (obj) { + return typeof obj === 'undefined'; + }; + + var isNull = function (obj) { + return obj === null; + }; + + var isDefinedAndNotNull = function (obj) { + return !isUndefined(obj) && !isNull(obj); + }; /** * Checks the expiration for the specified entry. @@ -29,7 +42,7 @@ nf.Storage = (function () { * @returns {boolean} */ var checkExpiration = function (entry) { - if (nf.Common.isDefinedAndNotNull(entry.expires)) { + if (isDefinedAndNotNull(entry.expires)) { // get the expiration var expires = new Date(entry.expires); var now = new Date(); @@ -52,7 +65,7 @@ nf.Storage = (function () { var entry = JSON.parse(localStorage.getItem(key)); // ensure the entry and item are present - if (nf.Common.isDefinedAndNotNull(entry)) { + if (isDefinedAndNotNull(entry)) { return entry; } else { return null; @@ -89,7 +102,7 @@ nf.Storage = (function () { */ setItem: function (key, item, expires) { // calculate the expiration - expires = nf.Common.isDefinedAndNotNull(expires) ? expires : new Date().valueOf() + TWO_DAYS; + expires = isDefinedAndNotNull(expires) ? expires : new Date().valueOf() + TWO_DAYS; // create the entry var entry = { @@ -132,7 +145,7 @@ nf.Storage = (function () { } // if the entry has the specified field return its value - if (nf.Common.isDefinedAndNotNull(entry['item'])) { + if (isDefinedAndNotNull(entry['item'])) { return entry['item']; } else { return null; @@ -153,7 +166,7 @@ nf.Storage = (function () { } // if the entry has the specified field return its value - if (nf.Common.isDefinedAndNotNull(entry['expires'])) { + if (isDefinedAndNotNull(entry['expires'])) { return entry['expires']; } else { return null; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/provenance/nf-provenance-table.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/provenance/nf-provenance-table.js index cdad348555..a977e08764 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/provenance/nf-provenance-table.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/provenance/nf-provenance-table.js @@ -37,7 +37,9 @@ nf.ProvenanceTable = (function () { provenance: '../nifi-api/controller/provenance', cluster: '../nifi-api/cluster', d3Script: 'js/d3/d3.min.js', - lineageScript: 'js/nf/provenance/nf-provenance-lineage.js' + lineageScript: 'js/nf/provenance/nf-provenance-lineage.js', + uiExtensionToken: '../nifi-api/access/ui-extension-token', + downloadToken: '../nifi-api/access/download-token' } }; @@ -78,17 +80,35 @@ nf.ProvenanceTable = (function () { var eventId = $('#provenance-event-id').text(); // build the url - var url = config.urls.provenance + '/events/' + encodeURIComponent(eventId) + '/content/' + encodeURIComponent(direction); + var dataUri = config.urls.provenance + '/events/' + encodeURIComponent(eventId) + '/content/' + encodeURIComponent(direction); - // conditionally include the cluster node id - var clusterNodeId = $('#provenance-event-cluster-node-id').text(); - if (!nf.Common.isBlank(clusterNodeId)) { - window.open(url + '?' + $.param({ - 'clusterNodeId': clusterNodeId - })); - } else { - window.open(url); - } + // perform the request once we've received a token + nf.Common.getAccessToken(config.urls.downloadToken).done(function (downloadToken) { + var parameters = {}; + + // conditionally include the ui extension token + if (!nf.Common.isBlank(downloadToken)) { + parameters['access_token'] = downloadToken; + } + + // conditionally include the cluster node id + var clusterNodeId = $('#provenance-event-cluster-node-id').text(); + if (!nf.Common.isBlank(clusterNodeId)) { + parameters['clusterNodeId'] = clusterNodeId; + } + + // open the url + if ($.isEmptyObject(parameters)) { + window.open(dataUri); + } else { + window.open(dataUri + '?' + $.param(parameters)); + } + }).fail(function () { + nf.Dialog.showOkDialog({ + dialogContent: 'Unable to generate access token for downloading content.', + overlayBackground: false + }); + }); }; /** @@ -103,32 +123,80 @@ nf.ProvenanceTable = (function () { // build the uri to the data var dataUri = controllerUri + '/provenance/events/' + encodeURIComponent(eventId) + '/content/' + encodeURIComponent(direction); - // conditionally include the cluster node id - var clusterNodeId = $('#provenance-event-cluster-node-id').text(); - if (!nf.Common.isBlank(clusterNodeId)) { - var parameters = { - 'clusterNodeId': clusterNodeId + // generate tokens as necessary + var getAccessTokens = $.Deferred(function (deferred) { + if (nf.Storage.hasItem('jwt')) { + // generate a token for the ui extension and another for the callback + var uiExtensionToken = $.ajax({ + type: 'POST', + url: config.urls.uiExtensionToken + }); + var downloadToken = $.ajax({ + type: 'POST', + url: config.urls.downloadToken + }); + + // wait for each token + $.when(uiExtensionToken, downloadToken).done(function (uiExtensionTokenResult, downloadTokenResult) { + var uiExtensionToken = uiExtensionTokenResult[0]; + var downloadToken = downloadTokenResult[0]; + deferred.resolve(uiExtensionToken, downloadToken); + }).fail(function () { + nf.Dialog.showOkDialog({ + dialogContent: 'Unable to generate access token for viewing content.', + overlayBackground: false + }); + deferred.reject(); + }); + } else { + deferred.resolve('', ''); + } + }).promise(); + + // perform the request after we've received the tokens + getAccessTokens.done(function (uiExtensionToken, downloadToken) { + var dataUriParameters = {}; + + // conditionally include the cluster node id + var clusterNodeId = $('#provenance-event-cluster-node-id').text(); + if (!nf.Common.isBlank(clusterNodeId)) { + dataUriParameters['clusterNodeId'] = clusterNodeId; + } + + // include the download token if applicable + if (!nf.Common.isBlank(downloadToken)) { + dataUriParameters['access_token'] = downloadToken; + } + + // include parameters if necessary + if ($.isEmptyObject(dataUriParameters) === false) { + dataUri = dataUri + '?' + $.param(dataUriParameters); + } + + // open the content viewer + var contentViewerUrl = $('#nifi-content-viewer-url').text(); + + // if there's already a query string don't add another ?... this assumes valid + // input meaning that if the url has already included a ? it also contains at + // least one query parameter + if (contentViewerUrl.indexOf('?') === -1) { + contentViewerUrl += '?'; + } else { + contentViewerUrl += '&'; + } + + var contentViewerParameters = { + 'ref': dataUri }; - dataUri = dataUri + '?' + $.param(parameters); - } + // include the download token if applicable + if (!nf.Common.isBlank(uiExtensionToken)) { + contentViewerParameters['access_token'] = uiExtensionToken; + } - // open the content viewer - var contentViewerUrl = $('#nifi-content-viewer-url').text(); - - // if there's already a query string don't add another ?... this assumes valid - // input meaning that if the url has already included a ? it also contains at - // least one query parameter - if (contentViewerUrl.indexOf('?') === -1) { - contentViewerUrl += '?'; - } else { - contentViewerUrl += '&'; - } - - // open the content viewer - window.open(contentViewerUrl + $.param({ - 'ref': dataUri - })); + // open the content viewer + window.open(contentViewerUrl + $.param(contentViewerParameters)); + }); }; /** diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/templates/nf-templates-table.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/templates/nf-templates-table.js index 9ec742f86d..0f9ea8a244 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/templates/nf-templates-table.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/templates/nf-templates-table.js @@ -28,7 +28,8 @@ nf.TemplatesTable = (function () { filterList: 'templates-filter-list' }, urls: { - templates: '../nifi-api/controller/templates' + templates: '../nifi-api/controller/templates', + downloadToken: '../nifi-api/access/download-token' } }; @@ -150,6 +151,34 @@ nf.TemplatesTable = (function () { return item[args.property].search(filterExp) >= 0; }; + /** + * Downloads the specified template. + * + * @param {object} template The template + */ + var downloadTemplate = function (template) { + nf.Common.getAccessToken(config.urls.downloadToken).done(function (downloadToken) { + var parameters = {}; + + // conditionally include the download token + if (!nf.Common.isBlank(downloadToken)) { + parameters['access_token'] = downloadToken; + } + + // open the url + if ($.isEmptyObject(parameters)) { + window.open(config.urls.templates + '/' + encodeURIComponent(template.id)); + } else { + window.open(config.urls.templates + '/' + encodeURIComponent(template.id) + '?' + $.param(parameters)); + } + }).fail(function () { + nf.Dialog.showOkDialog({ + dialogContent: 'Unable to generate access token for downloading content.', + overlayBackground: false + }); + }); + }; + return { /** * Initializes the templates list. @@ -262,7 +291,7 @@ nf.TemplatesTable = (function () { // determine the desired action if (templatesGrid.getColumns()[args.cell].id === 'actions') { if (target.hasClass('export-template')) { - window.open(config.urls.templates + '/' + encodeURIComponent(item.id)); + downloadTemplate(item); } else if (target.hasClass('prompt-to-delete-template')) { promptToDeleteTemplate(item); } diff --git a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/java/org/apache/nifi/update/attributes/api/RuleResource.java b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/java/org/apache/nifi/update/attributes/api/RuleResource.java index f68227ab1f..b211b726fe 100644 --- a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/java/org/apache/nifi/update/attributes/api/RuleResource.java +++ b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/java/org/apache/nifi/update/attributes/api/RuleResource.java @@ -148,7 +148,7 @@ public class RuleResource { try { criteria.reorder(requestEntity.getRuleOrder()); } catch (final IllegalArgumentException iae) { - throw new WebApplicationException(badRequest(iae.getMessage())); + throw new WebApplicationException(iae, badRequest(iae.getMessage())); } } @@ -157,7 +157,7 @@ public class RuleResource { try { criteria.setFlowFilePolicy(FlowFilePolicy.valueOf(requestEntity.getFlowFilePolicy())); } catch (final IllegalArgumentException iae) { - throw new WebApplicationException(badRequest("The specified matching strategy is unknown: " + requestEntity.getFlowFilePolicy())); + throw new WebApplicationException(iae, badRequest("The specified matching strategy is unknown: " + requestEntity.getFlowFilePolicy())); } } @@ -227,7 +227,7 @@ public class RuleResource { rule = factory.createRule(ruleDto); rule.setId(uuid); } catch (final IllegalArgumentException iae) { - throw new WebApplicationException(badRequest(iae.getMessage())); + throw new WebApplicationException(iae, badRequest(iae.getMessage())); } // add the rule @@ -268,7 +268,7 @@ public class RuleResource { condition = factory.createCondition(requestEntity.getCondition()); condition.setId(uuid); } catch (final IllegalArgumentException iae) { - throw new WebApplicationException(badRequest(iae.getMessage())); + throw new WebApplicationException(iae, badRequest(iae.getMessage())); } // build the response @@ -302,7 +302,7 @@ public class RuleResource { action = factory.createAction(requestEntity.getAction()); action.setId(uuid); } catch (final IllegalArgumentException iae) { - throw new WebApplicationException(badRequest(iae.getMessage())); + throw new WebApplicationException(iae, badRequest(iae.getMessage())); } // build the response @@ -522,7 +522,7 @@ public class RuleResource { rule.setConditions(conditions); rule.setActions(actions); } catch (final IllegalArgumentException iae) { - throw new WebApplicationException(badRequest(iae.getMessage())); + throw new WebApplicationException(iae, badRequest(iae.getMessage())); } // add the new rule if application @@ -598,11 +598,11 @@ public class RuleResource { // load the processor configuration processorDetails = configurationContext.getComponentDetails(requestContext); } catch (final InvalidRevisionException ire) { - throw new WebApplicationException(invalidRevision(ire.getMessage())); + throw new WebApplicationException(ire, invalidRevision(ire.getMessage())); } catch (final Exception e) { final String message = String.format("Unable to get UpdateAttribute[id=%s] criteria: %s", requestContext.getId(), e); logger.error(message, e); - throw new WebApplicationException(error(message)); + throw new WebApplicationException(e, error(message)); } Criteria criteria = null; @@ -612,7 +612,7 @@ public class RuleResource { } catch (final IllegalArgumentException iae) { final String message = String.format("Unable to deserialize existing rules for UpdateAttribute[id=%s]. Deserialization error: %s", requestContext.getId(), iae); logger.error(message, iae); - throw new WebApplicationException(error(message)); + throw new WebApplicationException(iae, error(message)); } } // ensure the criteria isn't null @@ -634,11 +634,11 @@ public class RuleResource { // save the annotation data configurationContext.setAnnotationData(requestContext, annotationData); } catch (final InvalidRevisionException ire) { - throw new WebApplicationException(invalidRevision(ire.getMessage())); + throw new WebApplicationException(ire, invalidRevision(ire.getMessage())); } catch (final Exception e) { final String message = String.format("Unable to save UpdateAttribute[id=%s] criteria: %s", requestContext.getId(), e); logger.error(message, e); - throw new WebApplicationException(error(message)); + throw new WebApplicationException(e, error(message)); } } diff --git a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/webapp/WEB-INF/jsp/worksheet.jsp b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/webapp/WEB-INF/jsp/worksheet.jsp index a7f93f1717..a92c4e0812 100644 --- a/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/webapp/WEB-INF/jsp/worksheet.jsp +++ b/nifi-nar-bundles/nifi-update-attribute-bundle/nifi-update-attribute-ui/src/main/webapp/WEB-INF/jsp/worksheet.jsp @@ -53,6 +53,9 @@ + + +