NIFI-655:

- Starting to implement the JWT service.
- Parsing JWT on client side in order to render who the user currently is when logged in.
This commit is contained in:
Matt Gilman 2015-11-05 18:26:00 -05:00
parent 93aa09dace
commit b6d09b86b6
18 changed files with 525 additions and 217 deletions

22
LICENSE
View File

@ -374,6 +374,28 @@ For details see http://jqueryui.com
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product bundles 'jquery.base64.js' which is available under an MIT style license.
Copyright (c) 2013 Yannick Albert (http://yckart.com/)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product bundles 'SlickGrid v2.2' which is available under an MIT style license.
Copyright (c) 2010 Michael Leibman, http://github.com/mleibman/slickgrid

View File

@ -374,6 +374,28 @@ For details see http://jqueryui.com
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product bundles 'jquery.base64.js' which is available under an MIT style license.
Copyright (c) 2013 Yannick Albert (http://yckart.com/)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product bundles 'SlickGrid v2.2' which is available under an MIT style license.
Copyright (c) 2010 Michael Leibman, http://github.com/mleibman/slickgrid

View File

@ -77,6 +77,11 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-core</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>

View File

@ -98,7 +98,7 @@ public class RegistrationStatusFilter extends AbstractAuthenticationProcessingFi
return new RegistrationStatusAuthenticationToken(tokenCredentials);
} else {
// we have a certificate so let's consider a proxy chain
final String principal = extractPrincipal(certificate);
final String principal = principalExtractor.extractPrincipal(certificate).toString();
try {
// validate the certificate
@ -144,12 +144,6 @@ public class RegistrationStatusFilter extends AbstractAuthenticationProcessingFi
userDetailsService.loadUserDetails(new NiFiAuthenticationRequestToken(proxyChain));
}
private String extractPrincipal(final X509Certificate certificate) {
// extract the principal
final Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
return ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
}
@Override
protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authentication)
throws IOException, ServletException {

View File

@ -44,7 +44,6 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
/**
@ -88,16 +87,16 @@ public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingF
// if there is no certificate, look for an existing token
if (certificate == null) {
final String principal = jwtService.getAuthentication(request);
if (principal == null) {
throw new AuthenticationCredentialsNotFoundException("Unable to issue token as issue token as no credentials were found in the request.");
}
final LoginCredentials tokenCredentials = new LoginCredentials(principal, null);
return new LoginAuthenticationToken(tokenCredentials);
} else {
// extract the principal
final String principal = extractPrincipal(certificate);
final String principal = principalExtractor.extractPrincipal(certificate).toString();
try {
certificateValidator.validateClientCertificate(request, certificate);
@ -151,16 +150,14 @@ public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingF
} catch (final UsernameNotFoundException unfe) {
// if a username not found exception was thrown, the proxies were authorized and now
// we can issue a new ID token to the end user
} catch (final Exception e) {
// any other issue we're going to treat as an authentication exception which will return 401
throw new AuthenticationException(e.getMessage(), e) {
};
}
}
}
private String extractPrincipal(final X509Certificate certificate) {
// extract the principal
final Object certificatePrincipal = principalExtractor.extractPrincipal(certificate);
return ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString());
}
private LoginCredentials getLoginCredentials(HttpServletRequest request) {
final String username = request.getParameter("username");
final String password = request.getParameter("password");
@ -178,20 +175,15 @@ public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingF
// generate JWT for response
jwtService.addToken(response, authentication);
// mark as successful
response.setStatus(HttpServletResponse.SC_CREATED);
response.setContentType("text/plain");
response.setContentLength(0);
}
@Override
protected void unsuccessfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException failed) throws IOException, ServletException {
response.setContentType("text/plain");
final PrintWriter out = response.getWriter();
out.println(failed.getMessage());
if (failed instanceof BadCredentialsException || failed instanceof AuthenticationCredentialsNotFoundException) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
} else {

View File

@ -16,9 +16,22 @@
*/
package org.apache.nifi.web.security.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.TextCodec;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Calendar;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.util.NiFiProperties;
import org.springframework.security.core.Authentication;
/**
@ -28,6 +41,16 @@ public class JwtService {
private final static String AUTHORIZATION = "Authorization";
private final String key;
private final Integer expires;
public JwtService(final NiFiProperties properties) {
// TODO - load key (and algo/provider?) and expiration from properties
key = TextCodec.BASE64.encode("nififtw!");
expires = 1;
}
/**
* Gets the Authentication by extracting a JWT token from the specified request.
*
@ -35,12 +58,16 @@ public class JwtService {
* @return The user identifier from the token
*/
public String getAuthentication(final HttpServletRequest request) {
// TODO : actually extract/verify token
// extract/verify token from incoming request
final String authorization = request.getHeader(AUTHORIZATION);
final String username = StringUtils.substringAfterLast(authorization, " ");
return username;
final String token = StringUtils.substringAfterLast(authorization, " ");
try {
final Jws<Claims> jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(token);
return jwt.getBody().getSubject();
} catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException e) {
return null;
}
}
/**
@ -48,14 +75,25 @@ public class JwtService {
*
* @param response The response to add the token to
* @param authentication The authentication to generate a token for
* @throws java.io.IOException if an io exception occurs
*/
public void addToken(final HttpServletResponse response, final Authentication authentication) {
// TODO : actually create real token... in header or response body?
public void addToken(final HttpServletResponse response, final Authentication authentication) throws IOException {
// set expiration to one day from now
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, expires);
// create a token the specified authentication
String token = authentication.getName();
final String identity = authentication.getPrincipal().toString();
final String username = authentication.getName();
final String token = Jwts.builder().setSubject(identity).claim("preferred_username", username).setExpiration(calendar.getTime()).signWith(SignatureAlgorithm.HS512, key).compact();
// add the token as a response header
response.setHeader(AUTHORIZATION, "Bearer " + token);
final PrintWriter out = response.getWriter();
out.print(token);
// mark the response as successful
response.setStatus(HttpServletResponse.SC_CREATED);
response.setContentType("text/plain");
}
}

