NIFI-655:

- Refactoring key service to expose the key id.
- Handling client side expiration better.
- Removing specialized active directory provider and abstract ldap provider.
This commit is contained in:
Matt Gilman 2015-11-18 14:01:45 -05:00
parent 7d04dfeac0
commit c94d0271d9
28 changed files with 599 additions and 420 deletions

View File

@ -87,30 +87,21 @@ public final class CertificateUtils {
*/ */
public static String extractUsername(String dn) { public static String extractUsername(String dn) {
String username = dn; String username = dn;
String cn = "";
// ensure the dn is specified // ensure the dn is specified
if (StringUtils.isNotBlank(dn)) { if (StringUtils.isNotBlank(dn)) {
// determine the separate
final String separator = StringUtils.indexOfIgnoreCase(dn, "/cn=") > 0 ? "/" : ",";
// attempt to locate the cn // attempt to locate the cd
if (dn.startsWith("CN=")) { final String cnPattern = "cn=";
cn = StringUtils.substringBetween(dn, "CN=", ","); final int cnIndex = StringUtils.indexOfIgnoreCase(dn, cnPattern);
} else if (dn.startsWith("/CN=")) { if (cnIndex >= 0) {
cn = StringUtils.substringBetween(dn, "CN=", "/"); int separatorIndex = StringUtils.indexOf(dn, separator, cnIndex);
} else if (dn.startsWith("C=") || dn.startsWith("/C=")) { if (separatorIndex > 0) {
cn = StringUtils.substringAfter(dn, "CN="); username = StringUtils.substring(dn, cnIndex + cnPattern.length(), separatorIndex);
} else if (dn.startsWith("/") && StringUtils.contains(dn, "CN=")) {
cn = StringUtils.substringAfter(dn, "CN=");
}
// attempt to get the username from the cn
if (StringUtils.isNotBlank(cn)) {
if (cn.endsWith(")")) {
username = StringUtils.substringBetween(cn, "(", ")");
} else if (cn.contains(" ")) {
username = StringUtils.substringAfterLast(cn, " ");
} else { } else {
username = cn; username = StringUtils.substring(dn, cnIndex + cnPattern.length());
} }
} }
} }

View File

@ -16,18 +16,28 @@
*/ */
package org.apache.nifi.admin.dao; package org.apache.nifi.admin.dao;
import org.apache.nifi.key.Key;
/** /**
* Key data access. * Key data access.
*/ */
public interface KeyDAO { public interface KeyDAO {
/** /**
* Gets the key for the specified user identity. Returns null if no key exists for the user identity. * Gets the key for the specified user identity. Returns null if no key exists for the key id.
* *
* @param identity The user identity * @param id The key id
* @return The key or null * @return The key or null
*/ */
String getKey(String identity); Key findKeyById(int id);
/**
* Gets the latest key for the specified identity. Returns null if no key exists for the user identity.
*
* @param identity The identity
* @return The key or null
*/
Key findLatestKeyByIdentity(String identity);
/** /**
* Creates a key for the specified user identity. * Creates a key for the specified user identity.
@ -35,5 +45,5 @@ public interface KeyDAO {
* @param identity The user identity * @param identity The user identity
* @return The key * @return The key
*/ */
String createKey(String identity); Key createKey(String identity);
} }

View File

@ -20,17 +20,23 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement;
import java.util.UUID; import java.util.UUID;
import org.apache.nifi.admin.RepositoryUtils; import org.apache.nifi.admin.RepositoryUtils;
import org.apache.nifi.admin.dao.DataAccessException; import org.apache.nifi.admin.dao.DataAccessException;
import org.apache.nifi.admin.dao.KeyDAO; import org.apache.nifi.admin.dao.KeyDAO;
import org.apache.nifi.key.Key;
/** /**
* *
*/ */
public class StandardKeyDAO implements KeyDAO { public class StandardKeyDAO implements KeyDAO {
private static final String SELECT_KEY_FOR_USER = "SELECT KEY " private static final String SELECT_KEY_FOR_USER_BY_ID = "SELECT ID, IDENTITY, KEY "
+ "FROM KEY "
+ "WHERE ID = ?";
private static final String SELECT_KEY_FOR_USER_BY_IDENTITY = "SELECT ID, IDENTITY, KEY "
+ "FROM KEY " + "FROM KEY "
+ "WHERE IDENTITY = ?"; + "WHERE IDENTITY = ?";
@ -47,26 +53,25 @@ public class StandardKeyDAO implements KeyDAO {
} }
@Override @Override
public String getKey(String identity) { public Key findKeyById(int id) {
if (identity == null) { Key key = null;
throw new IllegalArgumentException("Specified identity cannot be null.");
}
String key = null;
PreparedStatement statement = null; PreparedStatement statement = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
// add each authority for the specified user // add each authority for the specified user
statement = connection.prepareStatement(SELECT_KEY_FOR_USER); statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_ID);
statement.setString(1, identity); statement.setInt(1, id);
// execute the query // execute the query
rs = statement.executeQuery(); rs = statement.executeQuery();
// if the key was found, add it // if the key was found, add it
if (rs.next()) { if (rs.next()) {
key = rs.getString("KEY"); key = new Key();
key.setId(rs.getInt("ID"));
key.setIdentity(rs.getString("IDENTITY"));
key.setKey(rs.getString("KEY"));
} }
} catch (SQLException sqle) { } catch (SQLException sqle) {
throw new DataAccessException(sqle); throw new DataAccessException(sqle);
@ -79,20 +84,62 @@ public class StandardKeyDAO implements KeyDAO {
} }
@Override @Override
public String createKey(final String identity) { public Key findLatestKeyByIdentity(String identity) {
if (identity == null) {
throw new IllegalArgumentException("Specified identity cannot be null.");
}
Key key = null;
PreparedStatement statement = null; PreparedStatement statement = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
final String key = UUID.randomUUID().toString(); // add each authority for the specified user
statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_IDENTITY);
statement.setString(1, identity);
// execute the query
rs = statement.executeQuery();
// if the key was found, add it
if (rs.next()) {
key = new Key();
key.setId(rs.getInt("ID"));
key.setIdentity(rs.getString("IDENTITY"));
key.setKey(rs.getString("KEY"));
}
} catch (SQLException sqle) {
throw new DataAccessException(sqle);
} finally {
RepositoryUtils.closeQuietly(rs);
RepositoryUtils.closeQuietly(statement);
}
return key;
}
@Override
public Key createKey(final String identity) {
PreparedStatement statement = null;
ResultSet rs = null;
try {
final String keyValue = UUID.randomUUID().toString();
// add each authority for the specified user // add each authority for the specified user
statement = connection.prepareStatement(INSERT_KEY); statement = connection.prepareStatement(INSERT_KEY, Statement.RETURN_GENERATED_KEYS);
statement.setString(1, identity); statement.setString(1, identity);
statement.setString(2, key); statement.setString(2, keyValue);
// insert the key // insert the key
int updateCount = statement.executeUpdate(); int updateCount = statement.executeUpdate();
if (updateCount == 1) { rs = statement.getGeneratedKeys();
// verify the results
if (updateCount == 1 && rs.next()) {
final Key key = new Key();
key.setId(rs.getInt(1));
key.setIdentity(identity);
key.setKey(keyValue);
return key; return key;
} else { } else {
throw new DataAccessException("Unable to add key for user."); throw new DataAccessException("Unable to add key for user.");

View File

@ -16,6 +16,8 @@
*/ */
package org.apache.nifi.admin.service; package org.apache.nifi.admin.service;
import org.apache.nifi.key.Key;
/** /**
* Supports retrieving and issues keys for signing user tokens. * Supports retrieving and issues keys for signing user tokens.
*/ */
@ -24,10 +26,10 @@ public interface KeyService {
/** /**
* Gets a key for the specified user identity. Returns null if the user has not had a key issued * Gets a key for the specified user identity. Returns null if the user has not had a key issued
* *
* @param identity The user identity * @param id The key id
* @return The key or null * @return The key or null
*/ */
String getKey(String identity); Key getKey(int id);
/** /**
* Gets a key for the specified user identity. If a key does not exist, one will be created. * Gets a key for the specified user identity. If a key does not exist, one will be created.
@ -36,5 +38,5 @@ public interface KeyService {
* @return The key * @return The key
* @throws AdministrationException if it failed to get/create the key * @throws AdministrationException if it failed to get/create the key
*/ */
String getOrCreateKey(String identity); Key getOrCreateKey(String identity);
} }

View File

@ -0,0 +1,42 @@
/*
* 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.admin.service.action;
import org.apache.nifi.admin.dao.DAOFactory;
import org.apache.nifi.authorization.AuthorityProvider;
import org.apache.nifi.admin.dao.KeyDAO;
import org.apache.nifi.key.Key;
/**
* Gets a key for the specified key id.
*/
public class GetKeyByIdAction implements AdministrationAction<Key> {
private final int id;
public GetKeyByIdAction(int id) {
this.id = id;
}
@Override
public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
final KeyDAO keyDao = daoFactory.getKeyDAO();
return keyDao.findKeyById(id);
}
}

View File

@ -20,22 +20,23 @@ import org.apache.nifi.admin.dao.DAOFactory;
import org.apache.nifi.authorization.AuthorityProvider; import org.apache.nifi.authorization.AuthorityProvider;
import org.apache.nifi.admin.dao.KeyDAO; import org.apache.nifi.admin.dao.KeyDAO;
import org.apache.nifi.key.Key;
/** /**
* Gets a key for the specified user identity. * Gets a key for the specified key id.
*/ */
public class GetKeyAction implements AdministrationAction<String> { public class GetKeyByIdentityAction implements AdministrationAction<Key> {
private final String identity; private final String identity;
public GetKeyAction(String identity) { public GetKeyByIdentityAction(String identity) {
this.identity = identity; this.identity = identity;
} }
@Override @Override
public String execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) { public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
final KeyDAO keyDao = daoFactory.getKeyDAO(); final KeyDAO keyDao = daoFactory.getKeyDAO();
return keyDao.getKey(identity); return keyDao.findLatestKeyByIdentity(identity);
} }
} }

View File

@ -20,11 +20,12 @@ import org.apache.nifi.admin.dao.DAOFactory;
import org.apache.nifi.authorization.AuthorityProvider; import org.apache.nifi.authorization.AuthorityProvider;
import org.apache.nifi.admin.dao.KeyDAO; import org.apache.nifi.admin.dao.KeyDAO;
import org.apache.nifi.key.Key;
/** /**
* Gets a key for the specified user identity. * Gets a key for the specified user identity.
*/ */
public class GetOrCreateKeyAction implements AdministrationAction<String> { public class GetOrCreateKeyAction implements AdministrationAction<Key> {
private final String identity; private final String identity;
@ -33,10 +34,10 @@ public class GetOrCreateKeyAction implements AdministrationAction<String> {
} }
@Override @Override
public String execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) { public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
final KeyDAO keyDao = daoFactory.getKeyDAO(); final KeyDAO keyDao = daoFactory.getKeyDAO();
String key = keyDao.getKey(identity); Key key = keyDao.findLatestKeyByIdentity(identity);
if (key == null) { if (key == null) {
key = keyDao.createKey(identity); key = keyDao.createKey(identity);
} }

View File

@ -19,7 +19,7 @@ package org.apache.nifi.admin.service.impl;
import org.apache.nifi.admin.dao.DataAccessException; import org.apache.nifi.admin.dao.DataAccessException;
import org.apache.nifi.admin.service.AdministrationException; import org.apache.nifi.admin.service.AdministrationException;
import org.apache.nifi.admin.service.KeyService; import org.apache.nifi.admin.service.KeyService;
import org.apache.nifi.admin.service.action.GetKeyAction; import org.apache.nifi.admin.service.action.GetKeyByIdAction;
import org.apache.nifi.admin.service.action.GetOrCreateKeyAction; import org.apache.nifi.admin.service.action.GetOrCreateKeyAction;
import org.apache.nifi.admin.service.transaction.Transaction; import org.apache.nifi.admin.service.transaction.Transaction;
import org.apache.nifi.admin.service.transaction.TransactionBuilder; import org.apache.nifi.admin.service.transaction.TransactionBuilder;
@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.nifi.key.Key;
/** /**
* *
@ -44,19 +45,17 @@ public class StandardKeyService implements KeyService {
private TransactionBuilder transactionBuilder; private TransactionBuilder transactionBuilder;
@Override @Override
public String getKey(String identity) { public Key getKey(int id) {
// TODO: Change this service to look up by "key ID" instead of identity
// TODO: Change the return type to a Key POJO to support key rotation
Transaction transaction = null; Transaction transaction = null;
String key = null; Key key = null;
readLock.lock(); readLock.lock();
try { try {
// start the transaction // start the transaction
transaction = transactionBuilder.start(); transaction = transactionBuilder.start();
// seed the accounts // get the key
GetKeyAction addActions = new GetKeyAction(identity); GetKeyByIdAction addActions = new GetKeyByIdAction(id);
key = transaction.execute(addActions); key = transaction.execute(addActions);
// commit the transaction // commit the transaction
@ -76,11 +75,9 @@ public class StandardKeyService implements KeyService {
} }
@Override @Override
public String getOrCreateKey(String identity) { public Key getOrCreateKey(String identity) {
// TODO: Change this service to look up by "key ID" instead of identity
// TODO: Change the return type to a Key POJO to support key rotation
Transaction transaction = null; Transaction transaction = null;
String key = null; Key key = null;
writeLock.lock(); writeLock.lock();
try { try {

View File

@ -0,0 +1,69 @@
/*
* 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.key;
import java.io.Serializable;
/**
* An signing key for a NiFi user.
*/
public class Key implements Serializable {
private int id;
private String identity;
private String key;
/**
* The key id.
*
* @return the id
*/
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
/**
* The identity of the user this key is associated with.
*
* @return the identity
*/
public String getIdentity() {
return identity;
}
public void setIdentity(String identity) {
this.identity = identity;
}
/**
* The signing key.
*
* @return the signing key
*/
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}

View File

@ -55,6 +55,7 @@ import org.apache.nifi.web.api.dto.RevisionDTO;
import org.apache.nifi.web.api.entity.AccessStatusEntity; import org.apache.nifi.web.api.entity.AccessStatusEntity;
import org.apache.nifi.web.api.entity.AccessConfigurationEntity; import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
import org.apache.nifi.web.api.request.ClientIdParameter; import org.apache.nifi.web.api.request.ClientIdParameter;
import org.apache.nifi.web.security.InvalidAuthenticationException;
import org.apache.nifi.web.security.ProxiedEntitiesUtils; 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.JwtService; import org.apache.nifi.web.security.jwt.JwtService;
@ -185,16 +186,11 @@ public class AccessResource extends ApplicationResource {
accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name()); accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
accessStatus.setMessage("No credentials supplied, unknown user."); accessStatus.setMessage("No credentials supplied, unknown user.");
} else { } else {
try {
// Extract the Base64 encoded token from the Authorization header // Extract the Base64 encoded token from the Authorization header
final String token = StringUtils.substringAfterLast(authorization, " "); final String token = StringUtils.substringAfterLast(authorization, " ");
try {
final String principal = jwtService.getAuthenticationFromToken(token); final String principal = jwtService.getAuthenticationFromToken(token);
// ensure we have something we can work with (certificate or credentials)
if (principal == null) {
throw new IllegalArgumentException("The specific token is not valid.");
} else {
// set the user identity // set the user identity
accessStatus.setIdentity(principal); accessStatus.setIdentity(principal);
accessStatus.setUsername(CertificateUtils.extractUsername(principal)); accessStatus.setUsername(CertificateUtils.extractUsername(principal));
@ -208,13 +204,12 @@ public class AccessResource extends ApplicationResource {
// no issues with authorization // no issues with authorization
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name()); accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setMessage("Account is active and authorized"); accessStatus.setMessage("Account is active and authorized");
}
} catch (JwtException e) { } catch (JwtException e) {
// TODO: Handle the exception from a failed JWT verification throw new InvalidAuthenticationException(e.getMessage(), e);
throw new AccessDeniedException("The JWT could not be verified", e);
} }
} }
} else { } else {
try {
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates); final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
// get the proxy chain and ensure its populated // get the proxy chain and ensure its populated
@ -224,16 +219,19 @@ public class AccessResource extends ApplicationResource {
throw new IllegalArgumentException("Unable to determine the user from the incoming request."); throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
} }
// ensure the proxy chain is authorized
checkAuthorization(proxyChain);
// set the user identity // set the user identity
accessStatus.setIdentity(proxyChain.get(0)); accessStatus.setIdentity(proxyChain.get(0));
accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0))); accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0)));
// ensure the proxy chain is authorized
checkAuthorization(proxyChain);
// no issues with authorization // no issues with authorization
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name()); accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
accessStatus.setMessage("Account is active and authorized"); accessStatus.setMessage("Account is active and authorized");
} catch (final IllegalArgumentException iae) {
throw new InvalidAuthenticationException(iae.getMessage(), iae);
}
} }
} catch (final UsernameNotFoundException unfe) { } catch (final UsernameNotFoundException unfe) {
accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name()); accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name());

