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) {
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.");
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.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());
|
||||||
|
|
|
@ -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 -->
|
<!-- 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"/>
|
||||||
|
|
|
@ -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.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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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');
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
Loading…
Reference in New Issue