View File

@ -17,6 +17,7 @@
package org.apache.nifi.web.security.token;
import org.apache.nifi.authentication.LoginCredentials;
import org.apache.nifi.security.util.CertificateUtils;
import org.springframework.security.authentication.AbstractAuthenticationToken;
/**
@ -45,4 +46,11 @@ public class LoginAuthenticationToken extends AbstractAuthenticationToken {
public Object getPrincipal() {
return credentials.getUsername();
}
@Override
public String getName() {
// if the username is a DN this will extract the username or CN... if not will return what was passed
return CertificateUtils.extractUsername(credentials.getUsername());
}
}

View File

@ -40,7 +40,9 @@
</bean>
<!-- jwt service -->
<bean id="jwtService" class="org.apache.nifi.web.security.jwt.JwtService"></bean>
<bean id="jwtService" class="org.apache.nifi.web.security.jwt.JwtService">
<constructor-arg ref="nifiProperties"/>
</bean>
<!-- login identity provider -->
<bean id="loginIdentityProvider" class="org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean">

View File

@ -351,6 +351,28 @@ For details see http://jqueryui.com
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product bundles 'jquery.base64.js' which is available under an MIT style license.
Copyright (c) 2013 Yannick Albert (http://yckart.com/)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product bundles 'SlickGrid v2.2' which is available under an MIT style license.
Copyright (c) 2010 Michael Leibman, http://github.com/mleibman/slickgrid

View File

@ -27,6 +27,7 @@
<link rel="stylesheet" href="js/jquery/qtip2/jquery.qtip.min.css?" type="text/css" />
<link rel="stylesheet" href="js/jquery/ui-smoothness/jquery-ui-1.10.4.min.css" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
<script type="text/javascript" src="js/jquery/jquery.count.js"></script>
<script type="text/javascript" src="js/jquery/jquery.center.js"></script>
<script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script>

View File

@ -45,7 +45,9 @@
<div id="header-links-container">
<ul>
<li id="current-user-container">
<span id="current-user"></span>
<div id="anonymous-user-alert"></div>
<div id="current-user"></div>
<div class="clear"></div>
</li>
<li id="login-link-container">
<span id="login-link" class="link">login</span>

View File

@ -16,7 +16,17 @@
--%>
<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<div id="nifi-registration-container" class="hidden">
<div id="nifi-registration-title" class="login-title">Submit Justification</div>
<div id="nifi-registration-title" class="login-title nifi-submit-justification">Submit Justification</div>
<div id="nifi-user-submit-justification-container" class="nifi-submit-justification">
<div class="setting">
<div class="setting-name">User</div>
<div class="setting-field">
<div id="nifi-user-submit-justification"></div>
<span id="nifi-user-submit-justification-logout" class="link hidden">logout</span>
<div class="clear"></div>
</div>
</div>
</div>
<div class="setting">
<div class="setting-name">Justification</div>
<div class="setting-field">

View File

@ -506,7 +506,17 @@ div.search-glass-pane {
/* styles for the status link */
#anonymous-user-alert {
float: left;
margin-top: -2px;
margin-right: 6px;
width: 18px;
height: 16px;
background-image: url(../images/iconAlert.png);
}
#current-user {
float: left;
margin-right: 8px;
font-weight: bold;
}