View File

@ -0,0 +1,44 @@
/*
* 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.api.config;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.web.security.InvalidAuthenticationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Maps access denied exceptions into a client response.
*/
@Provider
public class InvalidAuthenticationExceptionMapper implements ExceptionMapper<InvalidAuthenticationException> {
private static final Logger logger = LoggerFactory.getLogger(InvalidAuthenticationExceptionMapper.class);
@Override
public Response toResponse(InvalidAuthenticationException exception) {
if (logger.isDebugEnabled()) {
logger.debug(StringUtils.EMPTY, exception);
}
return Response.status(Response.Status.UNAUTHORIZED).entity(exception.getMessage()).type("text/plain").build();
}
}

View File

@ -255,6 +255,7 @@
<!-- exception mapping --> <!-- exception mapping -->
<bean class="org.apache.nifi.web.api.config.AccessDeniedExceptionMapper" scope="singleton"/> <bean class="org.apache.nifi.web.api.config.AccessDeniedExceptionMapper" scope="singleton"/>
<bean class="org.apache.nifi.web.api.config.InvalidAuthenticationExceptionMapper" scope="singleton"/>
<bean class="org.apache.nifi.web.api.config.AuthenticationCredentialsNotFoundExceptionMapper" scope="singleton"/> <bean class="org.apache.nifi.web.api.config.AuthenticationCredentialsNotFoundExceptionMapper" scope="singleton"/>
<bean class="org.apache.nifi.web.api.config.AccountNotFoundExceptionMapper" scope="singleton"/> <bean class="org.apache.nifi.web.api.config.AccountNotFoundExceptionMapper" scope="singleton"/>
<bean class="org.apache.nifi.web.api.config.AdministrationExceptionMapper" scope="singleton"/> <bean class="org.apache.nifi.web.api.config.AdministrationExceptionMapper" scope="singleton"/>

