NIFI-1497: - Introducing a one time use password service for use in query parameters when accessing UI extensions and downloading resources. - Using one time use tokens when accessing ui extensions and downloading resources. - Ensuring appropriate roles when accessing component details through the web context for custom UIs. - Addressing typo in class name. - Ensuring appropriate roles when accessing content through the content access. - Code clean up. - Refactoring some basic scripts for accessing JWT tokens so UI extensions can reuse common functionality.

Signed-off-by: Bryan Bende <bbende@apache.org>
This commit is contained in:
Matt Gilman 2016-02-19 10:50:31 -05:00 committed by Bryan Bende
parent 86ab4428f0
commit a8edab2e79
47 changed files with 1505 additions and 184 deletions

View File

@ -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.JwtAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtService; import org.apache.nifi.web.security.jwt.JwtService;
import org.apache.nifi.web.security.node.NodeAuthorizedUserFilter; 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.X509AuthenticationFilter;
import org.apache.nifi.web.security.x509.X509CertificateExtractor; import org.apache.nifi.web.security.x509.X509CertificateExtractor;
import org.apache.nifi.web.security.x509.X509IdentityProvider; import org.apache.nifi.web.security.x509.X509IdentityProvider;
@ -54,12 +56,14 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
private UserService userService; private UserService userService;
private AuthenticationUserDetailsService userDetailsService; private AuthenticationUserDetailsService userDetailsService;
private JwtService jwtService; private JwtService jwtService;
private OtpService otpService;
private X509CertificateExtractor certificateExtractor; private X509CertificateExtractor certificateExtractor;
private X509IdentityProvider certificateIdentityProvider; private X509IdentityProvider certificateIdentityProvider;
private LoginIdentityProvider loginIdentityProvider; private LoginIdentityProvider loginIdentityProvider;
private NodeAuthorizedUserFilter nodeAuthorizedUserFilter; private NodeAuthorizedUserFilter nodeAuthorizedUserFilter;
private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationFilter jwtAuthenticationFilter;
private OtpAuthenticationFilter otpAuthenticationFilter;
private X509AuthenticationFilter x509AuthenticationFilter; private X509AuthenticationFilter x509AuthenticationFilter;
private NiFiAnonymousUserFilter anonymousAuthenticationFilter; private NiFiAnonymousUserFilter anonymousAuthenticationFilter;
@ -69,9 +73,12 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
@Override @Override
public void configure(WebSecurity webSecurity) throws Exception { 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 webSecurity
.ignoring() .ignoring()
.antMatchers("/access/**"); .antMatchers("/access", "/access/config", "/access/token");
} }
@Override @Override
@ -95,6 +102,9 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
// jwt // jwt
http.addFilterAfter(jwtFilterBean(), AnonymousAuthenticationFilter.class); http.addFilterAfter(jwtFilterBean(), AnonymousAuthenticationFilter.class);
// otp
http.addFilterAfter(otpFilterBean(), AnonymousAuthenticationFilter.class);
} }
@Bean @Bean
@ -135,6 +145,20 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
return jwtAuthenticationFilter; 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 @Bean
public X509AuthenticationFilter x509FilterBean() throws Exception { public X509AuthenticationFilter x509FilterBean() throws Exception {
if (x509AuthenticationFilter == null) { if (x509AuthenticationFilter == null) {
@ -157,7 +181,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
} }
@Autowired @Autowired
public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService) { public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService) {
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
} }
@ -176,6 +200,11 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
this.jwtService = jwtService; this.jwtService = jwtService;
} }
@Autowired
public void setOtpService(OtpService otpService) {
this.otpService = otpService;
}
@Autowired @Autowired
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) { public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
this.loginIdentityProvider = loginIdentityProvider; this.loginIdentityProvider = loginIdentityProvider;

View File

@ -17,7 +17,28 @@
package org.apache.nifi.web; package org.apache.nifi.web;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.core.util.MultivaluedMapImpl; 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.io.Serializable;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -27,23 +48,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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"; public static final String CLIENT_ID_PARAM = "clientId";
private static final Pattern FLOWFILE_CONTENT_URI_PATTERN = Pattern 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 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 NiFiProperties properties;
private NiFiServiceFacade serviceFacade; private NiFiServiceFacade serviceFacade;
private WebClusterManager clusterManager; private WebClusterManager clusterManager;
@Override @Override
@PreAuthorize("hasRole('ROLE_PROVENANCE')")
public DownloadableContent getContent(final ContentRequestContext request) { public DownloadableContent getContent(final ContentRequestContext request) {
// if clustered, send request to cluster manager // if clustered, send request to cluster manager
if (properties.isClusterManager()) { 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 // get the target node and ensure it exists
final Node targetNode = clusterManager.getNode(request.getClusterNodeId()); final Node targetNode = clusterManager.getNode(request.getClusterNodeId());
if (targetNode == null) { if (targetNode == null) {
@ -113,6 +121,16 @@ public class StandardNiFiContentAccess implements ContentAccess {
final ClientResponse clientResponse = nodeResponse.getClientResponse(); final ClientResponse clientResponse = nodeResponse.getClientResponse();
final MultivaluedMap<String, String> responseHeaders = clientResponse.getHeaders(); final MultivaluedMap<String, String> 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 // get the file name
final String contentDisposition = responseHeaders.getFirst("Content-Disposition"); final String contentDisposition = responseHeaders.getFirst("Content-Disposition");
final String filename = StringUtils.substringBetween(contentDisposition, "filename=\"", "\""); final String filename = StringUtils.substringBetween(contentDisposition, "filename=\"", "\"");
@ -140,7 +158,7 @@ public class StandardNiFiContentAccess implements ContentAccess {
final String connectionId = flowFileMatcher.group(2); final String connectionId = flowFileMatcher.group(2);
final String flowfileId = flowFileMatcher.group(3); final String flowfileId = flowFileMatcher.group(3);
return serviceFacade.getContent(groupId, connectionId, flowfileId, dataUri); return getFlowFileContent(groupId, connectionId, flowfileId, dataUri);
} }
// provenance event content // provenance event content
@ -150,7 +168,7 @@ public class StandardNiFiContentAccess implements ContentAccess {
final Long eventId = Long.parseLong(provenanceMatcher.group(1)); final Long eventId = Long.parseLong(provenanceMatcher.group(1));
final ContentDirection direction = ContentDirection.valueOf(provenanceMatcher.group(2).toUpperCase()); final ContentDirection direction = ContentDirection.valueOf(provenanceMatcher.group(2).toUpperCase());
return serviceFacade.getContent(eventId, dataUri, direction); return getProvenanceEventContent(eventId, dataUri, direction);
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
throw new IllegalArgumentException("The specified data reference URI is not valid."); 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) { public void setProperties(NiFiProperties properties) {
this.properties = properties; this.properties = properties;
} }

View File

@ -86,6 +86,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
private AuditService auditService; private AuditService auditService;
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public ControllerService getControllerService(String serviceIdentifier) { public ControllerService getControllerService(String serviceIdentifier) {
return controllerServiceLookup.getControllerService(serviceIdentifier); return controllerServiceLookup.getControllerService(serviceIdentifier);
} }
@ -157,6 +158,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
} }
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public String getCurrentUserDn() { public String getCurrentUserDn() {
String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY; String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY;
@ -169,6 +171,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
} }
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public String getCurrentUserName() { public String getCurrentUserName() {
String userName = NiFiUser.ANONYMOUS_USER_IDENTITY; String userName = NiFiUser.ANONYMOUS_USER_IDENTITY;
@ -181,6 +184,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
} }
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public ComponentDetails getComponentDetails(final NiFiWebRequestContext requestContext) throws ResourceNotFoundException, ClusterRequestException { public ComponentDetails getComponentDetails(final NiFiWebRequestContext requestContext) throws ResourceNotFoundException, ClusterRequestException {
final String id = requestContext.getId(); final String id = requestContext.getId();

View File

@ -81,6 +81,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
private AuditService auditService; private AuditService auditService;
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public ControllerService getControllerService(String serviceIdentifier) { public ControllerService getControllerService(String serviceIdentifier) {
return controllerServiceLookup.getControllerService(serviceIdentifier); return controllerServiceLookup.getControllerService(serviceIdentifier);
} }
@ -128,6 +129,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
} }
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public String getCurrentUserDn() { public String getCurrentUserDn() {
String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY; String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY;
@ -140,6 +142,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
} }
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public String getCurrentUserName() { public String getCurrentUserName() {
String userName = NiFiUser.ANONYMOUS_USER_IDENTITY; String userName = NiFiUser.ANONYMOUS_USER_IDENTITY;
@ -152,6 +155,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
} }
@Override @Override
@PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
public ProcessorInfo getProcessor(final NiFiWebContextConfig config) throws ResourceNotFoundException, ClusterRequestException { public ProcessorInfo getProcessor(final NiFiWebContextConfig config) throws ResourceNotFoundException, ClusterRequestException {
final Revision revision = config.getRevision(); final Revision revision = config.getRevision();

View File

@ -24,6 +24,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtException;
import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation; 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.UntrustedProxyException;
import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter; import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
import org.apache.nifi.web.security.jwt.JwtService; 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.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.X509CertificateExtractor;
import org.apache.nifi.web.security.x509.X509IdentityProvider; import org.apache.nifi.web.security.x509.X509IdentityProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -92,8 +96,9 @@ public class AccessResource extends ApplicationResource {
private X509CertificateExtractor certificateExtractor; private X509CertificateExtractor certificateExtractor;
private X509IdentityProvider certificateIdentityProvider; private X509IdentityProvider certificateIdentityProvider;
private JwtService jwtService; private JwtService jwtService;
private OtpService otpService;
private AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService; private AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService;
/** /**
* Retrieves the access configuration for this NiFi. * 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 * @throws AuthenticationException if the proxy chain is not authorized
*/ */
private UserDetails checkAuthorization(final List<String> proxyChain) throws AuthenticationException { private UserDetails checkAuthorization(final List<String> 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") @Path("/token")
@ApiOperation( @ApiOperation(
value = "Creates a token for accessing the REST API via username/password", 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 <token>'.",
response = String.class response = String.class
) )
@ApiResponses( @ApiResponses(
@ -399,7 +507,7 @@ public class AccessResource extends ApplicationResource {
private void authorizeProxyIfNecessary(final List<String> proxyChain) throws AuthenticationException { private void authorizeProxyIfNecessary(final List<String> proxyChain) throws AuthenticationException {
if (proxyChain.size() > 1) { if (proxyChain.size() > 1) {
try { try {
userDetailsService.loadUserDetails(new NiFiAuthortizationRequestToken(proxyChain)); userDetailsService.loadUserDetails(new NiFiAuthorizationRequestToken(proxyChain));
} catch (final UsernameNotFoundException unfe) { } catch (final UsernameNotFoundException unfe) {
// if a username not found exception was thrown, the proxies were authorized and now // 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 // 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; this.jwtService = jwtService;
} }
public void setOtpService(OtpService otpService) {
this.otpService = otpService;
}
public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
this.certificateExtractor = certificateExtractor; this.certificateExtractor = certificateExtractor;
} }
@ -435,7 +547,7 @@ public class AccessResource extends ApplicationResource {
this.certificateIdentityProvider = certificateIdentityProvider; this.certificateIdentityProvider = certificateIdentityProvider;
} }
public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService) { public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService) {
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
} }

View File

@ -259,6 +259,7 @@
<property name="certificateIdentityProvider" ref="certificateIdentityProvider"/> <property name="certificateIdentityProvider" ref="certificateIdentityProvider"/>
<property name="loginIdentityProvider" ref="loginIdentityProvider"/> <property name="loginIdentityProvider" ref="loginIdentityProvider"/>
<property name="jwtService" ref="jwtService"/> <property name="jwtService" ref="jwtService"/>
<property name="otpService" ref="otpService"/>
<property name="userDetailsService" ref="userDetailsService"/> <property name="userDetailsService" ref="userDetailsService"/>
</bean> </bean>

View File

@ -204,7 +204,7 @@ public class AccessTokenEndpointTest {
* @throws Exception ex * @throws Exception ex
*/ */
@Test @Test
public void testUnkownUser() throws Exception { public void testUnknownUser() throws Exception {
String url = BASE_URL + "/access/token"; String url = BASE_URL + "/access/token";
ClientResponse response = TOKEN_USER.testCreateToken(url, "not a real user", "not a real password"); ClientResponse response = TOKEN_USER.testCreateToken(url, "not a real user", "not a real password");

View File

@ -21,6 +21,7 @@ import com.ibm.icu.text.CharsetMatch;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -71,7 +72,19 @@ public class ContentViewerController extends HttpServlet {
final ServletContext servletContext = request.getServletContext(); final ServletContext servletContext = request.getServletContext();
final ContentAccess contentAccess = (ContentAccess) servletContext.getAttribute("nifi-content-access"); 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) { if (contentRequest.getDataUri() == null) {
request.setAttribute("title", "Error"); request.setAttribute("title", "Error");
request.setAttribute("messages", "The data reference must be specified."); 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 * @return Get the content request context based on the specified request
*/ */
private ContentRequestContext getContentRequest(final HttpServletRequest 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() { return new ContentRequestContext() {
@Override @Override
public String getDataUri() { public String getDataUri() {
return request.getParameter("ref"); return ref;
} }
@Override @Override
public String getClusterNodeId() { public String getClusterNodeId() {
final String ref = request.getParameter("ref"); return clusterNodeId;
return StringUtils.substringAfterLast(ref, "clusterNodeId=");
} }
@Override @Override
public String getClientId() { public String getClientId() {
return request.getParameter("clientId"); return clientId;
} }
@Override @Override
public String getProxiedEntitiesChain() { public String getProxiedEntitiesChain() {
return request.getHeader("X-ProxiedEntitiesChain"); return proxiedEntitiesChain;
} }
}; };
} }

View File

@ -25,14 +25,37 @@
<link href="../nifi/css/message-pane.css" rel="stylesheet" type="text/css" /> <link href="../nifi/css/message-pane.css" rel="stylesheet" type="text/css" />
<link href="../nifi/css/message-page.css" rel="stylesheet" type="text/css" /> <link href="../nifi/css/message-page.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="../nifi/js/jquery/combo/jquery.combo.css" type="text/css" /> <link rel="stylesheet" href="../nifi/js/jquery/combo/jquery.combo.css" type="text/css" />
<link rel="stylesheet" href="../nifi/js/jquery/modal/jquery.modal.css" type="text/css" />
<link rel="stylesheet" href="../nifi/css/reset.css" type="text/css" /> <link rel="stylesheet" href="../nifi/css/reset.css" type="text/css" />
<script type="text/javascript" src="../nifi/js/jquery/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="../nifi/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="../nifi/js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="../nifi/js/jquery/combo/jquery.combo.js"></script> <script type="text/javascript" src="../nifi/js/jquery/combo/jquery.combo.js"></script>
<script type="text/javascript" src="../nifi/js/jquery/modal/jquery.modal.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-namespace.js"></script> <script type="text/javascript" src="../nifi/js/nf/nf-namespace.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-storage.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-ajax-setup.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-universal-capture.js"></script> <script type="text/javascript" src="../nifi/js/nf/nf-universal-capture.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var $$ = $.noConflict(true); var $$ = $.noConflict(true);
$$(document).ready(function () { $$(document).ready(function () {
// initialize the dialog
$$('#content-viewer-message-dialog').modal({
overlayBackground: false,
buttons: [{
buttonText: 'Ok',
handler: {
click: function () {
$$('#content-viewer-message-dialog').modal('hide');
}
}
}],
handler: {
close: function () {
$$('#content-viewer-message').text('');
}
}
});
var url = $$('#requestUrl').text(); var url = $$('#requestUrl').text();
var ref = $$('#ref').text(); var ref = $$('#ref').text();
@ -77,9 +100,37 @@
// if the selection has changesd, reload the page // if the selection has changesd, reload the page
if (currentLocation !== option.value) { if (currentLocation !== option.value) {
window.location.href = url + '?' + $$.param($$.extend({ // get an access token if necessary
mode: option.value var getAccessToken = $.Deferred(function (deferred) {
}, params)); if (nf.Storage.hasItem('jwt')) {
$.ajax({
type: 'POST',
url: '../nifi-api/access/ui-extension-token'
}).done(function (token) {
deferred.resolve(token);
}).fail(function () {
$$('#content-viewer-message').text('Unable to generate a token to view the content.');
$$('#content-viewer-message-dialog').modal('show');
deferred.reject();
})
} else {
deferred.resolve('');
}
}).promise();
// reload as appropriate
getAccessToken.done(function (uiExtensionToken) {
var contentParameter = {
mode: option.value
};
// include the download token if applicable
if (typeof uiExtensionToken !== 'undefined' && uiExtensionToken !== null && $$.trim(uiExtensionToken) !== '') {
contentParameter['access_token'] = uiExtensionToken;
}
window.location.href = url + '?' + $$.param($$.extend(contentParameter, params));
});
} }
} }
}); });
@ -91,6 +142,11 @@
<span id="clusterNodeId" class="hidden"><%= request.getParameter("clusterNodeId") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getParameter("clusterNodeId")) %></span> <span id="clusterNodeId" class="hidden"><%= request.getParameter("clusterNodeId") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getParameter("clusterNodeId")) %></span>
<span id="mode" class="hidden"><%= request.getParameter("mode") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getParameter("mode")) %></span> <span id="mode" class="hidden"><%= request.getParameter("mode") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getParameter("mode")) %></span>
<span id="requestUrl" class="hidden"><%= org.apache.nifi.util.EscapeUtils.escapeHtml(request.getRequestURL().toString()) %></span> <span id="requestUrl" class="hidden"><%= org.apache.nifi.util.EscapeUtils.escapeHtml(request.getRequestURL().toString()) %></span>
<div id="content-viewer-message-dialog">
<div class="dialog-content" style="margin-top: -20px;">
<div id="content-viewer-message"></div>
</div>
</div>
<div id="view-as-label">View as</div> <div id="view-as-label">View as</div>
<div id="view-as" class="pointer button-normal"></div> <div id="view-as" class="pointer button-normal"></div>
<div id="content-filename"><span class="content-label">filename</span><%= request.getAttribute("filename") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getAttribute("filename").toString()) %></div> <div id="content-filename"><span class="content-label">filename</span><%= request.getAttribute("filename") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getAttribute("filename").toString()) %></div>

View File

@ -19,6 +19,14 @@
padding: 0; padding: 0;
} }
#content-viewer-message-dialog {
z-index: 1302;
display: none;
height: 225px;
width: 375px;
font-size: 62.5%
}
#view-as-label { #view-as-label {
position: absolute; position: absolute;
top: 72px; top: 72px;

View File

@ -27,7 +27,7 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.user.NiFiUser; import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.apache.nifi.web.security.user.NiFiUserUtils; import org.apache.nifi.web.security.user.NiFiUserUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -82,7 +82,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
private void authenticate(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { private void authenticate(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
String dnChain = null; String dnChain = null;
try { try {
final NiFiAuthortizationRequestToken authenticated = attemptAuthentication(request); final NiFiAuthorizationRequestToken authenticated = attemptAuthentication(request);
if (authenticated != null) { if (authenticated != null) {
dnChain = ProxiedEntitiesUtils.formatProxyDn(StringUtils.join(authenticated.getChain(), "><")); dnChain = ProxiedEntitiesUtils.formatProxyDn(StringUtils.join(authenticated.getChain(), "><"));
@ -125,7 +125,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
* @return The NiFiAutorizationRequestToken used to later authorized the client * @return The NiFiAutorizationRequestToken used to later authorized the client
* @throws InvalidAuthenticationException If the request contained an authentication attempt, but could not authenticate * @throws InvalidAuthenticationException If the request contained an authentication attempt, but could not authenticate
*/ */
public abstract NiFiAuthortizationRequestToken attemptAuthentication(HttpServletRequest request); public abstract NiFiAuthorizationRequestToken attemptAuthentication(HttpServletRequest request);
protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {

View File

@ -18,7 +18,7 @@ package org.apache.nifi.web.security;
import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken; import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken;
import org.apache.nifi.web.security.token.NewAccountAuthorizationToken; import org.apache.nifi.web.security.token.NewAccountAuthorizationToken;
import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.apache.nifi.web.security.token.NiFiAuthorizationToken; import org.apache.nifi.web.security.token.NiFiAuthorizationToken;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -32,15 +32,15 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
*/ */
public class NiFiAuthenticationProvider implements AuthenticationProvider { public class NiFiAuthenticationProvider implements AuthenticationProvider {
private final AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService; private final AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService;
public NiFiAuthenticationProvider(final AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService) { public NiFiAuthenticationProvider(final AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService) {
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
} }
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final NiFiAuthortizationRequestToken request = (NiFiAuthortizationRequestToken) authentication; final NiFiAuthorizationRequestToken request = (NiFiAuthorizationRequestToken) authentication;
try { try {
// defer to the nifi user details service to authorize the user // defer to the nifi user details service to authorize the user
@ -67,7 +67,7 @@ public class NiFiAuthenticationProvider implements AuthenticationProvider {
@Override @Override
public boolean supports(Class<?> authentication) { public boolean supports(Class<?> authentication) {
return NiFiAuthortizationRequestToken.class.isAssignableFrom(authentication); return NiFiAuthorizationRequestToken.class.isAssignableFrom(authentication);
} }
} }

View File

@ -58,8 +58,11 @@ public class NiFiAnonymousUserFilter extends AnonymousAuthenticationFilter {
user.getAuthorities().addAll(EnumSet.allOf(Authority.class)); user.getAuthorities().addAll(EnumSet.allOf(Authority.class));
} }
// only create an authentication token if the anonymous user has some authorities // only create an authentication token if the anonymous user has some authorities or they are accessing a ui
if (!user.getAuthorities().isEmpty()) { // extension. ui extensions have run this security filter but we shouldn't require authentication/authorization
// when accessing static resources like images, js, and css. authentication/authorization is required when
// interacting with nifi however and that will be verified in the NiFiWebContext or NiFiWebConfigurationContext
if (!user.getAuthorities().isEmpty() || !request.getContextPath().startsWith("/nifi-api")) {
NiFiUserDetails userDetails = new NiFiUserDetails(user); NiFiUserDetails userDetails = new NiFiUserDetails(user);
// get the granted authorities // get the granted authorities

View File

@ -30,7 +30,7 @@ import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.UntrustedProxyException; import org.apache.nifi.web.security.UntrustedProxyException;
import org.apache.nifi.web.security.user.NiFiUserDetails; import org.apache.nifi.web.security.user.NiFiUserDetails;
import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
@ -44,7 +44,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
/** /**
* UserDetailsService that will verify user identity and grant user authorities. * UserDetailsService that will verify user identity and grant user authorities.
*/ */
public class NiFiAuthorizationService implements AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> { public class NiFiAuthorizationService implements AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> {
private static final Logger logger = LoggerFactory.getLogger(NiFiAuthorizationService.class); private static final Logger logger = LoggerFactory.getLogger(NiFiAuthorizationService.class);
@ -63,7 +63,7 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
* @throws org.springframework.dao.DataAccessException ex * @throws org.springframework.dao.DataAccessException ex
*/ */
@Override @Override
public synchronized UserDetails loadUserDetails(NiFiAuthortizationRequestToken request) throws UsernameNotFoundException, DataAccessException { public synchronized UserDetails loadUserDetails(NiFiAuthorizationRequestToken request) throws UsernameNotFoundException, DataAccessException {
NiFiUserDetails userDetails = null; NiFiUserDetails userDetails = null;
final List<String> chain = new ArrayList<>(request.getChain()); final List<String> chain = new ArrayList<>(request.getChain());

View File

@ -20,7 +20,7 @@ import io.jsonwebtoken.JwtException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.web.security.NiFiAuthenticationFilter; import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken; import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken;
import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.apache.nifi.web.security.user.NewAccountRequest; import org.apache.nifi.web.security.user.NewAccountRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -40,7 +40,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
private JwtService jwtService; private JwtService jwtService;
@Override @Override
public NiFiAuthortizationRequestToken attemptAuthentication(final HttpServletRequest request) { public NiFiAuthorizationRequestToken attemptAuthentication(final HttpServletRequest request) {
// only suppport jwt login when running securely // only suppport jwt login when running securely
if (!request.isSecure()) { if (!request.isSecure()) {
return null; return null;
@ -68,7 +68,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
if (isNewAccountRequest(request)) { if (isNewAccountRequest(request)) {
return new NewAccountAuthorizationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request))); return new NewAccountAuthorizationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
} else { } else {
return new NiFiAuthortizationRequestToken(Arrays.asList(jwtPrincipal)); return new NiFiAuthorizationRequestToken(Arrays.asList(jwtPrincipal));
} }
} catch (JwtException e) { } catch (JwtException e) {
throw new InvalidAuthenticationException(e.getMessage(), e); throw new InvalidAuthenticationException(e.getMessage(), e);

View File

@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.otp;
import org.springframework.security.core.AuthenticationException;
/**
* Thrown if a one time use token fails authentication.
*/
public class OtpAuthenticationException extends AuthenticationException {
public OtpAuthenticationException(String msg) {
super(msg);
}
public OtpAuthenticationException(String msg, Throwable t) {
super(msg, t);
}
}

View File

@ -0,0 +1,96 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.otp;
import org.apache.nifi.web.security.InvalidAuthenticationException;
import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
*/
public class OtpAuthenticationFilter extends NiFiAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(OtpAuthenticationFilter.class);
private static final Pattern PROVENANCE_DOWNLOAD_PATTERN =
Pattern.compile("/controller/provenance/events/[0-9]+/content/(?:(?:output)|(?:input))");
private static final Pattern QUEUE_DOWNLOAD_PATTERN =
Pattern.compile("/controller/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/connections/[a-f0-9\\-]{36}/flowfiles/[a-f0-9\\-]{36}/content");
private static final Pattern TEMPLATE_DOWNLOAD_PATTERN =
Pattern.compile("/controller/templates/[a-f0-9\\-]{36}");
protected static final String ACCESS_TOKEN = "access_token";
private OtpService otpService;
@Override
public NiFiAuthorizationRequestToken attemptAuthentication(final HttpServletRequest request) {
// only support otp login when running securely
if (!request.isSecure()) {
return null;
}
// get the accessToken out of the query string
final String accessToken = request.getParameter(ACCESS_TOKEN);
// if there is no authorization header, we don't know the user
if (accessToken == null) {
return null;
} else {
if (otpService == null) {
throw new InvalidAuthenticationException("NiFi is not configured to support username/password logins.");
}
try {
String identity = null;
if (request.getContextPath().equals("/nifi-api")) {
if (isDownloadRequest(request.getPathInfo())) {
// handle download requests
identity = otpService.getAuthenticationFromDownloadToken(accessToken);
}
} else {
// handle requests to other context paths (other UI extensions)
identity = otpService.getAuthenticationFromUiExtensionToken(accessToken);
}
// the path is a support path for otp tokens
if (identity == null) {
return null;
}
return new NiFiAuthorizationRequestToken(Arrays.asList(identity));
} catch (final OtpAuthenticationException oae) {
throw new InvalidAuthenticationException(oae.getMessage(), oae);
}
}
}
private boolean isDownloadRequest(final String pathInfo) {
return PROVENANCE_DOWNLOAD_PATTERN.matcher(pathInfo).matches() || QUEUE_DOWNLOAD_PATTERN.matcher(pathInfo).matches() || TEMPLATE_DOWNLOAD_PATTERN.matcher(pathInfo).matches();
}
public void setOtpService(OtpService otpService) {
this.otpService = otpService;
}
}

View File

@ -0,0 +1,219 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.otp;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.commons.codec.binary.Base64;
import org.apache.nifi.web.security.token.OtpAuthenticationToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* OtpService is a service for generating and verifying one time password tokens.
*/
public class OtpService {
private static final Logger logger = LoggerFactory.getLogger(OtpService.class);
private static final String HMAC_SHA256 = "HmacSHA256";
// protected for testing purposes
protected static final int MAX_CACHE_SOFT_LIMIT = 100;
private final Cache<CacheKey, String> downloadTokenCache;
private final Cache<CacheKey, String> uiExtensionCache;
/**
* Creates a new OtpService with an expiration of 5 minutes.
*/
public OtpService() {
this(5, TimeUnit.MINUTES);
}
/**
* Creates a new OtpService.
*
* @param duration The expiration duration
* @param units The expiration units
* @throws NullPointerException If units is null
* @throws IllegalArgumentException If duration is negative
*/
public OtpService(final int duration, final TimeUnit units) {
downloadTokenCache = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
uiExtensionCache = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
}
/**
* Generates a download token for the specified authentication.
*
* @param authenticationToken The authentication
* @return The one time use download token
*/
public String generateDownloadToken(final OtpAuthenticationToken authenticationToken) {
return generateToken(downloadTokenCache.asMap(), authenticationToken);
}
/**
* Gets the authenticated identity from the specified one time use download token. This method will not return null.
*
* @param token The one time use download token
* @return The authenticated identity
* @throws OtpAuthenticationException When the specified token does not correspond to an authenticated identity
*/
public String getAuthenticationFromDownloadToken(final String token) throws OtpAuthenticationException {
return getAuthenticationFromToken(downloadTokenCache.asMap(), token);
}
/**
* Generates a UI extension token for the specified authentication.
*
* @param authenticationToken The authentication
* @return The one time use UI extension token
*/
public String generateUiExtensionToken(final OtpAuthenticationToken authenticationToken) {
return generateToken(uiExtensionCache.asMap(), authenticationToken);
}
/**
* Gets the authenticated identity from the specified one time use UI extension token. This method will not return null.
*
* @param token The one time use UI extension token
* @return The authenticated identity
* @throws OtpAuthenticationException When the specified token does not correspond to an authenticated identity
*/
public String getAuthenticationFromUiExtensionToken(final String token) throws OtpAuthenticationException {
return getAuthenticationFromToken(uiExtensionCache.asMap(), token);
}
/**
* Generates a token and stores it in the specified cache.
*
* @param cache The cache
* @param authenticationToken The authentication
* @return The one time use token
*/
private String generateToken(final ConcurrentMap<CacheKey, String> cache, final OtpAuthenticationToken authenticationToken) {
if (cache.size() >= MAX_CACHE_SOFT_LIMIT) {
throw new IllegalStateException("The maximum number of single use tokens have been issued.");
}
// hash the authentication and build a cache key
final CacheKey cacheKey = new CacheKey(hash(authenticationToken));
// store the token unless the token is already stored which should not update it's original timestamp
cache.putIfAbsent(cacheKey, authenticationToken.getName());
// return the token
return cacheKey.getToken();
}
/**
* Gets the corresponding authentication for the specified one time use token. The specified token will be removed.
*
* @param cache The cache
* @param token The one time use token
* @return The authenticated identity
*/
private String getAuthenticationFromToken(final ConcurrentMap<CacheKey, String> cache, final String token) throws OtpAuthenticationException {
final String authenticatedUser = cache.remove(new CacheKey(token));
if (authenticatedUser == null) {
throw new OtpAuthenticationException("Unable to validate the access token.");
}
return authenticatedUser;
}
/**
* Hashes the specified authentication token. The resulting value will be used as the one time use token.
*
* @param authenticationToken the authentication token
* @return the one time use token
*/
private String hash(final OtpAuthenticationToken authenticationToken) {
try {
// input is the user identity and timestamp
final String input = authenticationToken.getName() + "-" + System.nanoTime();
// create the secret using secure random
final SecureRandom secureRandom = new SecureRandom();
final byte[] randomBytes = new byte[32];
secureRandom.nextBytes(randomBytes);
final SecretKeySpec secret = new SecretKeySpec(randomBytes, HMAC_SHA256); // 256 bit
// hash the input
final Mac hmacSha256 = Mac.getInstance(HMAC_SHA256);
hmacSha256.init(secret);
final byte[] output = hmacSha256.doFinal(input.getBytes(StandardCharsets.UTF_8));
// return the result as a base 64 string
return Base64.encodeBase64URLSafeString(output);
} catch (final NoSuchAlgorithmException | InvalidKeyException e) {
final String errorMessage = "There was an error generating the OTP";
logger.error(errorMessage, e);
throw new IllegalStateException("Unable to generate single use token.");
}
}
/**
* Key for the cache. Necessary to override the default String.equals() to utilize MessageDigest.isEquals() to prevent timing attacks.
*/
private static class CacheKey {
final String token;
public CacheKey(String token) {
this.token = token;
}
public String getToken() {
return token;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final CacheKey otherCacheKey = (CacheKey) o;
return MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8), otherCacheKey.token.getBytes(StandardCharsets.UTF_8));
}
@Override
public int hashCode() {
return token.hashCode();
}
@Override
public String toString() {
return "CacheKey{token ending in '..." + token.substring(token.length() - 6) + "'}";
}
}
}

View File

@ -21,7 +21,7 @@ import org.apache.nifi.web.security.user.NewAccountRequest;
/** /**
* An authentication token that is used as an authorization request when submitting a new account. * An authentication token that is used as an authorization request when submitting a new account.
*/ */
public class NewAccountAuthorizationRequestToken extends NiFiAuthortizationRequestToken { public class NewAccountAuthorizationRequestToken extends NiFiAuthorizationRequestToken {
final NewAccountRequest newAccountRequest; final NewAccountRequest newAccountRequest;

View File

@ -24,11 +24,11 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
* An authentication token that is used as an authorization request. The request has already been authenticated and is now going to be authorized. * An authentication token that is used as an authorization request. The request has already been authenticated and is now going to be authorized.
* The request chain is specified during creation and is used authorize the user(s). * The request chain is specified during creation and is used authorize the user(s).
*/ */
public class NiFiAuthortizationRequestToken extends AbstractAuthenticationToken { public class NiFiAuthorizationRequestToken extends AbstractAuthenticationToken {
private final List<String> chain; private final List<String> chain;
public NiFiAuthortizationRequestToken(final List<String> chain) { public NiFiAuthorizationRequestToken(final List<String> chain) {
super(null); super(null);
this.chain = chain; this.chain = chain;
} }

View File

@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
/**
* This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token.
*/
public class OtpAuthenticationToken extends AbstractAuthenticationToken {
private final String identity;
/**
* Creates a representation of the otp authentication token for a user.
*
* @param identity The unique identifier for this user
*/
public OtpAuthenticationToken(final String identity) {
super(null);
setAuthenticated(true);
this.identity = identity;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return identity;
}
@Override
public String toString() {
return new StringBuilder("OtpAuthenticationToken for ")
.append(getName())
.toString();
}
}

View File

@ -24,7 +24,7 @@ import org.apache.nifi.web.security.InvalidAuthenticationException;
import org.apache.nifi.web.security.NiFiAuthenticationFilter; import org.apache.nifi.web.security.NiFiAuthenticationFilter;
import org.apache.nifi.web.security.ProxiedEntitiesUtils; import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken; import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken;
import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.apache.nifi.web.security.user.NewAccountRequest; import org.apache.nifi.web.security.user.NewAccountRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -40,7 +40,7 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
private X509IdentityProvider certificateIdentityProvider; private X509IdentityProvider certificateIdentityProvider;
@Override @Override
public NiFiAuthortizationRequestToken attemptAuthentication(final HttpServletRequest request) { public NiFiAuthorizationRequestToken attemptAuthentication(final HttpServletRequest request) {
// only suppport x509 login when running securely // only suppport x509 login when running securely
if (!request.isSecure()) { if (!request.isSecure()) {
return null; return null;
@ -64,7 +64,7 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
if (isNewAccountRequest(request)) { if (isNewAccountRequest(request)) {
return new NewAccountAuthorizationRequestToken(new NewAccountRequest(proxyChain, getJustification(request))); return new NewAccountAuthorizationRequestToken(new NewAccountRequest(proxyChain, getJustification(request)));
} else { } else {
return new NiFiAuthortizationRequestToken(proxyChain); return new NiFiAuthorizationRequestToken(proxyChain);
} }
} }

View File

@ -50,6 +50,9 @@
<constructor-arg ref="userService"/> <constructor-arg ref="userService"/>
</bean> </bean>
<!-- otp service -->
<bean id="otpService" class="org.apache.nifi.web.security.otp.OtpService"/>
<!-- login identity provider --> <!-- login identity provider -->
<bean id="loginIdentityProvider" class="org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean"> <bean id="loginIdentityProvider" class="org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean">
<property name="properties" ref="nifiProperties"/> <property name="properties" ref="nifiProperties"/>

View File

@ -26,7 +26,7 @@ import org.apache.nifi.authorization.Authority;
import org.apache.nifi.user.NiFiUser; import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.UntrustedProxyException; import org.apache.nifi.web.security.UntrustedProxyException;
import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.apache.nifi.web.security.user.NiFiUserDetails; import org.apache.nifi.web.security.user.NiFiUserDetails;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -104,8 +104,8 @@ public class NiFiAuthorizationServiceTest {
authorizationService.setUserService(userService); authorizationService.setUserService(userService);
} }
private NiFiAuthortizationRequestToken createRequestAuthentication(final String... identities) { private NiFiAuthorizationRequestToken createRequestAuthentication(final String... identities) {
return new NiFiAuthortizationRequestToken(Arrays.asList(identities)); return new NiFiAuthorizationRequestToken(Arrays.asList(identities));
} }
/** /**

View File

@ -0,0 +1,210 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.otp;
import org.apache.nifi.web.security.InvalidAuthenticationException;
import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class OtpAuthenticationFilterTest {
private final static String UI_EXTENSION_AUTHENTICATED_USER = "ui-extension-token-authenticated-user";
private final static String UI_EXTENSION_TOKEN = "ui-extension-token";
private final static String DOWNLOAD_AUTHENTICATED_USER = "download-token-authenticated-user";
private final static String DOWNLOAD_TOKEN = "download-token";
private OtpService otpService;
private OtpAuthenticationFilter otpAuthenticationFilter;
@Before
public void setUp() throws Exception {
otpService = mock(OtpService.class);
doAnswer(new Answer<String>() {
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
String downloadToken = (String) args[0];
if (DOWNLOAD_TOKEN.equals(downloadToken)) {
return DOWNLOAD_AUTHENTICATED_USER;
}
throw new OtpAuthenticationException("Invalid token");
}
}).when(otpService).getAuthenticationFromDownloadToken(anyString());
doAnswer(new Answer<String>() {
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
String uiExtensionToken = (String) args[0];
if (UI_EXTENSION_TOKEN.equals(uiExtensionToken)) {
return UI_EXTENSION_AUTHENTICATED_USER;
}
throw new OtpAuthenticationException("Invalid token");
}
}).when(otpService).getAuthenticationFromUiExtensionToken(anyString());
otpAuthenticationFilter = new OtpAuthenticationFilter();
otpAuthenticationFilter.setOtpService(otpService);
}
@Test
public void testInsecureHttp() throws Exception {
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(false);
assertNull(otpAuthenticationFilter.attemptAuthentication(request));
}
@Test
public void testNoAccessToken() throws Exception {
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn(null);
assertNull(otpAuthenticationFilter.attemptAuthentication(request));
}
@Test(expected = InvalidAuthenticationException.class)
public void testTokenSupportDisabled() throws Exception {
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn("my-access-token");
final OtpAuthenticationFilter noTokenAuthenticationFilter = new OtpAuthenticationFilter();
noTokenAuthenticationFilter.attemptAuthentication(request);
}
@Test
public void testUnsupportedDownloadPath() throws Exception {
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn("my-access-token");
when(request.getContextPath()).thenReturn("/nifi-api");
when(request.getPathInfo()).thenReturn("/controller/config");
assertNull(otpAuthenticationFilter.attemptAuthentication(request));
}
@Test
public void testUiExtensionPath() throws Exception {
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn(UI_EXTENSION_TOKEN);
when(request.getContextPath()).thenReturn("/nifi-update-attribute-ui");
final NiFiAuthorizationRequestToken result = otpAuthenticationFilter.attemptAuthentication(request);
final List<String> chain = result.getChain();
assertEquals(1, chain.size());
assertEquals(UI_EXTENSION_AUTHENTICATED_USER, chain.get(0));
verify(otpService, times(1)).getAuthenticationFromUiExtensionToken(UI_EXTENSION_TOKEN);
verify(otpService, never()).getAuthenticationFromDownloadToken(anyString());
}
@Test
public void testProvenanceInputContentDownload() throws Exception {
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn(DOWNLOAD_TOKEN);
when(request.getContextPath()).thenReturn("/nifi-api");
when(request.getPathInfo()).thenReturn("/controller/provenance/events/0/content/input");
final NiFiAuthorizationRequestToken result = otpAuthenticationFilter.attemptAuthentication(request);
final List<String> chain = result.getChain();
assertEquals(1, chain.size());
assertEquals(DOWNLOAD_AUTHENTICATED_USER, chain.get(0));
verify(otpService, never()).getAuthenticationFromUiExtensionToken(anyString());
verify(otpService, times(1)).getAuthenticationFromDownloadToken(DOWNLOAD_TOKEN);
}
@Test
public void testProvenanceOutputContentDownload() throws Exception {
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn(DOWNLOAD_TOKEN);
when(request.getContextPath()).thenReturn("/nifi-api");
when(request.getPathInfo()).thenReturn("/controller/provenance/events/0/content/output");
final NiFiAuthorizationRequestToken result = otpAuthenticationFilter.attemptAuthentication(request);
final List<String> chain = result.getChain();
assertEquals(1, chain.size());
assertEquals(DOWNLOAD_AUTHENTICATED_USER, chain.get(0));
verify(otpService, never()).getAuthenticationFromUiExtensionToken(anyString());
verify(otpService, times(1)).getAuthenticationFromDownloadToken(DOWNLOAD_TOKEN);
}
@Test
public void testFlowFileContentDownload() throws Exception {
final String uuid = UUID.randomUUID().toString();
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn(DOWNLOAD_TOKEN);
when(request.getContextPath()).thenReturn("/nifi-api");
when(request.getPathInfo()).thenReturn(String.format("/controller/process-groups/root/connections/%s/flowfiles/%s/content", uuid, uuid));
final NiFiAuthorizationRequestToken result = otpAuthenticationFilter.attemptAuthentication(request);
final List<String> chain = result.getChain();
assertEquals(1, chain.size());
assertEquals(DOWNLOAD_AUTHENTICATED_USER, chain.get(0));
verify(otpService, never()).getAuthenticationFromUiExtensionToken(anyString());
verify(otpService, times(1)).getAuthenticationFromDownloadToken(DOWNLOAD_TOKEN);
}
@Test
public void testTemplateDownload() throws Exception {
final String uuid = UUID.randomUUID().toString();
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.isSecure()).thenReturn(true);
when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn(DOWNLOAD_TOKEN);
when(request.getContextPath()).thenReturn("/nifi-api");
when(request.getPathInfo()).thenReturn(String.format("/controller/templates/%s", uuid));
final NiFiAuthorizationRequestToken result = otpAuthenticationFilter.attemptAuthentication(request);
final List<String> chain = result.getChain();
assertEquals(1, chain.size());
assertEquals(DOWNLOAD_AUTHENTICATED_USER, chain.get(0));
verify(otpService, never()).getAuthenticationFromUiExtensionToken(anyString());
verify(otpService, times(1)).getAuthenticationFromDownloadToken(DOWNLOAD_TOKEN);
}
}

View File

@ -0,0 +1,149 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.otp;
import org.apache.nifi.web.security.token.OtpAuthenticationToken;
import org.junit.Before;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class OtpServiceTest {
private final static String USER_1 = "user-identity-1";
private OtpService otpService;
@Before
public void setUp() throws Exception {
otpService = new OtpService();
}
@Test
public void testGetAuthenticationForValidDownloadToken() throws Exception {
final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(USER_1);
final String downloadToken = otpService.generateDownloadToken(authenticationToken);
final String authenticatedUser = otpService.getAuthenticationFromDownloadToken(downloadToken);
assertNotNull(authenticatedUser);
assertEquals(USER_1, authenticatedUser);
try {
// ensure the token is no longer valid
otpService.getAuthenticationFromDownloadToken(downloadToken);
fail();
} catch (final OtpAuthenticationException oae) {}
}
@Test
public void testGetAuthenticationForValidUiExtensionToken() throws Exception {
final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(USER_1);
final String uiExtensionToken = otpService.generateUiExtensionToken(authenticationToken);
final String authenticatedUser = otpService.getAuthenticationFromUiExtensionToken(uiExtensionToken);
assertNotNull(authenticatedUser);
assertEquals(USER_1, authenticatedUser);
try {
// ensure the token is no longer valid
otpService.getAuthenticationFromUiExtensionToken(uiExtensionToken);
fail();
} catch (final OtpAuthenticationException oae) {}
}
@Test(expected = OtpAuthenticationException.class)
public void testGetNonExistentDownloadToken() throws Exception {
otpService.getAuthenticationFromDownloadToken("Not a real download token");
}
@Test(expected = OtpAuthenticationException.class)
public void testGetNonExistentUiExtensionToken() throws Exception {
otpService.getAuthenticationFromUiExtensionToken("Not a real ui extension token");
}
@Test(expected = IllegalStateException.class)
public void testMaxDownloadTokenLimit() throws Exception {
// ensure we'll try to loop past the limit
for (int i = 1; i < OtpService.MAX_CACHE_SOFT_LIMIT + 10; i++) {
try {
final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken("user-identity-" + i);
otpService.generateDownloadToken(authenticationToken);
} catch (final IllegalStateException iae) {
// ensure we failed when we've past the limit
assertEquals(OtpService.MAX_CACHE_SOFT_LIMIT + 1, i);
throw iae;
}
}
}
@Test(expected = IllegalStateException.class)
public void testMaxUiExtensionTokenLimit() throws Exception {
// ensure we'll try to loop past the limit
for (int i = 1; i < OtpService.MAX_CACHE_SOFT_LIMIT + 10; i++) {
try {
final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken("user-identity-" + i);
otpService.generateUiExtensionToken(authenticationToken);
} catch (final IllegalStateException iae) {
// ensure we failed when we've past the limit
assertEquals(OtpService.MAX_CACHE_SOFT_LIMIT + 1, i);
throw iae;
}
}
}
@Test(expected = NullPointerException.class)
public void testNullTimeUnits() throws Exception {
new OtpService(0, null);
}
@Test(expected = IllegalArgumentException.class)
public void testNegativeExpiration() throws Exception {
new OtpService(-1, TimeUnit.MINUTES);
}
@Test(expected = OtpAuthenticationException.class)
public void testUiExtensionTokenExpiration() throws Exception {
final OtpService otpServiceWithTightExpiration = new OtpService(2, TimeUnit.SECONDS);
final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(USER_1);
final String downloadToken = otpServiceWithTightExpiration.generateUiExtensionToken(authenticationToken);
// sleep for 4 seconds which should sufficiently expire the valid token
Thread.sleep(4 * 1000);
// attempt to get the token now that its expired
otpServiceWithTightExpiration.getAuthenticationFromUiExtensionToken(downloadToken);
}
@Test(expected = OtpAuthenticationException.class)
public void testDownloadTokenExpiration() throws Exception {
final OtpService otpServiceWithTightExpiration = new OtpService(2, TimeUnit.SECONDS);
final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(USER_1);
final String downloadToken = otpServiceWithTightExpiration.generateDownloadToken(authenticationToken);
// sleep for 4 seconds which should sufficiently expire the valid token
Thread.sleep(4 * 1000);
// attempt to get the token now that its expired
otpServiceWithTightExpiration.getAuthenticationFromDownloadToken(downloadToken);
}
}

View File

@ -280,6 +280,7 @@
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-shell.js</include> <include>${staging.dir}/js/nf/nf-shell.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/canvas/nf-snippet.js</include> <include>${staging.dir}/js/nf/canvas/nf-snippet.js</include>
<include>${staging.dir}/js/nf/canvas/nf-canvas-toolbox.js</include> <include>${staging.dir}/js/nf/canvas/nf-canvas-toolbox.js</include>
<include>${staging.dir}/js/nf/canvas/nf-custom-ui.js</include> <include>${staging.dir}/js/nf/canvas/nf-custom-ui.js</include>
@ -336,6 +337,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/history/nf-history.js</include> <include>${staging.dir}/js/nf/history/nf-history.js</include>
<include>${staging.dir}/js/nf/history/nf-history-table.js</include> <include>${staging.dir}/js/nf/history/nf-history-table.js</include>
<include>${staging.dir}/js/nf/history/nf-history-model.js</include> <include>${staging.dir}/js/nf/history/nf-history-model.js</include>
@ -350,6 +352,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/provenance/nf-provenance.js</include> <include>${staging.dir}/js/nf/provenance/nf-provenance.js</include>
<include>${staging.dir}/js/nf/provenance/nf-provenance-table.js</include> <include>${staging.dir}/js/nf/provenance/nf-provenance-table.js</include>
</includes> </includes>
@ -363,6 +366,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/nf-processor-details.js</include> <include>${staging.dir}/js/nf/nf-processor-details.js</include>
<include>${staging.dir}/js/nf/nf-connection-details.js</include> <include>${staging.dir}/js/nf/nf-connection-details.js</include>
<include>${staging.dir}/js/nf/summary/nf-summary.js</include> <include>${staging.dir}/js/nf/summary/nf-summary.js</include>
@ -379,6 +383,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/counters/nf-counters.js</include> <include>${staging.dir}/js/nf/counters/nf-counters.js</include>
<include>${staging.dir}/js/nf/counters/nf-counters-table.js</include> <include>${staging.dir}/js/nf/counters/nf-counters-table.js</include>
</includes> </includes>
@ -392,6 +397,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/templates/nf-templates.js</include> <include>${staging.dir}/js/nf/templates/nf-templates.js</include>
<include>${staging.dir}/js/nf/templates/nf-templates-table.js</include> <include>${staging.dir}/js/nf/templates/nf-templates-table.js</include>
</includes> </includes>
@ -405,6 +411,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/cluster/nf-cluster.js</include> <include>${staging.dir}/js/nf/cluster/nf-cluster.js</include>
<include>${staging.dir}/js/nf/cluster/nf-cluster-table.js</include> <include>${staging.dir}/js/nf/cluster/nf-cluster-table.js</include>
</includes> </includes>
@ -418,6 +425,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/users/nf-users.js</include> <include>${staging.dir}/js/nf/users/nf-users.js</include>
<include>${staging.dir}/js/nf/users/nf-users-table.js</include> <include>${staging.dir}/js/nf/users/nf-users-table.js</include>
</includes> </includes>
@ -431,6 +439,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/bulletin-board/nf-bulletin-board.js</include> <include>${staging.dir}/js/nf/bulletin-board/nf-bulletin-board.js</include>
</includes> </includes>
</aggregation> </aggregation>
@ -443,6 +452,7 @@
<include>${staging.dir}/js/nf/nf-universal-capture.js</include> <include>${staging.dir}/js/nf/nf-universal-capture.js</include>
<include>${staging.dir}/js/nf/nf-dialog.js</include> <include>${staging.dir}/js/nf/nf-dialog.js</include>
<include>${staging.dir}/js/nf/nf-storage.js</include> <include>${staging.dir}/js/nf/nf-storage.js</include>
<include>${staging.dir}/js/nf/nf-ajax-setup.js</include>
<include>${staging.dir}/js/nf/login/nf-login.js</include> <include>${staging.dir}/js/nf/login/nf-login.js</include>
</includes> </includes>
</aggregation> </aggregation>
@ -622,6 +632,10 @@
js/nf/nf-namespace.js.gz, js/nf/nf-namespace.js.gz,
js/nf/nf-universal-capture.js, js/nf/nf-universal-capture.js,
js/nf/nf-universal-capture.js.gz, js/nf/nf-universal-capture.js.gz,
js/nf/nf-storage.js,
js/nf/nf-storage.js.gz,
js/nf/nf-ajax-setup.js,
js/nf/nf-ajax-setup.js.gz,
js/nf/nf-status-history.js, js/nf/nf-status-history.js,
js/nf/nf-status-history.js.gz, js/nf/nf-status-history.js.gz,
js/nf/canvas/nf-canvas-all.js, js/nf/canvas/nf-canvas-all.js,

View File

@ -18,6 +18,7 @@ nf.bulletin.board.script.tags=<script type="text/javascript" src="js/nf/nf-names
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/bulletin-board/nf-bulletin-board.js?${project.version}"></script> <script type="text/javascript" src="js/nf/bulletin-board/nf-bulletin-board.js?${project.version}"></script>
nf.bulletin.board.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\ nf.bulletin.board.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\
<link rel="stylesheet" href="css/main.css?${project.version}" type="text/css" />\n\ <link rel="stylesheet" href="css/main.css?${project.version}" type="text/css" />\n\

View File

@ -21,6 +21,7 @@ nf.canvas.script.tags=<script type="text/javascript" src="js/nf/nf-namespace.js?
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-shell.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-shell.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/canvas/nf-snippet.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/canvas/nf-snippet.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/canvas/nf-queue-listing.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/canvas/nf-queue-listing.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/canvas/nf-component-state.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/canvas/nf-component-state.js?${project.version}"></script>\n\

View File

@ -18,6 +18,7 @@ nf.cluster.script.tags=<script type="text/javascript" src="js/nf/nf-namespace.js
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/cluster/nf-cluster.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/cluster/nf-cluster.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/cluster/nf-cluster-table.js?${project.version}"></script> <script type="text/javascript" src="js/nf/cluster/nf-cluster-table.js?${project.version}"></script>
nf.cluster.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\ nf.cluster.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\

View File

@ -18,6 +18,7 @@ nf.counters.script.tags=<script type="text/javascript" src="js/nf/nf-namespace.j
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/counters/nf-counters.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/counters/nf-counters.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/counters/nf-counters-table.js?${project.version}"></script> <script type="text/javascript" src="js/nf/counters/nf-counters-table.js?${project.version}"></script>
nf.counters.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\ nf.counters.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\

View File

@ -18,6 +18,7 @@ nf.history.script.tags=<script type="text/javascript" src="js/nf/nf-namespace.js
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/history/nf-history.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/history/nf-history.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/history/nf-history-table.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/history/nf-history-table.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/history/nf-history-model.js?${project.version}"></script> <script type="text/javascript" src="js/nf/history/nf-history-model.js?${project.version}"></script>

View File

@ -17,6 +17,7 @@ nf.login.script.tags=<script type="text/javascript" src="js/nf/nf-common.js?${pr
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/login/nf-login.js?${project.version}"></script> <script type="text/javascript" src="js/nf/login/nf-login.js?${project.version}"></script>
nf.login.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\ nf.login.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\
<link rel="stylesheet" href="css/main.css?${project.version}" type="text/css" />\n\ <link rel="stylesheet" href="css/main.css?${project.version}" type="text/css" />\n\

View File

@ -18,6 +18,7 @@ nf.provenance.script.tags=<script type="text/javascript" src="js/nf/nf-namespace
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/provenance/nf-provenance.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/provenance/nf-provenance.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/provenance/nf-provenance-table.js?${project.version}"></script> <script type="text/javascript" src="js/nf/provenance/nf-provenance-table.js?${project.version}"></script>
nf.provenance.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\ nf.provenance.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\

View File

@ -18,6 +18,7 @@ nf.summary.script.tags=<script type="text/javascript" src="js/nf/nf-namespace.js
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-processor-details.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-processor-details.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-connection-details.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-connection-details.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/summary/nf-summary.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/summary/nf-summary.js?${project.version}"></script>\n\

View File

@ -18,6 +18,7 @@ nf.templates.script.tags=<script type="text/javascript" src="js/nf/nf-namespace.
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/templates/nf-templates.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/templates/nf-templates.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/templates/nf-templates-table.js?${project.version}"></script> <script type="text/javascript" src="js/nf/templates/nf-templates-table.js?${project.version}"></script>
nf.templates.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\ nf.templates.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\

View File

@ -18,6 +18,7 @@ nf.users.script.tags=<script type="text/javascript" src="js/nf/nf-namespace.js?$
<script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-universal-capture.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-dialog.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/nf-storage.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/nf-ajax-setup.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/users/nf-users.js?${project.version}"></script>\n\ <script type="text/javascript" src="js/nf/users/nf-users.js?${project.version}"></script>\n\
<script type="text/javascript" src="js/nf/users/nf-users-table.js?${project.version}"></script> <script type="text/javascript" src="js/nf/users/nf-users-table.js?${project.version}"></script>
nf.users.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\ nf.users.style.tags=<link rel="stylesheet" href="css/reset.css?${project.version}" type="text/css" />\n\

View File

@ -26,22 +26,38 @@ nf.CustomUi = {
* @argument {boolean} editable Whether the custom ui should support editing * @argument {boolean} editable Whether the custom ui should support editing
*/ */
showCustomUi: function (id, uri, editable) { 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 var revision = nf.Client.getRevision();
$('#shell-close-button');
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 // conditionally include the ui extension token
var customUiParams = { if (!nf.Common.isBlank(uiExtensionToken)) {
'id': id, customUiParams['access_token'] = uiExtensionToken;
'processorId': id, // deprecated }
'revision': revision.version,
'clientId': revision.clientId,
'editable': editable
};
// show the shell // show the shell
return nf.Shell.showPage('..' + uri + '?' + $.param(customUiParams), false); 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();
} }
}; };

View File

@ -22,6 +22,16 @@
*/ */
nf.QueueListing = (function () { 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. * Initializes the listing request status dialog.
*/ */
@ -57,15 +67,33 @@ nf.QueueListing = (function () {
var downloadContent = function () { var downloadContent = function () {
var dataUri = $('#flowfile-uri').text() + '/content'; var dataUri = $('#flowfile-uri').text() + '/content';
// conditionally include the cluster node id // perform the request once we've received a token
var clusterNodeId = $('#flowfile-cluster-node-id').text(); nf.Common.getAccessToken(config.urls.downloadToken).done(function (downloadToken) {
if (!nf.Common.isBlank(clusterNodeId)) { var parameters = {};
window.open(dataUri + '?' + $.param({
'clusterNodeId': clusterNodeId // conditionally include the ui extension token
})); if (!nf.Common.isBlank(downloadToken)) {
} else { parameters['access_token'] = downloadToken;
window.open(dataUri); }
}
// 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 viewContent = function () {
var dataUri = $('#flowfile-uri').text() + '/content'; var dataUri = $('#flowfile-uri').text() + '/content';
// conditionally include the cluster node id // generate tokens as necessary
var clusterNodeId = $('#flowfile-cluster-node-id').text(); var getAccessTokens = $.Deferred(function (deferred) {
if (!nf.Common.isBlank(clusterNodeId)) { if (nf.Storage.hasItem('jwt')) {
var parameters = { // generate a token for the ui extension and another for the callback
'clusterNodeId': clusterNodeId 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 // open the content viewer
var contentViewerUrl = $('#nifi-content-viewer-url').text(); window.open(contentViewerUrl + $.param(contentViewerParameters));
});
// 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
}));
}; };
/** /**

View File

@ -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;
}
}
}
});
});

View File

@ -51,24 +51,6 @@ $(document).ready(function () {
$('div.loading-container').removeClass('ajax-loading'); $('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 // initialize the tooltips
$('img.setting-icon').qtip(nf.Common.config.tooltipConfig); $('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. * Constants for time duration formatting.
*/ */

View File

@ -20,7 +20,20 @@
nf.Storage = (function () { nf.Storage = (function () {
// Store items for two days before being eligible for removal. // 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. * Checks the expiration for the specified entry.
@ -29,7 +42,7 @@ nf.Storage = (function () {
* @returns {boolean} * @returns {boolean}
*/ */
var checkExpiration = function (entry) { var checkExpiration = function (entry) {
if (nf.Common.isDefinedAndNotNull(entry.expires)) { if (isDefinedAndNotNull(entry.expires)) {
// get the expiration // get the expiration
var expires = new Date(entry.expires); var expires = new Date(entry.expires);
var now = new Date(); var now = new Date();
@ -52,7 +65,7 @@ nf.Storage = (function () {
var entry = JSON.parse(localStorage.getItem(key)); var entry = JSON.parse(localStorage.getItem(key));
// ensure the entry and item are present // ensure the entry and item are present
if (nf.Common.isDefinedAndNotNull(entry)) { if (isDefinedAndNotNull(entry)) {
return entry; return entry;
} else { } else {
return null; return null;
@ -89,7 +102,7 @@ nf.Storage = (function () {
*/ */
setItem: function (key, item, expires) { setItem: function (key, item, expires) {
// calculate the expiration // 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 // create the entry
var entry = { var entry = {
@ -132,7 +145,7 @@ nf.Storage = (function () {
} }
// if the entry has the specified field return its value // if the entry has the specified field return its value
if (nf.Common.isDefinedAndNotNull(entry['item'])) { if (isDefinedAndNotNull(entry['item'])) {
return entry['item']; return entry['item'];
} else { } else {
return null; return null;
@ -153,7 +166,7 @@ nf.Storage = (function () {
} }
// if the entry has the specified field return its value // if the entry has the specified field return its value
if (nf.Common.isDefinedAndNotNull(entry['expires'])) { if (isDefinedAndNotNull(entry['expires'])) {
return entry['expires']; return entry['expires'];
} else { } else {
return null; return null;

View File

@ -37,7 +37,9 @@ nf.ProvenanceTable = (function () {
provenance: '../nifi-api/controller/provenance', provenance: '../nifi-api/controller/provenance',
cluster: '../nifi-api/cluster', cluster: '../nifi-api/cluster',
d3Script: 'js/d3/d3.min.js', 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(); var eventId = $('#provenance-event-id').text();
// build the url // 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 // perform the request once we've received a token
var clusterNodeId = $('#provenance-event-cluster-node-id').text(); nf.Common.getAccessToken(config.urls.downloadToken).done(function (downloadToken) {
if (!nf.Common.isBlank(clusterNodeId)) { var parameters = {};
window.open(url + '?' + $.param({
'clusterNodeId': clusterNodeId // conditionally include the ui extension token
})); if (!nf.Common.isBlank(downloadToken)) {
} else { parameters['access_token'] = downloadToken;
window.open(url); }
}
// 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 // build the uri to the data
var dataUri = controllerUri + '/provenance/events/' + encodeURIComponent(eventId) + '/content/' + encodeURIComponent(direction); var dataUri = controllerUri + '/provenance/events/' + encodeURIComponent(eventId) + '/content/' + encodeURIComponent(direction);
// conditionally include the cluster node id // generate tokens as necessary
var clusterNodeId = $('#provenance-event-cluster-node-id').text(); var getAccessTokens = $.Deferred(function (deferred) {
if (!nf.Common.isBlank(clusterNodeId)) { if (nf.Storage.hasItem('jwt')) {
var parameters = { // generate a token for the ui extension and another for the callback
'clusterNodeId': clusterNodeId 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 // open the content viewer
var contentViewerUrl = $('#nifi-content-viewer-url').text(); window.open(contentViewerUrl + $.param(contentViewerParameters));
});
// 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
}));
}; };
/** /**

View File

@ -28,7 +28,8 @@ nf.TemplatesTable = (function () {
filterList: 'templates-filter-list' filterList: 'templates-filter-list'
}, },
urls: { 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; 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 { return {
/** /**
* Initializes the templates list. * Initializes the templates list.
@ -262,7 +291,7 @@ nf.TemplatesTable = (function () {
// determine the desired action // determine the desired action
if (templatesGrid.getColumns()[args.cell].id === 'actions') { if (templatesGrid.getColumns()[args.cell].id === 'actions') {
if (target.hasClass('export-template')) { if (target.hasClass('export-template')) {
window.open(config.urls.templates + '/' + encodeURIComponent(item.id)); downloadTemplate(item);
} else if (target.hasClass('prompt-to-delete-template')) { } else if (target.hasClass('prompt-to-delete-template')) {
promptToDeleteTemplate(item); promptToDeleteTemplate(item);
} }

View File

@ -148,7 +148,7 @@ public class RuleResource {
try { try {
criteria.reorder(requestEntity.getRuleOrder()); criteria.reorder(requestEntity.getRuleOrder());
} catch (final IllegalArgumentException iae) { } 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 { try {
criteria.setFlowFilePolicy(FlowFilePolicy.valueOf(requestEntity.getFlowFilePolicy())); criteria.setFlowFilePolicy(FlowFilePolicy.valueOf(requestEntity.getFlowFilePolicy()));
} catch (final IllegalArgumentException iae) { } 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 = factory.createRule(ruleDto);
rule.setId(uuid); rule.setId(uuid);
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
throw new WebApplicationException(badRequest(iae.getMessage())); throw new WebApplicationException(iae, badRequest(iae.getMessage()));
} }
// add the rule // add the rule
@ -268,7 +268,7 @@ public class RuleResource {
condition = factory.createCondition(requestEntity.getCondition()); condition = factory.createCondition(requestEntity.getCondition());
condition.setId(uuid); condition.setId(uuid);
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
throw new WebApplicationException(badRequest(iae.getMessage())); throw new WebApplicationException(iae, badRequest(iae.getMessage()));
} }
// build the response // build the response
@ -302,7 +302,7 @@ public class RuleResource {
action = factory.createAction(requestEntity.getAction()); action = factory.createAction(requestEntity.getAction());
action.setId(uuid); action.setId(uuid);
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
throw new WebApplicationException(badRequest(iae.getMessage())); throw new WebApplicationException(iae, badRequest(iae.getMessage()));
} }
// build the response // build the response
@ -522,7 +522,7 @@ public class RuleResource {
rule.setConditions(conditions); rule.setConditions(conditions);
rule.setActions(actions); rule.setActions(actions);
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
throw new WebApplicationException(badRequest(iae.getMessage())); throw new WebApplicationException(iae, badRequest(iae.getMessage()));
} }
// add the new rule if application // add the new rule if application
@ -598,11 +598,11 @@ public class RuleResource {
// load the processor configuration // load the processor configuration
processorDetails = configurationContext.getComponentDetails(requestContext); processorDetails = configurationContext.getComponentDetails(requestContext);
} catch (final InvalidRevisionException ire) { } catch (final InvalidRevisionException ire) {
throw new WebApplicationException(invalidRevision(ire.getMessage())); throw new WebApplicationException(ire, invalidRevision(ire.getMessage()));
} catch (final Exception e) { } catch (final Exception e) {
final String message = String.format("Unable to get UpdateAttribute[id=%s] criteria: %s", requestContext.getId(), e); final String message = String.format("Unable to get UpdateAttribute[id=%s] criteria: %s", requestContext.getId(), e);
logger.error(message, e); logger.error(message, e);
throw new WebApplicationException(error(message)); throw new WebApplicationException(e, error(message));
} }
Criteria criteria = null; Criteria criteria = null;
@ -612,7 +612,7 @@ public class RuleResource {
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
final String message = String.format("Unable to deserialize existing rules for UpdateAttribute[id=%s]. Deserialization error: %s", requestContext.getId(), 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); logger.error(message, iae);
throw new WebApplicationException(error(message)); throw new WebApplicationException(iae, error(message));
} }
} }
// ensure the criteria isn't null // ensure the criteria isn't null
@ -634,11 +634,11 @@ public class RuleResource {
// save the annotation data // save the annotation data
configurationContext.setAnnotationData(requestContext, annotationData); configurationContext.setAnnotationData(requestContext, annotationData);
} catch (final InvalidRevisionException ire) { } catch (final InvalidRevisionException ire) {
throw new WebApplicationException(invalidRevision(ire.getMessage())); throw new WebApplicationException(ire, invalidRevision(ire.getMessage()));
} catch (final Exception e) { } catch (final Exception e) {
final String message = String.format("Unable to save UpdateAttribute[id=%s] criteria: %s", requestContext.getId(), e); final String message = String.format("Unable to save UpdateAttribute[id=%s] criteria: %s", requestContext.getId(), e);
logger.error(message, e); logger.error(message, e);
throw new WebApplicationException(error(message)); throw new WebApplicationException(e, error(message));
} }
} }

View File

@ -53,6 +53,9 @@
<script type="text/javascript" src="../nifi/js/jquery/slickgrid/slick.grid.js"></script> <script type="text/javascript" src="../nifi/js/jquery/slickgrid/slick.grid.js"></script>
<script type="text/javascript" src="../nifi/js/codemirror/lib/codemirror-compressed.js"></script> <script type="text/javascript" src="../nifi/js/codemirror/lib/codemirror-compressed.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-namespace.js"></script> <script type="text/javascript" src="../nifi/js/nf/nf-namespace.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-common.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-storage.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-ajax-setup.js"></script>
<script type="text/javascript" src="../nifi/js/nf/nf-universal-capture.js"></script> <script type="text/javascript" src="../nifi/js/nf/nf-universal-capture.js"></script>
<script type="text/javascript" src="../nifi/js/jquery/nfeditor/languages/nfel.js"></script> <script type="text/javascript" src="../nifi/js/jquery/nfeditor/languages/nfel.js"></script>
<script type="text/javascript" src="../nifi/js/jquery/nfeditor/jquery.nfeditor.js"></script> <script type="text/javascript" src="../nifi/js/jquery/nfeditor/jquery.nfeditor.js"></script>