View File

@ -73,10 +73,24 @@ body.login-body input, body.login-body textarea {
*/
#nifi-registration-container {
margin-top: 10px;
width: 412px;
}
#nifi-user-submit-justification-container {
margin-bottom: 10px;
}
#nifi-user-submit-justification {
float: left;
font-weight: bold;
}
#nifi-user-submit-justification-logout {
margin-left: 10px;
float: left;
text-decoration: underline;
}
#nifi-registration-justification {
height: 200px;
}

View File

@ -0,0 +1,123 @@
/*!
* jquery.base64.js 0.0.3 - https://github.com/yckart/jquery.base64.js
* Makes Base64 en & -decoding simpler as it is.
*
* Based upon: https://gist.github.com/Yaffle/1284012
*
* Copyright (c) 2012 Yannick Albert (http://yckart.com)
* Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php).
* 2013/02/10
**/
;(function($) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
a256 = '',
r64 = [256],
r256 = [256],
i = 0;
var UTF8 = {
/**
* Encode multi-byte Unicode string into utf-8 multiple single-byte characters
* (BMP / basic multilingual plane only)
*
* Chars in range U+0080 - U+07FF are encoded in 2 chars, U+0800 - U+FFFF in 3 chars
*
* @param {String} strUni Unicode string to be encoded as UTF-8
* @returns {String} encoded string
*/
encode: function(strUni) {
// use regular expressions & String.replace callback function for better efficiency
// than procedural approaches
var strUtf = strUni.replace(/[\u0080-\u07ff]/g, // U+0080 - U+07FF => 2 bytes 110yyyyy, 10zzzzzz
function(c) {
var cc = c.charCodeAt(0);
return String.fromCharCode(0xc0 | cc >> 6, 0x80 | cc & 0x3f);
})
.replace(/[\u0800-\uffff]/g, // U+0800 - U+FFFF => 3 bytes 1110xxxx, 10yyyyyy, 10zzzzzz
function(c) {
var cc = c.charCodeAt(0);
return String.fromCharCode(0xe0 | cc >> 12, 0x80 | cc >> 6 & 0x3F, 0x80 | cc & 0x3f);
});
return strUtf;
},
/**
* Decode utf-8 encoded string back into multi-byte Unicode characters
*
* @param {String} strUtf UTF-8 string to be decoded back to Unicode
* @returns {String} decoded string
*/
decode: function(strUtf) {
// note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char!
var strUni = strUtf.replace(/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars
function(c) { // (note parentheses for precence)
var cc = ((c.charCodeAt(0) & 0x0f) << 12) | ((c.charCodeAt(1) & 0x3f) << 6) | (c.charCodeAt(2) & 0x3f);
return String.fromCharCode(cc);
})
.replace(/[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars
function(c) { // (note parentheses for precence)
var cc = (c.charCodeAt(0) & 0x1f) << 6 | c.charCodeAt(1) & 0x3f;
return String.fromCharCode(cc);
});
return strUni;
}
};
while(i < 256) {
var c = String.fromCharCode(i);
a256 += c;
r256[i] = i;
r64[i] = b64.indexOf(c);
++i;
}
function code(s, discard, alpha, beta, w1, w2) {
s = String(s);
var buffer = 0,
i = 0,
length = s.length,
result = '',
bitsInBuffer = 0;
while(i < length) {
var c = s.charCodeAt(i);
c = c < 256 ? alpha[c] : -1;
buffer = (buffer << w1) + c;
bitsInBuffer += w1;
while(bitsInBuffer >= w2) {
bitsInBuffer -= w2;
var tmp = buffer >> bitsInBuffer;
result += beta.charAt(tmp);
buffer ^= tmp << bitsInBuffer;
}
++i;
}
if(!discard && bitsInBuffer > 0) result += beta.charAt(buffer << (w2 - bitsInBuffer));
return result;
}
var Plugin = $.base64 = function(dir, input, encode) {
return input ? Plugin[dir](input, encode) : dir ? null : this;
};
Plugin.btoa = Plugin.encode = function(plain, utf8encode) {
plain = Plugin.raw === false || Plugin.utf8encode || utf8encode ? UTF8.encode(plain) : plain;
plain = code(plain, false, r256, b64, 8, 6);
return plain + '===='.slice((plain.length % 4) || 4);
};
Plugin.atob = Plugin.decode = function(coded, utf8decode) {
coded = coded.replace(/[^A-Za-z0-9\+\/\=]/g, "");
coded = String(coded).split('=');
var i = coded.length;
do {--i;
coded[i] = code(coded[i], true, r64, a256, 6, 8);
} while (i > 0);
coded = coded.join('');
return Plugin.raw === false || Plugin.utf8decode || utf8decode ? UTF8.decode(coded) : coded;
};
}(jQuery));