View File

@ -0,0 +1,35 @@
/*
* 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;
import org.springframework.security.core.AuthenticationException;
/**
* Thrown if the authentication of a given request is invalid. For instance,
* an expired certificate or token.
*/
public class InvalidAuthenticationException extends AuthenticationException {
public InvalidAuthenticationException(String msg) {
super(msg);
}
public InvalidAuthenticationException(String msg, Throwable t) {
super(msg, t);
}
}

View File

@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AccountStatusException; import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@ -134,8 +133,8 @@ public abstract class NiFiAuthenticationFilter implements Filter {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.println("Access is denied."); out.println("Access is denied.");
} }
} else if (ae instanceof BadCredentialsException) { } else if (ae instanceof InvalidAuthenticationException) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
out.println(ae.getMessage()); out.println(ae.getMessage());
} else if (ae instanceof AccountStatusException) { } else if (ae instanceof AccountStatusException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setStatus(HttpServletResponse.SC_FORBIDDEN);

View File

@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.Arrays; import java.util.Arrays;
import org.apache.nifi.web.security.InvalidAuthenticationException;
/** /**
*/ */
@ -49,7 +50,6 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
// TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource // TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource
// get the principal out of the user token // get the principal out of the user token
// look for an authorization token
final String authorization = request.getHeader(AUTHORIZATION); final String authorization = request.getHeader(AUTHORIZATION);
// if there is no authorization header, we don't know the user // if there is no authorization header, we don't know the user
@ -61,9 +61,6 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
try { try {
final String jwtPrincipal = jwtService.getAuthenticationFromToken(token); final String jwtPrincipal = jwtService.getAuthenticationFromToken(token);
if (jwtPrincipal == null) {
return null;
}
if (isNewAccountRequest(request)) { if (isNewAccountRequest(request)) {
return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request))); return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
@ -71,9 +68,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
return new NiFiAuthenticationRequestToken(Arrays.asList(jwtPrincipal)); return new NiFiAuthenticationRequestToken(Arrays.asList(jwtPrincipal));
} }
} catch (JwtException e) { } catch (JwtException e) {
// TODO: Is this the correct way to handle an unverified token? throw new InvalidAuthenticationException(e.getMessage(), e);
logger.error("Could not verify JWT", e);
return null;
} }
} }
} }

