mirror of https://github.com/apache/nifi.git
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:
parent
7d04dfeac0
commit
c94d0271d9
|
@ -87,30 +87,21 @@ public final class CertificateUtils {
|
|||
*/
|
||||
public static String extractUsername(String dn) {
|
||||
String username = dn;
|
||||
String cn = "";
|
||||
|
||||
// ensure the dn is specified
|
||||
if (StringUtils.isNotBlank(dn)) {
|
||||
// determine the separate
|
||||
final String separator = StringUtils.indexOfIgnoreCase(dn, "/cn=") > 0 ? "/" : ",";
|
||||
|
||||
// attempt to locate the cn
|
||||
if (dn.startsWith("CN=")) {
|
||||
cn = StringUtils.substringBetween(dn, "CN=", ",");
|
||||
} else if (dn.startsWith("/CN=")) {
|
||||
cn = StringUtils.substringBetween(dn, "CN=", "/");
|
||||
} else if (dn.startsWith("C=") || dn.startsWith("/C=")) {
|
||||
cn = StringUtils.substringAfter(dn, "CN=");
|
||||
} 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, " ");
|
||||
// attempt to locate the cd
|
||||
final String cnPattern = "cn=";
|
||||
final int cnIndex = StringUtils.indexOfIgnoreCase(dn, cnPattern);
|
||||
if (cnIndex >= 0) {
|
||||
int separatorIndex = StringUtils.indexOf(dn, separator, cnIndex);
|
||||
if (separatorIndex > 0) {
|
||||
username = StringUtils.substring(dn, cnIndex + cnPattern.length(), separatorIndex);
|
||||
} else {
|
||||
username = cn;
|
||||
username = StringUtils.substring(dn, cnIndex + cnPattern.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,18 +16,28 @@
|
|||
*/
|
||||
package org.apache.nifi.admin.dao;
|
||||
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
/**
|
||||
* Key data access.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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.
|
||||
|
@ -35,5 +45,5 @@ public interface KeyDAO {
|
|||
* @param identity The user identity
|
||||
* @return The key
|
||||
*/
|
||||
String createKey(String identity);
|
||||
Key createKey(String identity);
|
||||
}
|
||||
|
|
|
@ -20,17 +20,23 @@ import java.sql.Connection;
|
|||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.UUID;
|
||||
import org.apache.nifi.admin.RepositoryUtils;
|
||||
import org.apache.nifi.admin.dao.DataAccessException;
|
||||
import org.apache.nifi.admin.dao.KeyDAO;
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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 "
|
||||
+ "WHERE IDENTITY = ?";
|
||||
|
||||
|
@ -47,26 +53,25 @@ public class StandardKeyDAO implements KeyDAO {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getKey(String identity) {
|
||||
if (identity == null) {
|
||||
throw new IllegalArgumentException("Specified identity cannot be null.");
|
||||
}
|
||||
|
||||
String key = null;
|
||||
public Key findKeyById(int id) {
|
||||
Key key = null;
|
||||
|
||||
PreparedStatement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// add each authority for the specified user
|
||||
statement = connection.prepareStatement(SELECT_KEY_FOR_USER);
|
||||
statement.setString(1, identity);
|
||||
statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_ID);
|
||||
statement.setInt(1, id);
|
||||
|
||||
// execute the query
|
||||
rs = statement.executeQuery();
|
||||
|
||||
// if the key was found, add it
|
||||
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) {
|
||||
throw new DataAccessException(sqle);
|
||||
|
@ -79,20 +84,62 @@ public class StandardKeyDAO implements KeyDAO {
|
|||
}
|
||||
|
||||
@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;
|
||||
ResultSet rs = null;
|
||||
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
|
||||
statement = connection.prepareStatement(INSERT_KEY);
|
||||
statement = connection.prepareStatement(INSERT_KEY, Statement.RETURN_GENERATED_KEYS);
|
||||
statement.setString(1, identity);
|
||||
statement.setString(2, key);
|
||||
statement.setString(2, keyValue);
|
||||
|
||||
// insert the key
|
||||
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;
|
||||
} else {
|
||||
throw new DataAccessException("Unable to add key for user.");
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.apache.nifi.admin.service;
|
||||
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param identity The user identity
|
||||
* @param id The key id
|
||||
* @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.
|
||||
|
@ -36,5 +38,5 @@ public interface KeyService {
|
|||
* @return The key
|
||||
* @throws AdministrationException if it failed to get/create the key
|
||||
*/
|
||||
String getOrCreateKey(String identity);
|
||||
Key getOrCreateKey(String identity);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,22 +20,23 @@ 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 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;
|
||||
|
||||
public GetKeyAction(String identity) {
|
||||
public GetKeyByIdentityAction(String identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
|
||||
public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
|
||||
final KeyDAO keyDao = daoFactory.getKeyDAO();
|
||||
return keyDao.getKey(identity);
|
||||
return keyDao.findLatestKeyByIdentity(identity);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,11 +20,12 @@ 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 user identity.
|
||||
*/
|
||||
public class GetOrCreateKeyAction implements AdministrationAction<String> {
|
||||
public class GetOrCreateKeyAction implements AdministrationAction<Key> {
|
||||
|
||||
private final String identity;
|
||||
|
||||
|
@ -33,10 +34,10 @@ public class GetOrCreateKeyAction implements AdministrationAction<String> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
|
||||
public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
|
||||
final KeyDAO keyDao = daoFactory.getKeyDAO();
|
||||
|
||||
String key = keyDao.getKey(identity);
|
||||
Key key = keyDao.findLatestKeyByIdentity(identity);
|
||||
if (key == null) {
|
||||
key = keyDao.createKey(identity);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ package org.apache.nifi.admin.service.impl;
|
|||
import org.apache.nifi.admin.dao.DataAccessException;
|
||||
import org.apache.nifi.admin.service.AdministrationException;
|
||||
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.transaction.Transaction;
|
||||
import org.apache.nifi.admin.service.transaction.TransactionBuilder;
|
||||
|
@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -44,19 +45,17 @@ public class StandardKeyService implements KeyService {
|
|||
private TransactionBuilder transactionBuilder;
|
||||
|
||||
@Override
|
||||
public String getKey(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
|
||||
public Key getKey(int id) {
|
||||
Transaction transaction = null;
|
||||
String key = null;
|
||||
Key key = null;
|
||||
|
||||
readLock.lock();
|
||||
try {
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// seed the accounts
|
||||
GetKeyAction addActions = new GetKeyAction(identity);
|
||||
// get the key
|
||||
GetKeyByIdAction addActions = new GetKeyByIdAction(id);
|
||||
key = transaction.execute(addActions);
|
||||
|
||||
// commit the transaction
|
||||
|
@ -76,11 +75,9 @@ public class StandardKeyService implements KeyService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String 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
|
||||
public Key getOrCreateKey(String identity) {
|
||||
Transaction transaction = null;
|
||||
String key = null;
|
||||
Key key = null;
|
||||
|
||||
writeLock.lock();
|
||||
try {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.AccessConfigurationEntity;
|
||||
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.UntrustedProxyException;
|
||||
import org.apache.nifi.web.security.jwt.JwtService;
|
||||
|
@ -185,16 +186,11 @@ public class AccessResource extends ApplicationResource {
|
|||
accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
|
||||
accessStatus.setMessage("No credentials supplied, unknown user.");
|
||||
} else {
|
||||
try {
|
||||
// Extract the Base64 encoded token from the Authorization header
|
||||
final String token = StringUtils.substringAfterLast(authorization, " ");
|
||||
|
||||
try {
|
||||
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
|
||||
accessStatus.setIdentity(principal);
|
||||
accessStatus.setUsername(CertificateUtils.extractUsername(principal));
|
||||
|
@ -208,13 +204,12 @@ public class AccessResource extends ApplicationResource {
|
|||
// no issues with authorization
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
|
||||
accessStatus.setMessage("Account is active and authorized");
|
||||
}
|
||||
} catch (JwtException e) {
|
||||
// TODO: Handle the exception from a failed JWT verification
|
||||
throw new AccessDeniedException("The JWT could not be verified", e);
|
||||
throw new InvalidAuthenticationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
|
||||
|
||||
// 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.");
|
||||
}
|
||||
|
||||
// ensure the proxy chain is authorized
|
||||
checkAuthorization(proxyChain);
|
||||
|
||||
// set the user identity
|
||||
accessStatus.setIdentity(proxyChain.get(0));
|
||||
accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0)));
|
||||
|
||||
// ensure the proxy chain is authorized
|
||||
checkAuthorization(proxyChain);
|
||||
|
||||
// no issues with authorization
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
|
||||
accessStatus.setMessage("Account is active and authorized");
|
||||
} catch (final IllegalArgumentException iae) {
|
||||
throw new InvalidAuthenticationException(iae.getMessage(), iae);
|
||||
}
|
||||
}
|
||||
} catch (final UsernameNotFoundException unfe) {
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -255,6 +255,7 @@
|
|||
|
||||
<!-- exception mapping -->
|
||||
<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.AccountNotFoundExceptionMapper" scope="singleton"/>
|
||||
<bean class="org.apache.nifi.web.api.config.AdministrationExceptionMapper" scope="singleton"/>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory;
|
|||
import org.springframework.security.authentication.AccountStatusException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
@ -134,8 +133,8 @@ public abstract class NiFiAuthenticationFilter implements Filter {
|
|||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
out.println("Access is denied.");
|
||||
}
|
||||
} else if (ae instanceof BadCredentialsException) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
} else if (ae instanceof InvalidAuthenticationException) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
out.println(ae.getMessage());
|
||||
} else if (ae instanceof AccountStatusException) {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
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
|
||||
|
||||
// get the principal out of the user token
|
||||
// look for an authorization token
|
||||
final String authorization = request.getHeader(AUTHORIZATION);
|
||||
|
||||
// if there is no authorization header, we don't know the user
|
||||
|
@ -61,9 +61,6 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
|
|||
|
||||
try {
|
||||
final String jwtPrincipal = jwtService.getAuthenticationFromToken(token);
|
||||
if (jwtPrincipal == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNewAccountRequest(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));
|
||||
}
|
||||
} catch (JwtException e) {
|
||||
// TODO: Is this the correct way to handle an unverified token?
|
||||
logger.error("Could not verify JWT", e);
|
||||
return null;
|
||||
throw new InvalidAuthenticationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Calendar;
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -88,17 +89,16 @@ public class JwtService {
|
|||
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
|
||||
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
|
||||
final String keyId = claims.get(KEY_ID_CLAIM, String.class);
|
||||
// The key is unique per identity and should be retrieved from the key service
|
||||
final String key = keyService.getKey(keyId);
|
||||
// Get the key based on the key id in the claims
|
||||
final Integer keyId = claims.get(KEY_ID_CLAIM, Integer.class);
|
||||
final Key key = keyService.getKey(keyId);
|
||||
|
||||
// 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 + "]");
|
||||
}
|
||||
|
||||
return key.getBytes(StandardCharsets.UTF_8);
|
||||
return key.getKey().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}).parseClaimsJws(base64EncodedToken);
|
||||
} catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
|
||||
|
@ -137,21 +137,19 @@ public class JwtService {
|
|||
|
||||
try {
|
||||
// Get/create the key for this user
|
||||
final String key = keyService.getOrCreateKey(identity);
|
||||
final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
|
||||
final Key key = keyService.getOrCreateKey(identity);
|
||||
final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
logger.trace("Generating JWT for " + authenticationToken);
|
||||
|
||||
// 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
|
||||
return Jwts.builder().setSubject(identity)
|
||||
.setIssuer(authenticationToken.getIssuer())
|
||||
.setAudience(authenticationToken.getIssuer())
|
||||
.claim(USERNAME_CLAIM, username)
|
||||
.claim(KEY_ID_CLAIM, identity)
|
||||
.claim(KEY_ID_CLAIM, key.getId())
|
||||
.setExpiration(expiration.getTime())
|
||||
.setIssuedAt(Calendar.getInstance().getTime())
|
||||
.signWith(SIGNATURE_ALGORITHM, keyBytes).compact();
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.List;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
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.ProxiedEntitiesUtils;
|
||||
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.slf4j.Logger;
|
||||
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.
|
||||
|
@ -58,7 +58,7 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
|
|||
try {
|
||||
authenticationResponse = certificateIdentityProvider.authenticate(certificates);
|
||||
} 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());
|
||||
|
|
|
@ -22,10 +22,12 @@ import java.security.InvalidKeyException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
|
@ -117,9 +119,14 @@ public class JwtServiceTest {
|
|||
|
||||
@Before
|
||||
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);
|
||||
when(mockKeyService.getKey(anyString())).thenReturn(HMAC_SECRET);
|
||||
when(mockKeyService.getOrCreateKey(anyString())).thenReturn(HMAC_SECRET);
|
||||
when(mockKeyService.getKey(anyInt())).thenReturn(key);
|
||||
when(mockKeyService.getOrCreateKey(anyString())).thenReturn(key);
|
||||
jwtService = new JwtService(mockKeyService);
|
||||
}
|
||||
|
||||
|
|
|
@ -142,7 +142,7 @@ nf.CanvasHeader = (function () {
|
|||
});
|
||||
|
||||
// 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) {
|
||||
// login link
|
||||
$('#login-link').click(function () {
|
||||
|
|
|
@ -936,7 +936,6 @@ nf.Canvas = (function () {
|
|||
};
|
||||
|
||||
return {
|
||||
ANONYMOUS_USER_TEXT: 'Anonymous user',
|
||||
CANVAS_OFFSET: 0,
|
||||
/**
|
||||
* Determines if the current broswer supports SVG.
|
||||
|
@ -1056,17 +1055,8 @@ 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();
|
||||
// set the anonymous user label
|
||||
nf.Common.setAnonymousUserLabel();
|
||||
}
|
||||
deferred.resolve();
|
||||
}).fail(function (xhr, status, error) {
|
||||
|
|
|
@ -96,8 +96,10 @@ nf.Login = (function () {
|
|||
'password': $('#password').val()
|
||||
}
|
||||
}).done(function (jwt) {
|
||||
// store the jwt and reload the page
|
||||
nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt));
|
||||
// get the payload and store the token with the appropirate expiration
|
||||
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
|
||||
$.ajax({
|
||||
|
@ -112,8 +114,7 @@ nf.Login = (function () {
|
|||
nf.Common.scheduleTokenRefresh();
|
||||
|
||||
// show the user
|
||||
var user = nf.Common.getJwtSubject(jwt);
|
||||
$('#nifi-user-submit-justification').text(user);
|
||||
$('#nifi-user-submit-justification').text(token['preferred_username']);
|
||||
|
||||
// show the registration form
|
||||
initializeNiFiRegistration();
|
||||
|
@ -133,8 +134,7 @@ nf.Login = (function () {
|
|||
nf.Common.scheduleTokenRefresh();
|
||||
|
||||
// show the user
|
||||
var user = nf.Common.getJwtSubject(jwt);
|
||||
$('#nifi-user-submit-justification').text(user);
|
||||
$('#nifi-user-submit-justification').text(token['preferred_username']);
|
||||
|
||||
if (xhr.status === 401) {
|
||||
initializeNiFiRegistration();
|
||||
|
|
|
@ -54,9 +54,17 @@ $(document).ready(function () {
|
|||
// include jwt when possible
|
||||
$.ajaxSetup({
|
||||
'beforeSend': function(xhr) {
|
||||
var hadToken = nf.Storage.hasItem('jwt');
|
||||
|
||||
// get the token to include in all requests
|
||||
var token = nf.Storage.getItem('jwt');
|
||||
if (token) {
|
||||
if (token !== null) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
} else {
|
||||
// if the current user was logged in with a token and the token just expired, reload
|
||||
if (hadToken === true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -83,6 +91,8 @@ nf.Common = (function () {
|
|||
var tokenRefreshInterval = null;
|
||||
|
||||
return {
|
||||
ANONYMOUS_USER_TEXT: 'Anonymous user',
|
||||
|
||||
config: {
|
||||
sensitiveText: 'Sensitive value set',
|
||||
tooltipConfig: {
|
||||
|
@ -100,9 +110,6 @@ nf.Common = (function () {
|
|||
at: 'top right',
|
||||
my: 'bottom left'
|
||||
}
|
||||
},
|
||||
urls: {
|
||||
token: '../nifi-api/access/token'
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -148,7 +155,7 @@ nf.Common = (function () {
|
|||
}
|
||||
|
||||
// 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 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
|
||||
var timeRemaining = expirationDate.valueOf() - now.valueOf() - nf.Common.MILLIS_PER_MINUTE;
|
||||
if (timeRemaining < interval) {
|
||||
// if the token will expire before the next interval minus some bonus time, refresh now
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: nf.Common.config.urls.token
|
||||
}).done(function (jwt) {
|
||||
nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt));
|
||||
});
|
||||
if ($('#current-user').text() !== nf.Common.ANONYMOUS_USER_TEXT && !$('#anonymous-user-alert').is(':visible')) {
|
||||
// if the token will expire before the next interval minus some bonus time, notify the user to re-login
|
||||
$('#anonymous-user-alert').show().qtip($.extend({}, nf.Common.config.tooltipConfig, {
|
||||
content: 'Your session will expire soon. Please log in again to avoid being automatically logged out.',
|
||||
position: {
|
||||
my: 'top right',
|
||||
at: 'bottom left'
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -179,6 +189,28 @@ nf.Common = (function () {
|
|||
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
|
||||
* an empty string is returned.
|
||||
|
@ -186,7 +218,7 @@ nf.Common = (function () {
|
|||
* @param {string} jwt
|
||||
* @returns {string}
|
||||
*/
|
||||
getJwtSubject: function (jwt) {
|
||||
getJwtPayload: function (jwt) {
|
||||
if (nf.Common.isDefinedAndNotNull(jwt)) {
|
||||
var segments = jwt.split(/\./);
|
||||
if (segments.length !== 3) {
|
||||
|
@ -196,40 +228,8 @@ nf.Common = (function () {
|
|||
var rawPayload = $.base64.atob(segments[1]);
|
||||
var payload = JSON.parse(rawPayload);
|
||||
|
||||
if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) {
|
||||
return payload['preferred_username'];
|
||||
} 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;
|
||||
}
|
||||
if (nf.Common.isDefinedAndNotNull(payload)) {
|
||||
return payload;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -313,6 +313,28 @@ nf.Common = (function () {
|
|||
* @argument {string} error The 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 ($('#splash').is(':visible')) {
|
||||
if (xhr.status === 401) {
|
||||
|
|
|
@ -23,15 +23,6 @@ $(document).ready(function () {
|
|||
|
||||
// configure the ok dialog
|
||||
$('#nf-ok-dialog').modal({
|
||||
buttons: [{
|
||||
buttonText: 'Ok',
|
||||
handler: {
|
||||
click: function () {
|
||||
// close the dialog
|
||||
$('#nf-ok-dialog').modal('hide');
|
||||
}
|
||||
}
|
||||
}],
|
||||
handler: {
|
||||
close: function () {
|
||||
// clear the content
|
||||
|
@ -77,6 +68,20 @@ nf.Dialog = (function () {
|
|||
var content = $('<p></p>').append(options.dialogContent);
|
||||
$('#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
|
||||
$('#nf-ok-dialog').modal('setHeaderText', options.headerText).modal('setOverlayBackground', options.overlayBackground).modal('show');
|
||||
},
|
||||
|
|
|
@ -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} field
|
||||
* @return {object} the value
|
||||
*/
|
||||
var getEntryField = function (key, field) {
|
||||
var getEntry = function (key) {
|
||||
try {
|
||||
// parse the entry
|
||||
var entry = JSON.parse(localStorage.getItem(key));
|
||||
|
||||
// ensure the entry and item are present
|
||||
if (nf.Common.isDefinedAndNotNull(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;
|
||||
}
|
||||
return entry;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -86,11 +72,9 @@ nf.Storage = (function () {
|
|||
try {
|
||||
// get the next item
|
||||
var key = localStorage.key(i);
|
||||
var entry = JSON.parse(localStorage.getItem(key));
|
||||
|
||||
if (checkExpiration(entry)) {
|
||||
nf.Storage.removeItem(key);
|
||||
}
|
||||
// attempt to get the item which will expire if necessary
|
||||
nf.Storage.getItem(key);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +101,17 @@ nf.Storage = (function () {
|
|||
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
|
||||
* not exist, null is returned. If an item exists but cannot be parsed
|
||||
|
@ -125,18 +120,44 @@ nf.Storage = (function () {
|
|||
* @param {type} 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
|
||||
* not be parsed, returns null.
|
||||
* Gets the expiration for the specified item. This will not check the expiration. If
|
||||
* the entry is expired, it will return null on a subsequent getItem invocation.
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {integer}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -27,58 +27,71 @@ import java.util.Map;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.SSLContext;
|
||||
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.security.util.SslContextFactory;
|
||||
import org.apache.nifi.security.util.SslContextFactory.ClientAuth;
|
||||
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.DefaultTlsDirContextAuthenticationStrategy;
|
||||
import org.springframework.ldap.core.support.DigestMd5DirContextAuthenticationStrategy;
|
||||
import org.springframework.ldap.core.support.LdapContextSource;
|
||||
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.BindAuthenticator;
|
||||
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
|
||||
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
|
||||
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 AbstractLdapAuthenticationProvider provider;
|
||||
private long expiration;
|
||||
|
||||
@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 Map<String, Object> baseEnvironment = new HashMap<>();
|
||||
|
||||
// connection time out
|
||||
final String rawConnectTimeout = configurationContext.getProperty("Connect 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));
|
||||
}
|
||||
}
|
||||
// connect/read time out
|
||||
setTimeout(configurationContext, baseEnvironment, "Connect Timeout", "com.sun.jndi.ldap.connect.timeout");
|
||||
setTimeout(configurationContext, baseEnvironment, "Read Timeout", "com.sun.jndi.ldap.read.timeout");
|
||||
|
||||
// set the base environment is necessary
|
||||
if (!baseEnvironment.isEmpty()) {
|
||||
|
@ -140,7 +153,7 @@ public class LdapProvider extends AbstractLdapProvider {
|
|||
sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, TLS);
|
||||
} else {
|
||||
try {
|
||||
final ClientAuth clientAuth = ClientAuth.valueOf(rawClientAuth);
|
||||
final SslContextFactory.ClientAuth clientAuth = SslContextFactory.ClientAuth.valueOf(rawClientAuth);
|
||||
sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType,
|
||||
rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, TLS);
|
||||
} catch (final IllegalArgumentException iae) {
|
||||
|
@ -205,7 +218,56 @@ public class LdapProvider extends AbstractLdapProvider {
|
|||
}
|
||||
|
||||
// create the underlying provider
|
||||
final LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider(authenticator);
|
||||
return ldapAuthenticationProvider;
|
||||
provider = new LdapAuthenticationProvider(authenticator);
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,4 +13,3 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
org.apache.nifi.ldap.LdapProvider
|
||||
org.apache.nifi.ldap.ActiveDirectoryProvider
|
Loading…
Reference in New Issue