View File

@ -64,7 +64,6 @@ nf.Canvas = (function () {
bulletinBoard: '../nifi-api/controller/bulletin-board',
banners: '../nifi-api/controller/banners',
controller: '../nifi-api/controller',
token: '../nifi-api/token',
controllerConfig: '../nifi-api/controller/config',
loginConfig: '../nifi-api/controller/login/config',
cluster: '../nifi-api/cluster',
@ -191,7 +190,7 @@ nf.Canvas = (function () {
if (!refreshContainer.is(':visible')) {
$('#stats-last-refreshed').addClass('alert');
var refreshMessage = "This flow has been modified by '" + revision.lastModifier + "'. Please refresh.";
// update the tooltip
var refreshRequiredIcon = $('#refresh-required-icon');
if (refreshRequiredIcon.data('qtip')) {
@ -201,10 +200,10 @@ nf.Canvas = (function () {
content: refreshMessage
}, nf.CanvasUtils.config.systemTooltipConfig));
}
refreshContainer.show();
}
// insert the refresh needed text in the settings - if necessary
if (!settingsRefreshIcon.is(':visible')) {
$('#settings-last-refreshed').addClass('alert');
@ -336,7 +335,7 @@ nf.Canvas = (function () {
'offset': '100%',
'stop-color': '#ffffff'
});
// define the gradient for the expiration icon
var expirationBackground = defs.append('linearGradient')
.attr({
@ -346,7 +345,7 @@ nf.Canvas = (function () {
'x2': '0%',
'y2': '100%'
});
expirationBackground.append('stop')
.attr({
'offset': '0%',
@ -400,106 +399,106 @@ nf.Canvas = (function () {
// prevent further propagation (to parents and others handlers
// on the same element to prevent zoom behavior)
d3.event.stopImmediatePropagation();
// prevents the browser from changing to a text selection cursor
d3.event.preventDefault();
}
})
.on('mousemove.selection', function () {
// update selection box if shift is held down
if (d3.event.shiftKey) {
// get the selection box
var selectionBox = d3.select('rect.selection');
if (!selectionBox.empty()) {
// get the original position
var originalPosition = selectionBox.datum();
var position = d3.mouse(canvas.node());
var d = {};
if (originalPosition[0] < position[0]) {
d.x = originalPosition[0];
d.width = position[0] - originalPosition[0];
} else {
d.x = position[0];
d.width = originalPosition[0] - position[0];
.on('mousemove.selection', function () {
// update selection box if shift is held down
if (d3.event.shiftKey) {
// get the selection box
var selectionBox = d3.select('rect.selection');
if (!selectionBox.empty()) {
// get the original position
var originalPosition = selectionBox.datum();
var position = d3.mouse(canvas.node());
var d = {};
if (originalPosition[0] < position[0]) {
d.x = originalPosition[0];
d.width = position[0] - originalPosition[0];
} else {
d.x = position[0];
d.width = originalPosition[0] - position[0];
}
if (originalPosition[1] < position[1]) {
d.y = originalPosition[1];
d.height = position[1] - originalPosition[1];
} else {
d.y = position[1];
d.height = originalPosition[1] - position[1];
}
// update the selection box
selectionBox.attr(d);
// prevent further propagation (to parents)
d3.event.stopPropagation();
}
}
})
.on('mouseup.selection', function () {
// ensure this originated from clicking the canvas, not a component.
// when clicking on a component, the event propagation is stopped so
// it never reaches the canvas. we cannot do this however on up events
// since the drag events break down
if (canvasClicked === false) {
return;
}
if (originalPosition[1] < position[1]) {
d.y = originalPosition[1];
d.height = position[1] - originalPosition[1];
} else {
d.y = position[1];
d.height = originalPosition[1] - position[1];
// reset the canvas click flag
canvasClicked = false;
// get the selection box
var selectionBox = d3.select('rect.selection');
if (!selectionBox.empty()) {
var selectionBoundingBox = {
x: parseInt(selectionBox.attr('x'), 10),
y: parseInt(selectionBox.attr('y'), 10),
width: parseInt(selectionBox.attr('width'), 10),
height: parseInt(selectionBox.attr('height'), 10)
};
// see if a component should be selected or not
d3.selectAll('g.component').classed('selected', function (d) {
// consider it selected if its already selected or enclosed in the bounding box
return d3.select(this).classed('selected') ||
d.component.position.x >= selectionBoundingBox.x && (d.component.position.x + d.dimensions.width) <= (selectionBoundingBox.x + selectionBoundingBox.width) &&
d.component.position.y >= selectionBoundingBox.y && (d.component.position.y + d.dimensions.height) <= (selectionBoundingBox.y + selectionBoundingBox.height);
});
// see if a connection should be selected or not
d3.selectAll('g.connection').classed('selected', function (d) {
// consider all points
var points = [d.start].concat(d.bends, [d.end]);
// determine the bounding box
var x = d3.extent(points, function (pt) {
return pt.x;
});
var y = d3.extent(points, function (pt) {
return pt.y;
});
// consider it selected if its already selected or enclosed in the bounding box
return d3.select(this).classed('selected') ||
x[0] >= selectionBoundingBox.x && x[1] <= (selectionBoundingBox.x + selectionBoundingBox.width) &&
y[0] >= selectionBoundingBox.y && y[1] <= (selectionBoundingBox.y + selectionBoundingBox.height);
});
// remove the selection box
selectionBox.remove();
} else if (panning === false) {
// deselect as necessary if we are not panning
nf.CanvasUtils.getSelection().classed('selected', false);
}
// update the selection box
selectionBox.attr(d);
// prevent further propagation (to parents)
d3.event.stopPropagation();
}
}
})
.on('mouseup.selection', function () {
// ensure this originated from clicking the canvas, not a component.
// when clicking on a component, the event propagation is stopped so
// it never reaches the canvas. we cannot do this however on up events
// since the drag events break down
if (canvasClicked === false) {
return;
}
// reset the canvas click flag
canvasClicked = false;
// get the selection box
var selectionBox = d3.select('rect.selection');
if (!selectionBox.empty()) {
var selectionBoundingBox = {
x: parseInt(selectionBox.attr('x'), 10),
y: parseInt(selectionBox.attr('y'), 10),
width: parseInt(selectionBox.attr('width'), 10),
height: parseInt(selectionBox.attr('height'), 10)
};
// see if a component should be selected or not
d3.selectAll('g.component').classed('selected', function (d) {
// consider it selected if its already selected or enclosed in the bounding box
return d3.select(this).classed('selected') ||
d.component.position.x >= selectionBoundingBox.x && (d.component.position.x + d.dimensions.width) <= (selectionBoundingBox.x + selectionBoundingBox.width) &&
d.component.position.y >= selectionBoundingBox.y && (d.component.position.y + d.dimensions.height) <= (selectionBoundingBox.y + selectionBoundingBox.height);
// update the toolbar
nf.CanvasToolbar.refresh();
});
// see if a connection should be selected or not
d3.selectAll('g.connection').classed('selected', function (d) {
// consider all points
var points = [d.start].concat(d.bends, [d.end]);
// determine the bounding box
var x = d3.extent(points, function (pt) {
return pt.x;
});
var y = d3.extent(points, function (pt) {
return pt.y;
});
// consider it selected if its already selected or enclosed in the bounding box
return d3.select(this).classed('selected') ||
x[0] >= selectionBoundingBox.x && x[1] <= (selectionBoundingBox.x + selectionBoundingBox.width) &&
y[0] >= selectionBoundingBox.y && y[1] <= (selectionBoundingBox.y + selectionBoundingBox.height);
});
// remove the selection box
selectionBox.remove();
} else if (panning === false) {
// deselect as necessary if we are not panning
nf.CanvasUtils.getSelection().classed('selected', false);
}
// update the toolbar
nf.CanvasToolbar.refresh();
});
// define a function for update the graph dimensions
var updateGraphSize = function () {
// get the location of the bottom of the graph
@ -513,7 +512,7 @@ nf.Canvas = (function () {
var top = parseInt(canvasContainer.css('top'), 10);
var windowHeight = $(window).height();
var canvasHeight = (windowHeight - (bottom + top));
// canvas/svg
canvasContainer.css({
'height': canvasHeight + 'px',
@ -539,7 +538,7 @@ nf.Canvas = (function () {
}
}).on('keydown', function (evt) {
var isCtrl = evt.ctrlKey || evt.metaKey;
// consider escape, before checking dialogs
if (!isCtrl && evt.keyCode === 27) {
// esc
@ -555,7 +554,7 @@ nf.Canvas = (function () {
// first consider read only property detail dialog
if ($('div.property-detail').is(':visible')) {
nf.Common.removeAllPropertyDetailDialogs();
// prevent further bubbling as we're already handled it
evt.stopPropagation();
evt.preventDefault();
@ -573,7 +572,7 @@ nf.Canvas = (function () {
var dialogMax = null;
// identify the top most cancellable
$.each(cancellables, function(_, cancellable) {
$.each(cancellables, function (_, cancellable) {
var dialog = $(cancellable);
var zIndex = dialog.css('zIndex');
@ -618,10 +617,10 @@ nf.Canvas = (function () {
}
}
}
return;
}
// if a dialog is open, disable canvas shortcuts
if ($('.dialog').is(':visible')) {
return;
@ -836,7 +835,7 @@ nf.Canvas = (function () {
bulletinIcon.show();
}
}
// update controller service and reporting task bulletins
nf.Settings.setBulletins(controllerStatus.controllerServiceBulletins, controllerStatus.reportingTaskBulletins);
@ -937,22 +936,18 @@ nf.Canvas = (function () {
};
return {
ANONYMOUS_USER_TEXT: 'Anonymous user',
CANVAS_OFFSET: 0,
/**
* Determines if the current broswer supports SVG.
*/
SUPPORTS_SVG: !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect,
/**
* Hides the splash that is displayed while the application is loading.
*/
hideSplash: function () {
$('#splash').fadeOut();
},
/**
* Stop polling for revision.
*/
@ -960,7 +955,6 @@ nf.Canvas = (function () {
// set polling flag
revisionPolling = false;
},
/**
* Remove the status poller.
*/
@ -968,7 +962,6 @@ nf.Canvas = (function () {
// set polling flag
statusPolling = false;
},
/**
* Reloads the flow from the server based on the currently specified group id.
* To load another group, update nf.Canvas.setGroupId and call nf.Canvas.reload.
@ -977,7 +970,7 @@ nf.Canvas = (function () {
return $.Deferred(function (deferred) {
// hide the context menu
nf.ContextMenu.hide();
// get the process group to refresh everything
var processGroupXhr = reloadProcessGroup(nf.Canvas.getGroupId());
var statusXhr = reloadFlowStatus();
@ -1011,7 +1004,6 @@ nf.Canvas = (function () {
});
}).promise();
},
/**
* Reloads the status.
*/
@ -1025,7 +1017,6 @@ nf.Canvas = (function () {
});
}).promise();
},
/**
* Initialize NiFi.
*/
@ -1036,16 +1027,16 @@ nf.Canvas = (function () {
url: config.urls.identity,
dataType: 'json'
});
// get the current user's authorities
var authoritiesXhr = $.ajax({
type: 'GET',
url: config.urls.authorities,
dataType: 'json'
});
// load the identity and authorities for the current user
var userXhr = $.Deferred(function(deferred) {
var userXhr = $.Deferred(function (deferred) {
$.when(authoritiesXhr, identityXhr).done(function (authoritiesResult, identityResult) {
var authoritiesResponse = authoritiesResult[0];
var identityResponse = identityResult[0];
@ -1057,6 +1048,7 @@ nf.Canvas = (function () {
// if the user is logged, we want to determine if they were logged in using a certificate
if (identityResponse.identity !== 'anonymous') {
// rendner the users name
$('#current-user').text(identityResponse.identity).show();
// render the logout button if there is a token locally
@ -1064,6 +1056,16 @@ nf.Canvas = (function () {
$('#logout-link-container').show();
}
} else {
// alert user's of anonymous access
$('#anonymous-user-alert').show().qtip($.extend({}, nf.Common.config.tooltipConfig, {
content: 'You are accessing with limited authority. Log in or request an account to access with additional authority granted to you by an administrator.',
position: {
my: 'top right',
at: 'bottom left'
}
}));
// render the anonymous user text
$('#current-user').text(nf.Canvas.ANONYMOUS_USER_TEXT).show();
}
deferred.resolve();
@ -1076,7 +1078,7 @@ nf.Canvas = (function () {
}
});
}).promise();
userXhr.done(function () {
// get the controller config to register the status poller
var configXhr = $.ajax({
@ -1109,7 +1111,7 @@ nf.Canvas = (function () {
}
});
}).promise();
// ensure the config requests are loaded
$.when(configXhr, loginXhr, userXhr).done(function (configResult, loginResult) {
var configResponse = configResult[0];
@ -1193,7 +1195,6 @@ nf.Canvas = (function () {
}).fail(nf.Common.handleAjaxError);
}).fail(nf.Common.handleAjaxError);
},
/**
* Defines the gradient colors used to render processors.
*
@ -1202,7 +1203,6 @@ nf.Canvas = (function () {
defineProcessorColors: function (colors) {
setColors(colors, 'processor');
},
/**
* Defines the gradient colors used to render label.
*
@ -1211,7 +1211,6 @@ nf.Canvas = (function () {
defineLabelColors: function (colors) {
setColors(colors, 'label');
},
/**
* Return whether this instance of NiFi is clustered.
*
@ -1220,14 +1219,12 @@ nf.Canvas = (function () {
isClustered: function () {
return clustered === true;
},
/**
* Returns whether site to site communications is secure.
*/
isSecureSiteToSite: function () {
return secureSiteToSite;
},
/**
* Set the group id.
*
@ -1236,14 +1233,12 @@ nf.Canvas = (function () {
setGroupId: function (gi) {
groupId = gi;
},
/**
* Get the group id.
*/
getGroupId: function () {
return groupId;
},
/**
* Set the group name.
*
@ -1252,14 +1247,12 @@ nf.Canvas = (function () {
setGroupName: function (gn) {
groupName = gn;
},
/**
* Get the group name.
*/
getGroupName: function () {
return groupName;
},
/**
* Set the parent group id.
*
@ -1268,16 +1261,14 @@ nf.Canvas = (function () {
setParentGroupId: function (pgi) {
parentGroupId = pgi;
},
/**
* Get the parent group id.
*/
getParentGroupId: function () {
return parentGroupId;
},
View: (function () {
/**
* Updates component visibility based on their proximity to the screen's viewport.
*/
@ -1344,8 +1335,8 @@ nf.Canvas = (function () {
.classed('entering', function () {
return visible && !wasVisible;
}).classed('leaving', function () {
return !visible && wasVisible;
});
return !visible && wasVisible;
});
};
// get the all components
@ -1432,7 +1423,6 @@ nf.Canvas = (function () {
// add the behavior to the canvas and disable dbl click zoom
svg.call(behavior).on('dblclick.zoom', null);
},
/**
* Whether or not a component should be rendered based solely on the current scale.
*
@ -1441,7 +1431,6 @@ nf.Canvas = (function () {
shouldRenderPerScale: function () {
return nf.Canvas.View.scale() >= MIN_SCALE_TO_RENDER;
},
/**
* Updates component visibility based on the current translation/scale.
*/
@ -1449,7 +1438,6 @@ nf.Canvas = (function () {
updateComponentVisibility();
nf.Graph.pan();
},
/**
* Sets/gets the current translation.
*
@ -1462,7 +1450,6 @@ nf.Canvas = (function () {
behavior.translate(translate);
}
},
/**
* Sets/gets the current scale.
*
@ -1475,7 +1462,6 @@ nf.Canvas = (function () {
behavior.scale(scale);
}
},
/**
* Zooms in a single zoom increment.
*/
@ -1500,7 +1486,6 @@ nf.Canvas = (function () {
height: 1
});
},
/**
* Zooms out a single zoom increment.
*/
@ -1525,7 +1510,6 @@ nf.Canvas = (function () {
height: 1
});
},
/**
* Zooms to fit the entire graph on the canvas.
*/
@ -1572,7 +1556,6 @@ nf.Canvas = (function () {
height: canvasHeight / newScale
});
},
/**
* Zooms to the actual size (1 to 1).
*/
@ -1621,7 +1604,6 @@ nf.Canvas = (function () {
// center as appropriate
nf.CanvasUtils.centerBoundingBox(box);
},
/**
* Refreshes the view based on the configured translation and scale.
*

View File

@ -75,7 +75,7 @@ nf.Login = (function () {
var showUserRegistration = function () {
showNiFiRegistration();
$('#nifi-registration-title').hide();
$('div.nifi-submit-justification').hide();
$('#user-registration-container').show();
$('#login-submission-button').text('Create');
};
@ -109,41 +109,15 @@ nf.Login = (function () {
'username': $('#username').val(),
'password': $('#password').val()
}
}).done(function (response, status, xhr) {
var authorization = xhr.getResponseHeader('Authorization');
var badToken = false;
}).done(function (jwt) {
// store the jwt and reload the page
nf.Storage.setItem('jwt', jwt);
// ensure there was a token in the response
if (authorization) {
var tokens = authorization.split(/ /);
// ensure the token is the appropriate length
if (tokens.length === 2) {
// store the jwt and reload the page
nf.Storage.setItem('jwt', tokens[1]);
// reload as appropriate
if (top !== window) {
parent.window.location = '/nifi';
} else {
window.location = '/nifi';
}
return;
} else {
badToken = true;
}
// reload as appropriate
if (top !== window) {
parent.window.location = '/nifi';
} else {
badToken = true;
}
if (badToken === true) {
$('#login-message-title').text('An unexpected error has occurred');
$('#login-message').text('The user token could not be parsed.');
// update visibility
$('#login-container').hide();
$('#login-submission-container').hide();
$('#login-message-container').show();
window.location = '/nifi';
}
}).fail(function (xhr, status, error) {
if (xhr.status === 400) {
@ -164,13 +138,25 @@ nf.Login = (function () {
};
var createUserAccount = function () {
var password = $('#registration-password').val();
var passwordConfirmation = $('#registration-password-confirmation').val();
// ensure the password matches
if (password !== passwordConfirmation) {
nf.Dialog.showOkDialog({
dialogContent: 'The specified passwords do not match.',
overlayBackground: false
});
return;
}
// attempt to create the user account registration
$.ajax({
type: 'POST',
url: config.urls.registration,
data: {
'username': $('#registration-username').val(),
'password': $('#registration-password').val(),
'password': password,
'justification': $('#nifi-registration-justification').val()
}
}).done(function (response, status, xhr) {
@ -220,6 +206,33 @@ nf.Login = (function () {
});
};
/**
* Extracts the subject from the specified jwt. If the jwt is not as expected
* an empty string is returned.
*
* @param {string} jwt
* @returns {string}
*/
var getJwtSubject = function (jwt) {
if (nf.Common.isDefinedAndNotNull(jwt)) {
var segments = jwt.split(/\./);
if (segments.length !== 3) {
return '';
}
var rawPayload = $.base64.atob(segments[1]);
var payload = JSON.parse(rawPayload);
if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) {
return payload['preferred_username'];
} else {
'';
}
}
return '';
};
return {
/**
* Initializes the login page.
@ -231,6 +244,16 @@ nf.Login = (function () {
var needsLogin = false;
var needsNiFiRegistration = false;
var logout = function () {
nf.Storage.removeItem('jwt');
};
// handle logout
$('#nifi-user-submit-justification-logout').on('click', function () {
logout();
window.location = '/nifi/login';
});
var token = $.ajax({
type: 'GET',
url: config.urls.token
@ -250,7 +273,8 @@ nf.Login = (function () {
isAnonymous = true;
// request a token without including credentials, if successful then the user is using a certificate
token.done(function () {
token.done(function (jwt) {
// the user is using a certificate/token, see if their account is active/pending/revoked/etc
$.ajax({
type: 'GET',
@ -263,6 +287,16 @@ nf.Login = (function () {
$('#login-message').text('Your account is active and you are already logged in.');
}).fail(function (xhr, status, error) {
if (xhr.status === 401) {
var user = getJwtSubject(jwt);
// show the user
$('#nifi-user-submit-justification').text(user);
// render the logout button if there is a token locally
if (nf.Storage.getItem('jwt') !== null) {
$('#nifi-user-submit-justification-logout').show();
}
// anonymous user and 401 means they need nifi registration
needsNiFiRegistration = true;
} else {
@ -279,7 +313,12 @@ nf.Login = (function () {
}).always(function () {
deferred.resolve();
});
}).fail(function () {
}).fail(function (tokenXhr) {
if (tokenXhr.status === 400) {
// no credentials supplied so 400 must be due to an invalid/expired token
logout();
}
// no token granted, user has no certificate and needs to login with their credentials
needsLogin = true;
deferred.resolve();
@ -296,10 +335,25 @@ nf.Login = (function () {
// unable to get identity (and no anonymous user) see if we can offer login
if (xhr.status === 401) {
// attempt to get a token for the current user without passing login credentials
token.done(function () {
token.done(function (jwt) {
var user = getJwtSubject(jwt);
// show the user
$('#nifi-user-submit-justification').text(user);
// render the logout button if there is a token locally
if (nf.Storage.getItem('jwt') !== null) {
$('#nifi-user-submit-justification-logout').show();
}
// 401 from identity request and 200 from token means they have a certificate/token but have not yet requested an account
needsNiFiRegistration = true;
}).fail(function () {
}).fail(function (tokenXhr) {
if (tokenXhr.status === 400) {
// no credentials supplied so 400 must be due to an invalid/expired token
logout();
}
// no token granted, user needs to login with their credentials
needsLogin = true;
}).always(function () {

View File

@ -206,7 +206,14 @@ nf.Common = {
// if an error occurs while the splash screen is visible close the canvas show the error message
if ($('#splash').is(':visible')) {
$('#message-title').text('An unexpected error has occurred');
if (xhr.status === 401) {
$('#message-title').text('Unauthorized');
} else if (xhr.status === 403) {
$('#message-title').text('Access Denied');
} else {
$('#message-title').text('An unexpected error has occurred');
}
if ($.trim(xhr.responseText) === '') {
$('#message-content').text('Please check the logs.');
} else {
@ -249,7 +256,7 @@ nf.Common = {
$('#message-content').text(xhr.responseText);
}
} else if (xhr.status === 403) {
$('#message-title').text('Forbidden');
$('#message-title').text('Access Denied');
if ($.trim(xhr.responseText) === '') {
$('#message-content').text('Unable to authorize you to use this NiFi.');
} else {