View File

@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Calendar; import java.util.Calendar;
import org.apache.nifi.key.Key;
/** /**
* *
@ -88,17 +89,16 @@ public class JwtService {
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
final String identity = claims.getSubject(); final String identity = claims.getSubject();
// TODO: Currently the kid field is identical to identity, but will be a unique key ID when key rotation is implemented // Get the key based on the key id in the claims
final String keyId = claims.get(KEY_ID_CLAIM, String.class); final Integer keyId = claims.get(KEY_ID_CLAIM, Integer.class);
// The key is unique per identity and should be retrieved from the key service final Key key = keyService.getKey(keyId);
final String key = keyService.getKey(keyId);
// Ensure we were able to find a key that was previously issued by this key service for this user // Ensure we were able to find a key that was previously issued by this key service for this user
if (key == null) { if (key == null || key.getKey() == null) {
throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]"); throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]");
} }
return key.getBytes(StandardCharsets.UTF_8); return key.getKey().getBytes(StandardCharsets.UTF_8);
} }
}).parseClaimsJws(base64EncodedToken); }).parseClaimsJws(base64EncodedToken);
} catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) { } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
@ -137,21 +137,19 @@ public class JwtService {
try { try {
// Get/create the key for this user // Get/create the key for this user
final String key = keyService.getOrCreateKey(identity); final Key key = keyService.getOrCreateKey(identity);
final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8);
logger.trace("Generating JWT for " + authenticationToken); logger.trace("Generating JWT for " + authenticationToken);
// TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
// TODO: Change kid field to key ID when KeyService is refactored
// Build the token // Build the token
return Jwts.builder().setSubject(identity) return Jwts.builder().setSubject(identity)
.setIssuer(authenticationToken.getIssuer()) .setIssuer(authenticationToken.getIssuer())
.setAudience(authenticationToken.getIssuer()) .setAudience(authenticationToken.getIssuer())
.claim(USERNAME_CLAIM, username) .claim(USERNAME_CLAIM, username)
.claim(KEY_ID_CLAIM, identity) .claim(KEY_ID_CLAIM, key.getId())
.setExpiration(expiration.getTime()) .setExpiration(expiration.getTime())
.setIssuedAt(Calendar.getInstance().getTime()) .setIssuedAt(Calendar.getInstance().getTime())
.signWith(SIGNATURE_ALGORITHM, keyBytes).compact(); .signWith(SIGNATURE_ALGORITHM, keyBytes).compact();

View File

@ -21,6 +21,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.nifi.authentication.AuthenticationResponse; import org.apache.nifi.authentication.AuthenticationResponse;
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.NewAccountAuthenticationRequestToken; import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken;
@ -28,7 +29,6 @@ import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
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;
import org.springframework.security.authentication.BadCredentialsException;
/** /**
* Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate. * Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate.
@ -58,7 +58,7 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
try { try {
authenticationResponse = certificateIdentityProvider.authenticate(certificates); authenticationResponse = certificateIdentityProvider.authenticate(certificates);
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
throw new BadCredentialsException(iae.getMessage(), iae); throw new InvalidAuthenticationException(iae.getMessage(), iae);
} }
final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(request, authenticationResponse.getIdentity()); final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(request, authenticationResponse.getIdentity());

View File

@ -22,10 +22,12 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.apache.nifi.key.Key;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
@ -117,9 +119,14 @@ public class JwtServiceTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
final Key key = new Key();
key.setId(0);
key.setIdentity(HMAC_SECRET);
key.setKey(HMAC_SECRET);
mockKeyService = Mockito.mock(KeyService.class); mockKeyService = Mockito.mock(KeyService.class);
when(mockKeyService.getKey(anyString())).thenReturn(HMAC_SECRET); when(mockKeyService.getKey(anyInt())).thenReturn(key);
when(mockKeyService.getOrCreateKey(anyString())).thenReturn(HMAC_SECRET); when(mockKeyService.getOrCreateKey(anyString())).thenReturn(key);
jwtService = new JwtService(mockKeyService); jwtService = new JwtService(mockKeyService);
} }

View File

@ -142,7 +142,7 @@ nf.CanvasHeader = (function () {
}); });
// show the login link if supported and user is currently anonymous // show the login link if supported and user is currently anonymous
var isAnonymous = $('#current-user').text() === nf.Canvas.ANONYMOUS_USER_TEXT; var isAnonymous = $('#current-user').text() === nf.Common.ANONYMOUS_USER_TEXT;
if (supportsLogin === true && isAnonymous) { if (supportsLogin === true && isAnonymous) {
// login link // login link
$('#login-link').click(function () { $('#login-link').click(function () {

View File

@ -936,7 +936,6 @@ nf.Canvas = (function () {
}; };
return { return {
ANONYMOUS_USER_TEXT: 'Anonymous user',
CANVAS_OFFSET: 0, CANVAS_OFFSET: 0,
/** /**
* Determines if the current broswer supports SVG. * Determines if the current broswer supports SVG.
@ -1056,17 +1055,8 @@ nf.Canvas = (function () {
$('#logout-link-container').show(); $('#logout-link-container').show();
} }
} else { } else {
// alert user's of anonymous access // set the anonymous user label
$('#anonymous-user-alert').show().qtip($.extend({}, nf.Common.config.tooltipConfig, { nf.Common.setAnonymousUserLabel();
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(); deferred.resolve();
}).fail(function (xhr, status, error) { }).fail(function (xhr, status, error) {

View File

@ -96,8 +96,10 @@ nf.Login = (function () {
'password': $('#password').val() 'password': $('#password').val()
} }
}).done(function (jwt) { }).done(function (jwt) {
// store the jwt and reload the page // get the payload and store the token with the appropirate expiration
nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt)); var token = nf.Common.getJwtPayload(jwt);
var expiration = parseInt(token['exp'], 10) * nf.Common.MILLIS_PER_SECOND;
nf.Storage.setItem('jwt', jwt, expiration);
// check to see if they actually have access now // check to see if they actually have access now
$.ajax({ $.ajax({
@ -112,8 +114,7 @@ nf.Login = (function () {
nf.Common.scheduleTokenRefresh(); nf.Common.scheduleTokenRefresh();
// show the user // show the user
var user = nf.Common.getJwtSubject(jwt); $('#nifi-user-submit-justification').text(token['preferred_username']);
$('#nifi-user-submit-justification').text(user);
// show the registration form // show the registration form
initializeNiFiRegistration(); initializeNiFiRegistration();
@ -133,8 +134,7 @@ nf.Login = (function () {
nf.Common.scheduleTokenRefresh(); nf.Common.scheduleTokenRefresh();
// show the user // show the user
var user = nf.Common.getJwtSubject(jwt); $('#nifi-user-submit-justification').text(token['preferred_username']);
$('#nifi-user-submit-justification').text(user);
if (xhr.status === 401) { if (xhr.status === 401) {
initializeNiFiRegistration(); initializeNiFiRegistration();

View File

@ -54,9 +54,17 @@ $(document).ready(function () {
// include jwt when possible // include jwt when possible
$.ajaxSetup({ $.ajaxSetup({
'beforeSend': function(xhr) { 'beforeSend': function(xhr) {
var hadToken = nf.Storage.hasItem('jwt');
// get the token to include in all requests
var token = nf.Storage.getItem('jwt'); var token = nf.Storage.getItem('jwt');
if (token) { if (token !== null) {
xhr.setRequestHeader('Authorization', 'Bearer ' + token); 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;
}
} }
} }
}); });
@ -83,6 +91,8 @@ nf.Common = (function () {
var tokenRefreshInterval = null; var tokenRefreshInterval = null;
return { return {
ANONYMOUS_USER_TEXT: 'Anonymous user',
config: { config: {
sensitiveText: 'Sensitive value set', sensitiveText: 'Sensitive value set',
tooltipConfig: { tooltipConfig: {
@ -100,9 +110,6 @@ nf.Common = (function () {
at: 'top right', at: 'top right',
my: 'bottom left' my: 'bottom left'
} }
},
urls: {
token: '../nifi-api/access/token'
} }
}, },
@ -148,7 +155,7 @@ nf.Common = (function () {
} }
// set the interval to one hour // set the interval to one hour
var interval = nf.Common.MILLIS_PER_HOUR; var interval = 10 * nf.Common.MILLIS_PER_MINUTE;
var checkExpiration = function () { var checkExpiration = function () {
var expiration = nf.Storage.getItemExpiration('jwt'); var expiration = nf.Storage.getItemExpiration('jwt');
@ -161,13 +168,16 @@ nf.Common = (function () {
// get the time remainging plus a little bonus time to reload the token // get the time remainging plus a little bonus time to reload the token
var timeRemaining = expirationDate.valueOf() - now.valueOf() - nf.Common.MILLIS_PER_MINUTE; var timeRemaining = expirationDate.valueOf() - now.valueOf() - nf.Common.MILLIS_PER_MINUTE;
if (timeRemaining < interval) { if (timeRemaining < interval) {
// if the token will expire before the next interval minus some bonus time, refresh now if ($('#current-user').text() !== nf.Common.ANONYMOUS_USER_TEXT && !$('#anonymous-user-alert').is(':visible')) {
$.ajax({ // if the token will expire before the next interval minus some bonus time, notify the user to re-login
type: 'POST', $('#anonymous-user-alert').show().qtip($.extend({}, nf.Common.config.tooltipConfig, {
url: nf.Common.config.urls.token content: 'Your session will expire soon. Please log in again to avoid being automatically logged out.',
}).done(function (jwt) { position: {
nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt)); my: 'top right',
}); at: 'bottom left'
}
}));
}
} }
} }
}; };
@ -179,6 +189,28 @@ nf.Common = (function () {
tokenRefreshInterval = setInterval(checkExpiration, interval); tokenRefreshInterval = setInterval(checkExpiration, interval);
}, },
/**
* Sets the anonymous user label.
*/
setAnonymousUserLabel: function () {
var anonymousUserAlert = $('#anonymous-user-alert');
if (anonymousUserAlert.data('qtip')) {
anonymousUserAlert.qtip('api').destroy(true);
}
// alert user's of anonymous access
anonymousUserAlert.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.Common.ANONYMOUS_USER_TEXT).show();
},
/** /**
* Extracts the subject from the specified jwt. If the jwt is not as expected * Extracts the subject from the specified jwt. If the jwt is not as expected
* an empty string is returned. * an empty string is returned.
@ -186,7 +218,7 @@ nf.Common = (function () {
* @param {string} jwt * @param {string} jwt
* @returns {string} * @returns {string}
*/ */
getJwtSubject: function (jwt) { getJwtPayload: function (jwt) {
if (nf.Common.isDefinedAndNotNull(jwt)) { if (nf.Common.isDefinedAndNotNull(jwt)) {
var segments = jwt.split(/\./); var segments = jwt.split(/\./);
if (segments.length !== 3) { if (segments.length !== 3) {
@ -196,40 +228,8 @@ nf.Common = (function () {
var rawPayload = $.base64.atob(segments[1]); var rawPayload = $.base64.atob(segments[1]);
var payload = JSON.parse(rawPayload); var payload = JSON.parse(rawPayload);
if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) { if (nf.Common.isDefinedAndNotNull(payload)) {
return payload['preferred_username']; return payload;
} else {
'';
}
}
return '';
},
/**
* Extracts the expiration from the specified jwt. If the jwt is not as expected
* a null value is returned.
*
* @param {string} jwt
* @returns {integer}
*/
getJwtExpiration: function (jwt) {
if (nf.Common.isDefinedAndNotNull(jwt)) {
var segments = jwt.split(/\./);
if (segments.length !== 3) {
return null;
}
var rawPayload = $.base64.atob(segments[1]);
var payload = JSON.parse(rawPayload);
if (nf.Common.isDefinedAndNotNull(payload['exp'])) {
try {
// jwt exp is in seconds
return parseInt(payload['exp'], 10) * nf.Common.MILLIS_PER_SECOND;
} catch (e) {
return null;
}
} else { } else {
return null; return null;
} }
@ -313,6 +313,28 @@ nf.Common = (function () {
* @argument {string} error The error * @argument {string} error The error
*/ */
handleAjaxError: function (xhr, status, error) { handleAjaxError: function (xhr, status, error) {
if (status === 'canceled') {
if ($('#splash').is(':visible')) {
$('#message-title').text('Session Expired');
$('#message-content').text('Your session has expired. Please reload to log in again.');
// show the error pane
$('#message-pane').show();
// close the canvas
nf.Common.closeCanvas();
} else {
nf.Dialog.showOkDialog({
dialogContent: 'Your session has expired. Please press Ok to log in again.',
overlayBackground: false,
okHandler: function () {
window.location = '/nifi';
}
});
}
return;
}
// if an error occurs while the splash screen is visible close the canvas show the error message // if an error occurs while the splash screen is visible close the canvas show the error message
if ($('#splash').is(':visible')) { if ($('#splash').is(':visible')) {
if (xhr.status === 401) { if (xhr.status === 401) {

View File

@ -23,15 +23,6 @@ $(document).ready(function () {
// configure the ok dialog // configure the ok dialog
$('#nf-ok-dialog').modal({ $('#nf-ok-dialog').modal({
buttons: [{
buttonText: 'Ok',
handler: {
click: function () {
// close the dialog
$('#nf-ok-dialog').modal('hide');
}
}
}],
handler: { handler: {
close: function () { close: function () {
// clear the content // clear the content
@ -77,6 +68,20 @@ nf.Dialog = (function () {
var content = $('<p></p>').append(options.dialogContent); var content = $('<p></p>').append(options.dialogContent);
$('#nf-ok-dialog-content').append(content); $('#nf-ok-dialog-content').append(content);
// update the button model
$('#nf-ok-dialog').modal('setButtonModel', [{
buttonText: 'Ok',
handler: {
click: function () {
// close the dialog
$('#nf-ok-dialog').modal('hide');
if (typeof options.okHandler === 'function') {
options.okHandler.call(this);
}
}
}
}]);
// show the dialog // show the dialog
$('#nf-ok-dialog').modal('setHeaderText', options.headerText).modal('setOverlayBackground', options.overlayBackground).modal('show'); $('#nf-ok-dialog').modal('setHeaderText', options.headerText).modal('setOverlayBackground', options.overlayBackground).modal('show');
}, },

View File

@ -42,32 +42,18 @@ nf.Storage = (function () {
}; };
/** /**
* If the item at key is not expired, the value of field is returned. Otherwise, null. * Gets an enty for the key. The entry expiration is not checked.
* *
* @param {string} key * @param {string} key
* @param {string} field
* @return {object} the value
*/ */
var getEntryField = function (key, field) { var getEntry = function (key) {
try { try {
// parse the entry // parse the entry
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 (nf.Common.isDefinedAndNotNull(entry)) {
return entry;
// if the entry is expired, drop it and return null
if (checkExpiration(entry)) {
nf.Storage.removeItem(key);
return null;
}
// if the entry has the specified field return its value
if (nf.Common.isDefinedAndNotNull(entry[field])) {
return entry[field];
} else {
return null;
}
} else { } else {
return null; return null;
} }
@ -86,11 +72,9 @@ nf.Storage = (function () {
try { try {
// get the next item // get the next item
var key = localStorage.key(i); var key = localStorage.key(i);
var entry = JSON.parse(localStorage.getItem(key));
if (checkExpiration(entry)) { // attempt to get the item which will expire if necessary
nf.Storage.removeItem(key); nf.Storage.getItem(key);
}
} catch (e) { } catch (e) {
} }
} }
@ -117,6 +101,17 @@ nf.Storage = (function () {
localStorage.setItem(key, JSON.stringify(entry)); localStorage.setItem(key, JSON.stringify(entry));
}, },
/**
* Returns whether there is an entry for this key. This will not check the expiration. If
* the entry is expired, it will return null on a subsequent getItem invocation.
*
* @param {string} key
* @returns {boolean}
*/
hasItem: function (key) {
return getEntry(key) !== null;
},
/** /**
* Gets the item with the specified key. If an item with this key does * Gets the item with the specified key. If an item with this key does
* not exist, null is returned. If an item exists but cannot be parsed * not exist, null is returned. If an item exists but cannot be parsed
@ -125,18 +120,44 @@ nf.Storage = (function () {
* @param {type} key * @param {type} key
*/ */
getItem: function (key) { getItem: function (key) {
return getEntryField(key, 'item'); var entry = getEntry(key);
if (entry === null) {
return null;
}
// if the entry is expired, drop it and return null
if (checkExpiration(entry)) {
nf.Storage.removeItem(key);
return null;
}
// if the entry has the specified field return its value
if (nf.Common.isDefinedAndNotNull(entry['item'])) {
return entry['item'];
} else {
return null;
}
}, },
/** /**
* Gets the expiration for the specified item. If the item does not exists our could * Gets the expiration for the specified item. This will not check the expiration. If
* not be parsed, returns null. * the entry is expired, it will return null on a subsequent getItem invocation.
* *
* @param {string} key * @param {string} key
* @returns {integer} * @returns {integer}
*/ */
getItemExpiration: function (key) { getItemExpiration: function (key) {
return getEntryField(key, 'expires'); var entry = getEntry(key);
if (entry === null) {
return null;
}
// if the entry has the specified field return its value
if (nf.Common.isDefinedAndNotNull(entry['expires'])) {
return entry['expires'];
} else {
return null;
}
}, },
/** /**

View File

@ -1,106 +0,0 @@
/*
* 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.ldap;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext;
import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext;
import org.apache.nifi.authentication.exception.IdentityAccessException;
import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
import org.apache.nifi.authorization.exception.ProviderCreationException;
import org.apache.nifi.authorization.exception.ProviderDestructionException;
import org.apache.nifi.util.FormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.CommunicationException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.security.ldap.userdetails.LdapUserDetails;
/**
* Abstract LDAP based implementation of a login identity provider.
*/
public abstract class AbstractLdapProvider implements LoginIdentityProvider {
private static final Logger logger = LoggerFactory.getLogger(AbstractLdapProvider.class);
private AbstractLdapAuthenticationProvider provider;
private long expiration;
@Override
public final void initialize(final LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
}
@Override
public final void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
final String rawExpiration = configurationContext.getProperty("Expiration Duration");
if (StringUtils.isBlank(rawExpiration)) {
throw new ProviderCreationException("The Expiration Duration must be specified.");
}
try {
expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS);
} catch (final IllegalArgumentException iae) {
throw new ProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration));
}
provider = getLdapAuthenticationProvider(configurationContext);
}
protected abstract AbstractLdapAuthenticationProvider getLdapAuthenticationProvider(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException;
@Override
public final AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException {
if (provider == null) {
throw new IdentityAccessException("The LDAP authentication provider is not initialized.");
}
try {
// perform the authentication
final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword());
final Authentication authentication = provider.authenticate(token);
// attempt to get the ldap user details to get the DN
if (authentication.getPrincipal() instanceof LdapUserDetails) {
final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration);
} else {
return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration);
}
} catch (final CommunicationException | AuthenticationServiceException e) {
logger.error(e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug(StringUtils.EMPTY, e);
}
throw new IdentityAccessException("Unable to query the configured directory server. See the logs for additional details.", e);
} catch (final BadCredentialsException bce) {
throw new InvalidLoginCredentialsException(bce.getMessage(), bce);
}
}
@Override
public final void preDestruction() throws ProviderDestructionException {
}
}

View File

@ -1,51 +0,0 @@
/*
* 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.ldap;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext;
import org.apache.nifi.authorization.exception.ProviderCreationException;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
/**
* Active Directory based implementation of a login identity provider.
*/
public class ActiveDirectoryProvider extends AbstractLdapProvider {
@Override
protected AbstractLdapAuthenticationProvider getLdapAuthenticationProvider(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
final String url = configurationContext.getProperty("Url");
if (StringUtils.isBlank(url)) {
throw new ProviderCreationException("The Active Directory 'Url' must be specified.");
}
final String domain = configurationContext.getProperty("Domain");
final String userSearchBase = configurationContext.getProperty("User Search Base");
final ActiveDirectoryLdapAuthenticationProvider activeDirectoryAuthenticationProvider
= new ActiveDirectoryLdapAuthenticationProvider(StringUtils.isBlank(domain) ? null : domain, url, StringUtils.isBlank(userSearchBase) ? null : userSearchBase);
final String userSearchFilter = configurationContext.getProperty("User Search Filter");
if (StringUtils.isNotBlank(userSearchFilter)) {
activeDirectoryAuthenticationProvider.setSearchFilter(userSearchFilter);
}
return activeDirectoryAuthenticationProvider;
}
}

View File

@ -27,58 +27,71 @@ import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext; import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext;
import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext;
import org.apache.nifi.authentication.exception.IdentityAccessException;
import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
import org.apache.nifi.authorization.exception.ProviderCreationException; import org.apache.nifi.authorization.exception.ProviderCreationException;
import org.apache.nifi.authorization.exception.ProviderDestructionException;
import org.apache.nifi.security.util.SslContextFactory; import org.apache.nifi.security.util.SslContextFactory;
import org.apache.nifi.security.util.SslContextFactory.ClientAuth;
import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.FormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.CommunicationException;
import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy; import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy;
import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy; import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
import org.springframework.ldap.core.support.DigestMd5DirContextAuthenticationStrategy; import org.springframework.ldap.core.support.DigestMd5DirContextAuthenticationStrategy;
import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy; import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider; import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.security.ldap.authentication.BindAuthenticator; import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.search.LdapUserSearch; import org.springframework.security.ldap.search.LdapUserSearch;
import org.springframework.security.ldap.userdetails.LdapUserDetails;
/** /**
* LDAP based implementation of a login identity provider. * Abstract LDAP based implementation of a login identity provider.
*/ */
public class LdapProvider extends AbstractLdapProvider { public class LdapProvider implements LoginIdentityProvider {
private static final Logger logger = LoggerFactory.getLogger(LdapProvider.class);
private static final String TLS = "TLS"; private static final String TLS = "TLS";
private AbstractLdapAuthenticationProvider provider;
private long expiration;
@Override @Override
protected AbstractLdapAuthenticationProvider getLdapAuthenticationProvider(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException { public final void initialize(final LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
}
@Override
public final void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
final String rawExpiration = configurationContext.getProperty("Expiration Duration");
if (StringUtils.isBlank(rawExpiration)) {
throw new ProviderCreationException("The Expiration Duration must be specified.");
}
try {
expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS);
} catch (final IllegalArgumentException iae) {
throw new ProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration));
}
final LdapContextSource context = new LdapContextSource(); final LdapContextSource context = new LdapContextSource();
final Map<String, Object> baseEnvironment = new HashMap<>(); final Map<String, Object> baseEnvironment = new HashMap<>();
// connection time out // connect/read time out
final String rawConnectTimeout = configurationContext.getProperty("Connect Timeout"); setTimeout(configurationContext, baseEnvironment, "Connect Timeout", "com.sun.jndi.ldap.connect.timeout");
setTimeout(configurationContext, baseEnvironment, "Read Timeout", "com.sun.jndi.ldap.read.timeout");
// TODO: Refactor to utility method to remove duplicate code
if (StringUtils.isNotBlank(rawConnectTimeout)) {
try {
final Long connectTimeout = FormatUtils.getTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
baseEnvironment.put("com.sun.jndi.ldap.connect.timeout", connectTimeout.toString());
} catch (final IllegalArgumentException iae) {
throw new ProviderCreationException(String.format("The Connect Timeout '%s' is not a valid time duration", rawConnectTimeout));
}
}
// read time out
final String rawReadTimeout = configurationContext.getProperty("Read Timeout");
if (StringUtils.isNotBlank(rawReadTimeout)) {
try {
final Long readTimeout = FormatUtils.getTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
baseEnvironment.put("com.sun.jndi.ldap.read.timeout", readTimeout.toString());
} catch (final IllegalArgumentException iae) {
throw new ProviderCreationException(String.format("The Read Timeout '%s' is not a valid time duration", rawReadTimeout));
}
}
// set the base environment is necessary // set the base environment is necessary
if (!baseEnvironment.isEmpty()) { if (!baseEnvironment.isEmpty()) {
@ -140,7 +153,7 @@ public class LdapProvider extends AbstractLdapProvider {
sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, TLS); sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, TLS);
} else { } else {
try { try {
final ClientAuth clientAuth = ClientAuth.valueOf(rawClientAuth); final SslContextFactory.ClientAuth clientAuth = SslContextFactory.ClientAuth.valueOf(rawClientAuth);
sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType,
rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, TLS); rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, TLS);
} catch (final IllegalArgumentException iae) { } catch (final IllegalArgumentException iae) {
@ -205,7 +218,56 @@ public class LdapProvider extends AbstractLdapProvider {
} }
// create the underlying provider // create the underlying provider
final LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider(authenticator); provider = new LdapAuthenticationProvider(authenticator);
return ldapAuthenticationProvider;
} }
private void setTimeout(final LoginIdentityProviderConfigurationContext configurationContext,
final Map<String, Object> baseEnvironment,
final String configurationProperty,
final String environmentKey) {
final String rawTimeout = configurationContext.getProperty(configurationProperty);
if (StringUtils.isNotBlank(rawTimeout)) {
try {
final Long timeout = FormatUtils.getTimeDuration(rawTimeout, TimeUnit.MILLISECONDS);
baseEnvironment.put(environmentKey, timeout.toString());
} catch (final IllegalArgumentException iae) {
throw new ProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout));
}
}
}
@Override
public final AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException {
if (provider == null) {
throw new IdentityAccessException("The LDAP authentication provider is not initialized.");
}
try {
// perform the authentication
final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword());
final Authentication authentication = provider.authenticate(token);
// attempt to get the ldap user details to get the DN
if (authentication.getPrincipal() instanceof LdapUserDetails) {
final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration);
} else {
return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration);
}
} catch (final CommunicationException | AuthenticationServiceException e) {
logger.error(e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug(StringUtils.EMPTY, e);
}
throw new IdentityAccessException("Unable to query the configured directory server. See the logs for additional details.", e);
} catch (final BadCredentialsException bce) {
throw new InvalidLoginCredentialsException(bce.getMessage(), bce);
}
}
@Override
public final void preDestruction() throws ProviderDestructionException {
}
} }

View File

@ -13,4 +13,3 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
org.apache.nifi.ldap.LdapProvider org.apache.nifi.ldap.LdapProvider
org.apache.nifi.ldap.ActiveDirectoryProvider