KNOX-2375 - Token state eviction should access the keystore file less frequently (#337)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
index d47583b..18c3a4b 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
@@ -277,6 +277,9 @@
@Message( level = MessageLevel.ERROR, text = "Failed to add credential for cluster {0}: {1}" )
void failedToAddCredentialForCluster( String clusterName, @StackTrace( level = MessageLevel.DEBUG ) Exception e );
+ @Message( level = MessageLevel.ERROR, text = "Failed to add credentials for cluster {0}: {1}" )
+ void failedToAddCredentialsForCluster( String clusterName, @StackTrace( level = MessageLevel.DEBUG ) Exception e );
+
@Message( level = MessageLevel.ERROR, text = "Failed to get key for Gateway {0}: {1}" )
void failedToGetKeyForGateway( String alias, @StackTrace( level=MessageLevel.DEBUG ) Exception e );
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index 6f690cc..d03deec 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -248,9 +248,11 @@
private static final String KNOX_TOKEN_EVICTION_INTERVAL = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.eviction.interval";
private static final String KNOX_TOKEN_EVICTION_GRACE_PERIOD = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.eviction.grace.period";
+ private static final String KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.state.alias.persistence.interval";
private static final String KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.permissive.validation";
private static final long KNOX_TOKEN_EVICTION_INTERVAL_DEFAULT = TimeUnit.MINUTES.toSeconds(5);
private static final long KNOX_TOKEN_EVICTION_GRACE_PERIOD_DEFAULT = TimeUnit.HOURS.toSeconds(24);
+ private static final long KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL_DEFAULT = TimeUnit.SECONDS.toSeconds(15);
private static final boolean KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED_DEFAULT = false;
private static final String KNOX_HOMEPAGE_PINNED_TOPOLOGIES = "knox.homepage.pinned.topologies";
@@ -1139,6 +1141,11 @@
}
@Override
+ public long getKnoxTokenStateAliasPersistenceInterval() {
+ return getLong(KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL, KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL_DEFAULT);
+ }
+
+ @Override
public Set<String> getHiddenTopologiesOnHomepage() {
final Set<String> hiddenTopologies = new HashSet<>(getTrimmedStringCollection(KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES));
return hiddenTopologies == null || hiddenTopologies.isEmpty() ? KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES_DEFAULT : hiddenTopologies;
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultAliasService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultAliasService.java
index a3af2c5..c0a20ec 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultAliasService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultAliasService.java
@@ -24,6 +24,7 @@
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.apache.knox.gateway.GatewayMessages;
import org.apache.knox.gateway.config.GatewayConfig;
@@ -160,6 +161,15 @@
}
@Override
+ public void addAliasesForCluster(String clusterName, Map<String, String> aliases) throws AliasServiceException {
+ try {
+ keystoreService.addCredentialsForCluster(clusterName, aliases);
+ } catch (KeystoreServiceException e) {
+ LOG.failedToAddCredentialsForCluster(clusterName, e);
+ }
+ }
+
+ @Override
public void removeAliasForCluster(String clusterName, String alias)
throws AliasServiceException {
try {
@@ -170,6 +180,15 @@
}
@Override
+ public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
+ try {
+ keystoreService.removeCredentialsForCluster(clusterName, aliases);
+ } catch (KeystoreServiceException e) {
+ throw new AliasServiceException(e);
+ }
+ }
+
+ @Override
public char[] getPasswordFromAliasForGateway(String alias)
throws AliasServiceException {
return getPasswordFromAliasForCluster(NO_CLUSTER_NAME, alias);
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java
index 021fb82..5a58029 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java
@@ -59,8 +59,11 @@
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.crypto.spec.SecretKeySpec;
@@ -75,9 +78,10 @@
private static GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
private static GatewayResources RES = ResourcesFactory.get(GatewayResources.class);
- //let's configure the cache with hard-coded attributes now; we can introduce new gateway configuration later on if needed
- // visible for testing
+ // Let's configure the cache with hard-coded attributes now; we can introduce new gateway configuration later on if
+ // needed visible for testing
final Cache<CacheKey, String> cache = Caffeine.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).maximumSize(1000).build();
+
private GatewayConfig config;
private MasterService masterService;
@@ -287,18 +291,28 @@
@Override
public void addCredentialForCluster(String clusterName, String alias, String value)
throws KeystoreServiceException {
+ addCredentialsForCluster(clusterName, Collections.singletonMap(alias, value));
+ }
+
+ @Override
+ public void addCredentialsForCluster(String clusterName, Map<String, String> credentials)
+ throws KeystoreServiceException {
// Needed to prevent read then write synchronization issue where alias is not added
synchronized (this) {
- removeFromCache(clusterName, alias);
+ removeFromCache(clusterName, credentials.keySet());
KeyStore ks = getCredentialStoreForCluster(clusterName);
if (ks != null) {
try {
- final Key key = new SecretKeySpec(value.getBytes(StandardCharsets.UTF_8), "AES");
- ks.setKeyEntry(alias, key, masterService.getMasterSecret(), null);
+ // Add all the credential keys to the keystore
+ for (Map.Entry<String, String> credential : credentials.entrySet()) {
+ final Key key = new SecretKeySpec(credential.getValue().getBytes(StandardCharsets.UTF_8), "AES");
+ ks.setKeyEntry(credential.getKey(), key, masterService.getMasterSecret(), null);
+ }
+ // Write all the changes once
final Path keyStoreFilePath = keyStoreDirPath.resolve(clusterName + CREDENTIALS_SUFFIX);
writeKeyStoreToFile(ks, keyStoreFilePath, masterService.getMasterSecret());
- addToCache(clusterName, alias, value);
+ addToCache(clusterName, credentials);
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
LOG.failedToAddCredentialForCluster(clusterName, e);
}
@@ -338,18 +352,27 @@
@Override
public void removeCredentialForCluster(String clusterName, String alias) throws KeystoreServiceException {
+ removeCredentialsForCluster(clusterName, Collections.singleton(alias));
+ }
+
+ @Override
+ public void removeCredentialsForCluster(String clusterName, Set<String> aliases) throws KeystoreServiceException {
// Needed to prevent read then write synchronization issue where alias is not removed
synchronized (this) {
KeyStore ks = getCredentialStoreForCluster(clusterName);
if (ks != null) {
try {
- if (ks.containsAlias(alias)) {
- ks.deleteEntry(alias);
+ // Delete all the entries
+ for (String alias : aliases) {
+ if (ks.containsAlias(alias)) {
+ ks.deleteEntry(alias);
+ }
}
+ removeFromCache(clusterName, aliases);
+ // Update the keystore file once to reflect all the alias deletions
final Path keyStoreFilePath = keyStoreDirPath.resolve(clusterName + CREDENTIALS_SUFFIX);
writeKeyStoreToFile(ks, keyStoreFilePath, masterService.getMasterSecret());
- removeFromCache(clusterName, alias);
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
LOG.failedToRemoveCredentialForCluster(clusterName, e);
}
@@ -375,10 +398,30 @@
/**
* Called only from within critical sections of other methods above.
*/
+ private void addToCache(String clusterName, Map<String, String> credentials) {
+ for (String alias : credentials.keySet()) {
+ cache.put(CacheKey.of(clusterName, alias), credentials.get(alias));
+ }
+ }
+
+ /**
+ * Called only from within critical sections of other methods above.
+ */
private void removeFromCache(String clusterName, String alias) {
cache.invalidate(CacheKey.of(clusterName, alias));
}
+ /**
+ * Called only from within critical sections of other methods above.
+ */
+ private void removeFromCache(String clusterName, Set<String> aliases) {
+ Set<CacheKey> keys = new HashSet<>();
+ for (String alias : aliases) {
+ keys.add(CacheKey.of(clusterName, alias));
+ }
+ cache.invalidateAll(keys);
+ }
+
@Override
public String getKeystorePath() {
return config.getIdentityKeystorePath();
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/RemoteAliasService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/RemoteAliasService.java
index 5182fd1..5f5b0ed 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/RemoteAliasService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/RemoteAliasService.java
@@ -29,10 +29,14 @@
import java.security.cert.Certificate;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ServiceLoader;
+import java.util.Set;
/**
* An {@link AliasService} implementation based on remote service registry.
@@ -93,32 +97,46 @@
@Override
public void addAliasForCluster(final String clusterName,
- final String givenAlias, final String value)
- throws AliasServiceException {
+ final String givenAlias, final String value) throws AliasServiceException {
+ addAliasesForCluster(clusterName, Collections.singletonMap(givenAlias, value));
+ }
- /* convert all alias names to lower case since JDK expects the same behaviour */
- final String alias = givenAlias.toLowerCase(Locale.ROOT);
+ @Override
+ public void addAliasesForCluster(String clusterName, Map<String, String> credentials) throws AliasServiceException {
+ // Convert all alias names to lower case since JDK expects the same behaviour
+ Map<String, String> loweredCredentials = new HashMap<>();
+ for (Map.Entry<String, String> credential : credentials.entrySet()) {
+ loweredCredentials.put(credential.getKey().toLowerCase(Locale.ROOT), credential.getValue());
+ }
- /* first add the alias to the local keystore */
- localAliasService.addAliasForCluster(clusterName, alias, value);
+ // First add the alias to the local keystore
+ localAliasService.addAliasesForCluster(clusterName, loweredCredentials);
if (remoteAliasServiceImpl != null) {
- remoteAliasServiceImpl.addAliasForCluster(clusterName, alias, value);
+ remoteAliasServiceImpl.addAliasesForCluster(clusterName, loweredCredentials);
}
}
@Override
public void removeAliasForCluster(final String clusterName, final String givenAlias)
throws AliasServiceException {
- /* convert all alias names to lower case since JDK expects the same behaviour */
- final String alias = givenAlias.toLowerCase(Locale.ROOT);
+ removeAliasesForCluster(clusterName, Collections.singleton(givenAlias));
+ }
- /* first remove it from the local keystore */
- localAliasService.removeAliasForCluster(clusterName, alias);
+ @Override
+ public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
+ // Convert all alias names to lower case since JDK expects the same behavior
+ Set<String> loweredAliases = new HashSet<>();
+ for (String alias : aliases) {
+ loweredAliases.add(alias.toLowerCase(Locale.ROOT));
+ }
- /* If we have remote registry configured, query it */
+ // First, remove them from the local keystore
+ localAliasService.removeAliasesForCluster(clusterName, loweredAliases);
+
+ // If we have remote registry configured, remove them there also
if (remoteAliasServiceImpl != null) {
- remoteAliasServiceImpl.removeAliasForCluster(clusterName, alias);
+ remoteAliasServiceImpl.removeAliasesForCluster(clusterName, loweredAliases);
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasService.java
index 4abc411..10bd1bf 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasService.java
@@ -40,6 +40,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
/**
* An {@link AliasService} implementation based on zookeeper remote service registry.
@@ -222,6 +223,13 @@
}
@Override
+ public void addAliasesForCluster(String clusterName, Map<String, String> credentials) throws AliasServiceException {
+ for (Map.Entry<String, String> credential : credentials.entrySet()) {
+ addAliasForCluster(clusterName, credential.getKey(), credential.getValue());
+ }
+ }
+
+ @Override
public void removeAliasForCluster(final String clusterName, final String alias) throws AliasServiceException {
/* If we have remote registry configured, query it */
if (remoteClient != null) {
@@ -238,6 +246,13 @@
}
@Override
+ public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
+ for (String alias : aliases) {
+ removeAliasForCluster(clusterName, alias);
+ }
+ }
+
+ @Override
public char[] getPasswordFromAliasForCluster(String clusterName, String alias) throws AliasServiceException {
return getPasswordFromAliasForCluster(clusterName, alias, false);
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
index 1178e87..dd457e5 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
@@ -23,8 +23,15 @@
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@@ -32,8 +39,15 @@
*/
public class AliasBasedTokenStateService extends DefaultTokenStateService {
+ static final String TOKEN_MAX_LIFETIME_POSTFIX = "--max";
+
private AliasService aliasService;
- private static final String TOKEN_MAX_LIFETIME_POSTFIX = "--max";
+
+ private long statePersistenceInterval = TimeUnit.SECONDS.toSeconds(15);
+
+ private ScheduledExecutorService statePersistenceScheduler;
+
+ private final List<TokenState> unpersistedState = new ArrayList<>();
public void setAliasService(AliasService aliasService) {
this.aliasService = aliasService;
@@ -45,6 +59,66 @@
if (aliasService == null) {
throw new ServiceLifecycleException("The required AliasService reference has not been set.");
}
+ statePersistenceInterval = config.getKnoxTokenStateAliasPersistenceInterval();
+ if (statePersistenceInterval > 0) {
+ statePersistenceScheduler = Executors.newScheduledThreadPool(1);
+ }
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ super.start();
+ if (statePersistenceScheduler != null) {
+ // Run token eviction task at configured interval
+ statePersistenceScheduler.scheduleAtFixedRate(this::persistTokenState,
+ statePersistenceInterval,
+ statePersistenceInterval,
+ TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ super.stop();
+ if (statePersistenceScheduler != null) {
+ statePersistenceScheduler.shutdown();
+ }
+ }
+
+ protected void persistTokenState() {
+ Set<String> tokenIds = new HashSet<>(); // Collect the tokenIds for logging
+
+ List<TokenState> processing;
+ synchronized (unpersistedState) {
+ // Move unpersisted state to temp collection
+ processing = new ArrayList<>(unpersistedState);
+ unpersistedState.clear();
+ }
+
+ // Create a set of aliases based on the unpersisted TokenState objects
+ Map<String, String> aliases = new HashMap<>();
+ for (TokenState state : processing) {
+ tokenIds.add(state.getTokenId());
+ aliases.put(state.getAlias(), state.getAliasValue());
+ log.creatingTokenStateAliases(state.getTokenId());
+ }
+
+ // Write aliases in a batch
+ if (!aliases.isEmpty()) {
+ log.creatingTokenStateAliases();
+
+ try {
+ aliasService.addAliasesForCluster(AliasService.NO_CLUSTER_NAME, aliases);
+ for (String tokenId : tokenIds) {
+ log.createdTokenStateAliases(tokenId);
+ }
+ } catch (AliasServiceException e) {
+ log.failedToCreateTokenStateAliases(e);
+ synchronized (unpersistedState) {
+ unpersistedState.addAll(processing); // Restore the unpersisted state objects so they can be attempted later
+ }
+ }
+ }
}
@Override
@@ -52,53 +126,67 @@
long issueTime,
long expiration,
long maxLifetimeDuration) {
- isValidIdentifier(tokenId);
- try {
- aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId, String.valueOf(expiration));
- setMaxLifetime(tokenId, issueTime, maxLifetimeDuration);
- log.addedToken(tokenId, getTimestampDisplay(expiration));
- } catch (AliasServiceException e) {
- log.failedToSaveTokenState(tokenId, e);
+ super.addToken(tokenId, issueTime, expiration, maxLifetimeDuration);
+
+ synchronized (unpersistedState) {
+ unpersistedState.add(new TokenExpiration(tokenId, expiration));
}
}
@Override
protected void setMaxLifetime(final String tokenId, long issueTime, long maxLifetimeDuration) {
- try {
- aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME,
- tokenId + TOKEN_MAX_LIFETIME_POSTFIX,
- String.valueOf(issueTime + maxLifetimeDuration));
- } catch (AliasServiceException e) {
- log.failedToSaveTokenState(tokenId, e);
+ super.setMaxLifetime(tokenId, issueTime, maxLifetimeDuration);
+ synchronized (unpersistedState) {
+ unpersistedState.add(new TokenMaxLifetime(tokenId, issueTime, maxLifetimeDuration));
}
}
@Override
protected long getMaxLifetime(final String tokenId) {
- long result = 0;
- try {
- char[] maxLifetimeStr =
- aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME,
- tokenId + TOKEN_MAX_LIFETIME_POSTFIX);
- if (maxLifetimeStr != null) {
- result = Long.parseLong(new String(maxLifetimeStr));
+ long result = super.getMaxLifetime(tokenId);
+
+ // If there is no result from the in-memory collection, proceed to check the alias service
+ if (result < 1L) {
+ try {
+ char[] maxLifetimeStr =
+ aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME,
+ tokenId + TOKEN_MAX_LIFETIME_POSTFIX);
+ if (maxLifetimeStr != null) {
+ result = Long.parseLong(new String(maxLifetimeStr));
+ }
+ } catch (AliasServiceException e) {
+ log.errorAccessingTokenState(tokenId, e);
}
- } catch (AliasServiceException e) {
- log.errorAccessingTokenState(tokenId, e);
}
return result;
}
@Override
- public long getTokenExpiration(final String tokenId) throws UnknownTokenException {
+ public long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException {
long expiration = 0;
- validateToken(tokenId);
+ if (!validate) {
+ // If validation is not required, then check the in-memory collection first
+ try {
+ expiration = super.getTokenExpiration(tokenId, validate);
+ return expiration;
+ } catch (UnknownTokenException e) {
+ // It's not in memory
+ }
+ }
+
+ // If validating, or there is no associated record in the in-memory collection, proceed to check the alias service
+
+ if (validate) {
+ validateToken(tokenId);
+ }
try {
char[] expStr = aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId);
if (expStr != null) {
expiration = Long.parseLong(new String(expStr));
+ // Update the in-memory record
+ super.updateExpiration(tokenId, expiration);
}
} catch (Exception e) {
log.errorAccessingTokenState(tokenId, e);
@@ -109,30 +197,51 @@
@Override
protected boolean isUnknown(final String tokenId) {
- boolean isUnknown = false;
- try {
- isUnknown = (aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId) == null);
- } catch (AliasServiceException e) {
- log.errorAccessingTokenState(tokenId, e);
+ boolean isUnknown = super.isUnknown(tokenId);
+
+ // If it's not in the cache, then check the underlying alias
+ if (isUnknown) {
+ try {
+ isUnknown = (aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId) == null);
+ } catch (AliasServiceException e) {
+ log.errorAccessingTokenState(tokenId, e);
+ }
}
return isUnknown;
}
@Override
protected void removeToken(final String tokenId) throws UnknownTokenException {
- validateToken(tokenId);
+ removeTokens(Collections.singleton(tokenId));
+ }
- try {
- aliasService.removeAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId);
- aliasService.removeAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId + TOKEN_MAX_LIFETIME_POSTFIX);
- log.removedTokenState(tokenId);
- } catch (AliasServiceException e) {
- log.failedToRemoveTokenState(tokenId, e);
+ @Override
+ protected void removeTokens(Set<String> tokenIds) throws UnknownTokenException {
+ // Add the max lifetime aliases to the list of aliases to remove
+ Set<String> aliasesToRemove = new HashSet<>(tokenIds);
+ for (String tokenId : tokenIds) {
+ aliasesToRemove.add(tokenId + TOKEN_MAX_LIFETIME_POSTFIX);
+ log.removingTokenStateAliases(tokenId);
}
+
+ if (!aliasesToRemove.isEmpty()) {
+ log.removingTokenStateAliases();
+ try {
+ aliasService.removeAliasesForCluster(AliasService.NO_CLUSTER_NAME, aliasesToRemove);
+ for (String tokenId : tokenIds) {
+ log.removedTokenStateAliases(tokenId);
+ }
+ } catch (AliasServiceException e) {
+ log.failedToRemoveTokenStateAliases(e);
+ }
+ }
+
+ super.removeTokens(tokenIds);
}
@Override
protected void updateExpiration(final String tokenId, long expiration) {
+ super.updateExpiration(tokenId, expiration);
try {
aliasService.removeAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId);
aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME, tokenId, String.valueOf(expiration));
@@ -143,15 +252,79 @@
@Override
protected List<String> getTokens() {
- List<String> allAliases = new ArrayList<>();
+ List<String> tokenIds = null;
+
try {
- allAliases = aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME);
- /* only get the aliases that represent tokens and extract the current list of tokens */
- allAliases = allAliases.stream().filter(a -> a.contains(TOKEN_MAX_LIFETIME_POSTFIX)).map(a -> a.substring(0, a.indexOf(TOKEN_MAX_LIFETIME_POSTFIX)))
- .collect(Collectors.toList());
+ List<String> allAliases = aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME);
+
+ // Filter for the token state aliases, and extract the token ID
+ tokenIds = allAliases.stream()
+ .filter(a -> a.contains(TOKEN_MAX_LIFETIME_POSTFIX))
+ .map(a -> a.substring(0, a.indexOf(TOKEN_MAX_LIFETIME_POSTFIX)))
+ .collect(Collectors.toList());
} catch (AliasServiceException e) {
- log.errorEvictingTokens(e);
+ log.errorAccessingTokenState(e);
}
- return allAliases;
+
+ return (tokenIds != null ? tokenIds : Collections.emptyList());
}
+
+ private interface TokenState {
+ String getTokenId();
+ String getAlias();
+ String getAliasValue();
+ }
+
+ private static final class TokenMaxLifetime implements TokenState {
+ private String tokenId;
+ private long issueTime;
+ private long maxLifetime;
+
+ TokenMaxLifetime(String tokenId, long issueTime, long maxLifetime) {
+ this.tokenId = tokenId;
+ this.issueTime = issueTime;
+ this.maxLifetime = maxLifetime;
+ }
+
+ @Override
+ public String getTokenId() {
+ return tokenId;
+ }
+
+ @Override
+ public String getAlias() {
+ return tokenId + TOKEN_MAX_LIFETIME_POSTFIX;
+ }
+
+ @Override
+ public String getAliasValue() {
+ return String.valueOf(issueTime + maxLifetime);
+ }
+ }
+
+ private static final class TokenExpiration implements TokenState {
+ private String tokenId;
+ private long expiration;
+
+ TokenExpiration(String tokenId, long expiration) {
+ this.tokenId = tokenId;
+ this.expiration = expiration;
+ }
+
+ @Override
+ public String getTokenId() {
+ return tokenId;
+ }
+
+ @Override
+ public String getAlias() {
+ return tokenId;
+ }
+
+ @Override
+ public String getAliasValue() {
+ return String.valueOf(expiration);
+ }
+ }
+
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java
index 50ffb62..465e370 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java
@@ -77,6 +77,9 @@
private KeystoreService ks;
private GatewayConfig config;
+ private char[] cachedSigningKeyPassphrase;
+ private RSAPrivateKey signingKey;
+
static {
// Only standard RSA signature algorithms are accepted
// https://tools.ietf.org/html/rfc7518
@@ -135,6 +138,20 @@
return issueToken(p, audiences, algorithm, expires, null, null, null);
}
+ private RSAPrivateKey getSigningKey(final String signingKeystoreName,
+ final String signingKeystoreAlias,
+ final char[] signingKeystorePassphrase)
+ throws KeystoreServiceException, TokenServiceException {
+
+ if (signingKeystorePassphrase != null) {
+ return (RSAPrivateKey) ks.getSigningKey(signingKeystoreName,
+ getSigningKeyAlias(signingKeystoreAlias),
+ getSigningKeyPassphrase(signingKeystorePassphrase));
+ }
+
+ return signingKey;
+ }
+
@Override
public JWT issueToken(Principal p, List<String> audiences, String algorithm, long expires,
String signingKeystoreName, String signingKeystoreAlias, char[] signingKeystorePassphrase)
@@ -153,15 +170,8 @@
JWT token;
if (SUPPORTED_SIG_ALGS.contains(algorithm)) {
token = new JWTToken(algorithm, claimArray, audiences);
- char[] passphrase;
try {
- passphrase = getSigningKeyPassphrase(signingKeystorePassphrase);
- } catch (AliasServiceException e) {
- throw new TokenServiceException(e);
- }
- try {
- RSAPrivateKey key = (RSAPrivateKey) ks.getSigningKey(signingKeystoreName,
- getSigningKeyAlias(signingKeystoreAlias), passphrase);
+ RSAPrivateKey key = getSigningKey(signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase);
// allowWeakKey to not break existing 1024 bit certificates
JWSSigner signer = new RSASSASigner(key, true);
token.sign(signer);
@@ -176,12 +186,8 @@
return token;
}
- private char[] getSigningKeyPassphrase(char[] signingKeyPassphrase) throws AliasServiceException {
- if(signingKeyPassphrase != null) {
- return signingKeyPassphrase;
- }
-
- return as.getSigningKeyPassphrase();
+ private char[] getSigningKeyPassphrase(char[] signingKeyPassphrase) {
+ return (signingKeyPassphrase != null) ? signingKeyPassphrase : cachedSigningKeyPassphrase;
}
private String getSigningKeyAlias() {
@@ -275,10 +281,9 @@
}
// Ensure that the password for the signing key is available
- char[] passphrase;
try {
- passphrase = as.getSigningKeyPassphrase();
- if (passphrase == null) {
+ cachedSigningKeyPassphrase = as.getSigningKeyPassphrase();
+ if (cachedSigningKeyPassphrase == null) {
throw new ServiceLifecycleException(RESOURCES.signingKeyPassphraseNotAvailable(config.getSigningKeyPassphraseAlias()));
}
} catch (AliasServiceException e) {
@@ -306,13 +311,14 @@
// Ensure that the private signing keys is available
try {
- Key key = keystore.getKey(signingKeyAlias, passphrase);
+ Key key = keystore.getKey(signingKeyAlias, cachedSigningKeyPassphrase);
if (key == null) {
throw new ServiceLifecycleException(RESOURCES.privateSigningKeyNotFound(signingKeyAlias));
}
- else if (! (key instanceof RSAPrivateKey)) {
+ else if (! (key instanceof RSAPrivateKey)) {
throw new ServiceLifecycleException(RESOURCES.privateSigningKeyWrongType(signingKeyAlias));
}
+ signingKey = (RSAPrivateKey) key;
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
throw new ServiceLifecycleException(RESOURCES.privateSigningKeyNotFound(signingKeyAlias), e);
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
index ad1f892..2361269 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
@@ -26,13 +26,16 @@
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import java.time.Instant;
-import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
/**
* In-Memory authentication token state management implementation.
@@ -146,11 +149,21 @@
@Override
public long getTokenExpiration(final String tokenId) throws UnknownTokenException {
+ return getTokenExpiration(tokenId, true);
+ }
+
+ @Override
+ public long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException {
long expiration;
- validateToken(tokenId);
+ if (validate) {
+ validateToken(tokenId);
+ }
synchronized (tokenExpirations) {
+ if (!tokenExpirations.containsKey(tokenId)) {
+ throw new UnknownTokenException(tokenId);
+ }
expiration = tokenExpirations.get(tokenId);
}
@@ -222,7 +235,7 @@
}
/**
- * @param token token to check
+ * @param token
* @return false, if the service has previously stored the specified token; Otherwise, true.
*/
protected boolean isUnknown(final String token) {
@@ -237,23 +250,40 @@
protected void updateExpiration(final String tokenId, long expiration) {
synchronized (tokenExpirations) {
- tokenExpirations.replace(tokenId, expiration);
+ tokenExpirations.put(tokenId, expiration);
}
}
protected void removeToken(final String tokenId) throws UnknownTokenException {
validateToken(tokenId);
+ removeTokens(Collections.singleton(tokenId));
+ }
+
+ /**
+ * Bulk removal of the specified tokens.
+ *
+ * @param tokenIds The unique identifiers of the tokens whose state should be removed.
+ *
+ * @throws UnknownTokenException
+ */
+ protected void removeTokens(final Set<String> tokenIds) throws UnknownTokenException {
+ removeTokenState(tokenIds);
+ }
+
+ private void removeTokenState(final Set<String> tokenIds) {
synchronized (tokenExpirations) {
- tokenExpirations.remove(tokenId);
+ tokenExpirations.keySet().removeAll(tokenIds);
}
synchronized (maxTokenLifetimes) {
- maxTokenLifetimes.remove(tokenId);
+ maxTokenLifetimes.keySet().removeAll(tokenIds);
}
- log.removedTokenState(tokenId);
+ for (String tokenId : tokenIds) {
+ log.removedTokenState(tokenId);
+ }
}
protected boolean hasRemainingRenewals(final String tokenId, long renewInterval) {
- // Is the current time + 30-second buffer + the renewal interval is less than the max lifetime for the token?
+ // If the current time + buffer + the renewal interval is less than the max lifetime for the token?
return ((System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30) + renewInterval) < getMaxLifetime(tokenId));
}
@@ -297,16 +327,26 @@
* Method that deletes expired tokens based on the token timestamp.
*/
protected void evictExpiredTokens() {
+ Set<String> tokensToEvict = new HashSet<>();
+
for (final String tokenId : getTokens()) {
try {
if (needsEviction(tokenId)) {
log.evictToken(tokenId);
- removeToken(tokenId);
+ tokensToEvict.add(tokenId); // Add the token to the set of tokens to evict
}
} catch (final Exception e) {
log.failedExpiredTokenEviction(tokenId, e);
}
}
+
+ if (!tokensToEvict.isEmpty()) {
+ try {
+ removeTokens(tokensToEvict);
+ } catch (UnknownTokenException e) {
+ log.failedExpiredTokenEviction(e);
+ }
+ }
}
/**
@@ -319,16 +359,19 @@
*/
protected boolean needsEviction(final String tokenId) throws UnknownTokenException {
// If the expiration time(+ grace period) has already passed, it should be considered expired
- long expirationWithGrace = getTokenExpiration(tokenId) + TimeUnit.SECONDS.toMillis(tokenEvictionGracePeriod);
+ long expirationWithGrace = getTokenExpiration(tokenId, false) + TimeUnit.SECONDS.toMillis(tokenEvictionGracePeriod);
return (expirationWithGrace <= System.currentTimeMillis());
}
/**
* Get a list of tokens
*
- * @return List of tokens
+ * @return
*/
protected List<String> getTokens() {
- return new ArrayList<>(tokenExpirations.keySet());
+ synchronized (tokenExpirations) {
+ return tokenExpirations.keySet().stream().collect(Collectors.toList());
+ }
}
+
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
index aace46a..a85f3bf 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
@@ -45,6 +45,9 @@
@Message(level = MessageLevel.ERROR, text = "Failed to save state for token {0} : {1}")
void failedToSaveTokenState(String tokenId, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+ @Message(level = MessageLevel.ERROR, text = "Error accessing token state : {0}")
+ void errorAccessingTokenState(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
@Message(level = MessageLevel.ERROR, text = "Error accessing state for token {0} : {1}")
void errorAccessingTokenState(String tokenId, @StackTrace(level = MessageLevel.DEBUG) Exception e);
@@ -52,19 +55,46 @@
text = "Referencing the expiration in the token ({0}) because no state could not be found: {1}")
void permissiveTokenHandling(String tokenId, String errorMessage);
- @Message(level = MessageLevel.ERROR, text = "Failed to update expiration for token {1} : {1}")
+ @Message(level = MessageLevel.ERROR, text = "Failed to update expiration for token {0} : {1}")
void failedToUpdateTokenExpiration(String tokenId, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+ @Message(level = MessageLevel.ERROR, text = "Failed to create token state aliases : {0}")
+ void failedToCreateTokenStateAliases(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
@Message(level = MessageLevel.ERROR, text = "Failed to remove state for token {0} : {1}")
- void failedToRemoveTokenState(String tokenId, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+ void failedToRemoveTokenStateAliases(String tokenId, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.ERROR, text = "Failed to remove token state aliases : {0}")
+ void failedToRemoveTokenStateAliases(@StackTrace(level = MessageLevel.DEBUG) Exception e);
@Message(level = MessageLevel.ERROR, text = "Failed to evict expired token {0} : {1}")
void failedExpiredTokenEviction(String tokenId, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+ @Message(level = MessageLevel.ERROR, text = "Failed to evict expired tokens : {0}")
+ void failedExpiredTokenEviction(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
@Message(level = MessageLevel.INFO, text = "Evicting expired token {0}")
void evictToken(String tokenId);
@Message(level = MessageLevel.ERROR, text = "Error occurred evicting token {0}")
void errorEvictingTokens(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+ @Message(level = MessageLevel.INFO, text = "Creating token state aliases")
+ void creatingTokenStateAliases();
+
+ @Message(level = MessageLevel.DEBUG, text = "Creating token state aliases for {0}")
+ void creatingTokenStateAliases(String tokenId);
+
+ @Message(level = MessageLevel.INFO, text = "Created token state aliases for {0}")
+ void createdTokenStateAliases(String tokenId);
+
+ @Message(level = MessageLevel.INFO, text = "Removing token state aliases")
+ void removingTokenStateAliases();
+
+ @Message(level = MessageLevel.DEBUG, text = "Removing token state aliases for {0}")
+ void removingTokenStateAliases(String tokenId);
+
+ @Message(level = MessageLevel.INFO, text = "Removed token state aliases for {0}")
+ void removedTokenStateAliases(String tokenId);
+
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/CryptoServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/CryptoServiceTest.java
index c0dd1a1..dfcab8b 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/CryptoServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/CryptoServiceTest.java
@@ -33,6 +33,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -63,6 +64,13 @@
}
@Override
+ public void addAliasesForCluster(String clusterName, Map<String, String> credentials) throws AliasServiceException {
+ for (Map.Entry<String, String> credential : credentials.entrySet()) {
+ addAliasForCluster(clusterName, credential.getKey(), credential.getValue());
+ }
+ }
+
+ @Override
public char[] getPasswordFromAliasForCluster(String clusterName,
String alias) {
return "password".toCharArray();
@@ -97,6 +105,10 @@
}
@Override
+ public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
+ }
+
+ @Override
public List<String> getAliasesForCluster(String clusterName) {
return null;
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreServiceTest.java
index 8c2b1b5..dadb6e6 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreServiceTest.java
@@ -71,8 +71,10 @@
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
@@ -608,6 +610,59 @@
verify(masterService);
}
+ /**
+ * Test the bulk key removal method, which should only load the keystore file once, and subsequently write the
+ * keystore file only once, rather than once each per key.
+ */
+ @Test
+ public void testRemoveCredentialsForCluster() throws Exception {
+ char[] masterPassword = "master_password".toCharArray();
+
+ MasterService masterService = createMock(MasterService.class);
+ expect(masterService.getMasterSecret()).andReturn(masterPassword).anyTimes();
+
+ replay(masterService);
+
+ Path baseDir = testFolder.newFolder().toPath();
+ GatewayConfigImpl config = createGatewayConfig(baseDir);
+
+ CountingDefaultKeystoreService keystoreService = new CountingDefaultKeystoreService();
+ keystoreService.setMasterService(masterService);
+ keystoreService.init(config, Collections.emptyMap());
+
+ String clusterName = "cluster";
+
+ Map<String, String> testAliases = new HashMap<>();
+ testAliases.put("alias1", "value1");
+ testAliases.put("alias2", "value2");
+ testAliases.put("alias3", "value3");
+
+ Set<String> aliases = testAliases.keySet();
+
+ keystoreService.createCredentialStoreForCluster(clusterName);
+
+ for (String alias : aliases) {
+ keystoreService.addCredentialForCluster(clusterName, alias, testAliases.get(alias));
+ assertEquals(testAliases.get(alias), String.valueOf(keystoreService.getCredentialForCluster(clusterName, alias)));
+ }
+
+ // Clear the counts recorded from adding the credentials
+ keystoreService.clearCounts();
+
+ // Invoke the bulk removal method
+ keystoreService.removeCredentialsForCluster(clusterName, aliases);
+
+ // Validate the number of loads/writes of the keystore file
+ assertEquals("Expected only a single load of the keystore file.", 1, keystoreService.loadCount);
+ assertEquals("Expected only a single write to the keystore file.", 1, keystoreService.storeCount);
+
+ for (String alias : aliases) {
+ assertNull(keystoreService.getCredentialForCluster(clusterName, alias));
+ }
+
+ verify(masterService);
+ }
+
private void testAddSelfSignedCertForGateway(String hostname) throws Exception {
char[] masterPassword = "master_password".toCharArray();
@@ -732,4 +787,28 @@
keystoreService.writeKeyStoreToFile(keystore, keystoreFilePath, password);
}
+
+
+ private static class CountingDefaultKeystoreService extends DefaultKeystoreService {
+
+ int loadCount;
+ int storeCount;
+
+ void clearCounts() {
+ loadCount = 0;
+ storeCount = 0;
+ }
+
+ @Override
+ synchronized KeyStore loadKeyStore(Path keyStoreFilePath, String storeType, char[] password) throws KeystoreServiceException {
+ loadCount++;
+ return super.loadKeyStore(keyStoreFilePath, storeType, password);
+ }
+
+ @Override
+ synchronized void writeKeyStoreToFile(KeyStore keyStore, Path path, char[] password) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
+ storeCount++;
+ super.writeKeyStoreToFile(keyStore, path, password);
+ }
+ }
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTest.java
index a285c14..8675ced 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTest.java
@@ -28,8 +28,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import static org.apache.knox.gateway.services.security.impl.RemoteAliasService.REMOTE_ALIAS_SERVICE_TYPE;
import static org.easymock.EasyMock.capture;
@@ -334,4 +336,88 @@
Assert.assertTrue("Expected alias 'knox.test.alias' not found ",
aliases.contains(testAutoGeneratedpasswordAlias));
}
+
+ /**
+ * Test the bulk alias removal method.
+ */
+ @Test
+ public void testRemoveAliasesForCluster() throws Exception {
+ Map<String, String> remoteAliasConfigs = new HashMap<>();
+ remoteAliasConfigs.put(REMOTE_ALIAS_SERVICE_TYPE, "test");
+
+ GatewayConfig gc = EasyMock.createNiceMock(GatewayConfig.class);
+ EasyMock.expect(gc.isRemoteAliasServiceEnabled()).andReturn(true).anyTimes();
+ EasyMock.expect(gc.getRemoteAliasServiceConfiguration()).andReturn(remoteAliasConfigs).anyTimes();
+ EasyMock.replay(gc);
+
+ final String expectedClusterName = "sandbox";
+ final String expectedAlias = "knox.test.alias";
+ final String expectedPassword = "dummyPassword";
+
+ final int aliasCount = 5;
+ final Set<String> expectedAliases = new HashSet<>();
+ for (int i = 0; i < aliasCount ; i++) {
+ expectedAliases.add(expectedAlias + i);
+ }
+
+ final String expectedClusterNameDev = "development";
+ final String expectedAliasDev = "knox.test.alias.dev";
+ final String expectedPasswordDev = "otherDummyPassword";
+
+ final int devAliasCount = 3;
+ final Set<String> expectedDevAliases = new HashSet<>();
+ for (int i = 0; i < 3 ; i++) {
+ expectedDevAliases.add(expectedAliasDev + i);
+ }
+
+ final DefaultMasterService ms = EasyMock.createNiceMock(DefaultMasterService.class);
+ EasyMock.expect(ms.getMasterSecret()).andReturn("knox".toCharArray()).anyTimes();
+ EasyMock.replay(ms);
+
+ // Mock Alias Service
+ final DefaultAliasService defaultAlias = EasyMock.createNiceMock(DefaultAliasService.class);
+ // Captures for validating the alias creation for a generated topology
+ final Capture<String> capturedCluster = EasyMock.newCapture();
+ final Capture<String> capturedAlias = EasyMock.newCapture();
+ final Capture<String> capturedPwd = EasyMock.newCapture();
+
+ defaultAlias
+ .addAliasForCluster(capture(capturedCluster), capture(capturedAlias),
+ capture(capturedPwd));
+ EasyMock.expectLastCall().anyTimes();
+
+ // defaultAlias.getAliasesForCluster() never returns null
+ EasyMock.expect(defaultAlias.getAliasesForCluster(expectedClusterName))
+ .andReturn(new ArrayList<>()).anyTimes();
+ EasyMock.expect(defaultAlias.getAliasesForCluster(expectedClusterNameDev))
+ .andReturn(new ArrayList<>()).anyTimes();
+
+ EasyMock.replay(defaultAlias);
+
+ final RemoteAliasService remoteAliasService = new RemoteAliasService(defaultAlias, ms);
+ remoteAliasService.init(gc, Collections.emptyMap());
+ remoteAliasService.start();
+
+ // Put
+ for (String alias : expectedAliases) {
+ remoteAliasService.addAliasForCluster(expectedClusterName, alias, expectedPassword);
+ }
+ for (String alias : expectedDevAliases) {
+ remoteAliasService.addAliasForCluster(expectedClusterNameDev, alias, expectedPasswordDev);
+ }
+
+ Assert.assertEquals(aliasCount, remoteAliasService.getAliasesForCluster(expectedClusterName).size());
+ Assert.assertEquals(devAliasCount, remoteAliasService.getAliasesForCluster(expectedClusterNameDev).size());
+
+ // Invoke the bulk removal method for the dev cluster
+ remoteAliasService.removeAliasesForCluster(expectedClusterNameDev, expectedDevAliases);
+ List<String> aliasesDev = remoteAliasService.getAliasesForCluster(expectedClusterNameDev);
+ Assert.assertEquals("Expected 'knox.test.alias.dev' aliases to have been removed.", 0, aliasesDev.size());
+
+ // Invoke the bulk removal method for the sandbox cluster
+ remoteAliasService.removeAliasesForCluster(expectedClusterName, expectedAliases);
+ List<String> aliases = remoteAliasService.getAliasesForCluster(expectedClusterName);
+ Assert.assertEquals("Expected 'knox.test.alias' aliases to have been removed.", 0, aliases.size());
+ }
+
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTestProvider.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTestProvider.java
index 523c1e2..2a0dc05 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTestProvider.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/RemoteAliasServiceTestProvider.java
@@ -30,6 +30,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class RemoteAliasServiceTestProvider implements RemoteAliasServiceProvider {
@Override
@@ -60,11 +61,25 @@
}
@Override
+ public void addAliasesForCluster(String clusterName, Map<String, String> credentials) throws AliasServiceException {
+ for (Map.Entry<String, String> credential : credentials.entrySet()) {
+ addAliasForCluster(clusterName, credential.getKey(), credential.getValue());
+ }
+ }
+
+ @Override
public void removeAliasForCluster(String clusterName, String alias) {
aliases.getOrDefault(clusterName, new HashMap<>()).remove(alias);
}
@Override
+ public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
+ for (String alias : aliases) {
+ removeAliasForCluster(clusterName, alias);
+ }
+ }
+
+ @Override
public char[] getPasswordFromAliasForCluster(String clusterName, String alias) {
return aliases.getOrDefault(clusterName, new HashMap<>()).get(alias).toCharArray();
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasServiceTest.java
index 7a2a36e..bee945a 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/security/impl/ZookeeperRemoteAliasServiceTest.java
@@ -41,9 +41,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import static org.easymock.EasyMock.capture;
@@ -265,40 +267,116 @@
}
}
- @Test
- @Ignore("should be executed manually in case you'd like to measure how much time alias addition/fetch takes")
- public void testPerformance() throws Exception {
- final MasterService masterService = EasyMock.createNiceMock(MasterService.class);
- EasyMock.expect(masterService.getMasterSecret()).andReturn("ThisIsMyM4sterP4sW0r!d".toCharArray()).anyTimes();
- EasyMock.replay(masterService);
+ @Test
+ public void testRemoveAliasesForCluster() throws Exception {
+ final String expectedClusterName = "sandbox";
+ final String expectedAlias = "knox.test.alias";
+ final String expectedPassword = "dummyPassword";
- final DefaultKeystoreService keystoreService = new DefaultKeystoreService();
- keystoreService.init(gc, null);
- keystoreService.setMasterService(masterService);
-
- final int rounds = 11;
- final int numOfAliases = 200;
- final String cluster = "myTestCluster";
- for (int round = 0; round < rounds; round++) {
- //re-creating the alias service every time so that its cache is empty too
- final DefaultAliasService aliasService = new DefaultAliasService();
- aliasService.init(gc, null);
- aliasService.setMasterService(masterService);
- aliasService.setKeystoreService(keystoreService);
-
- RemoteConfigurationRegistryClientService clientService = (new ZooKeeperClientServiceProvider()).newInstance();
- clientService.setAliasService(aliasService);
- clientService.init(gc, Collections.emptyMap());
-
- final ZookeeperRemoteAliasService zkAlias = new ZookeeperRemoteAliasService(aliasService, masterService, clientService);
- zkAlias.init(gc, Collections.emptyMap());
- zkAlias.start();
- final long start = System.currentTimeMillis();
- for (int i = 0; i < numOfAliases; i++) {
- zkAlias.addAliasForCluster(cluster, "alias" + i, "password" + i);
- Assert.assertEquals("password" + i, new String(zkAlias.getPasswordFromAliasForCluster(cluster, "alias" + i)));
- }
- System.out.println(System.currentTimeMillis() - start);
- }
+ final int aliasCount = 5;
+ final Set<String> expectedAliases = new HashSet<>();
+ for (int i = 0; i < aliasCount ; i++) {
+ expectedAliases.add(expectedAlias + i);
}
+
+ final String expectedClusterNameDev = "development";
+ final String expectedAliasDev = "knox.test.alias.dev";
+ final String expectedPasswordDev = "otherDummyPassword";
+
+ final int devAliasCount = 3;
+ final Set<String> expectedDevAliases = new HashSet<>();
+ for (int i = 0; i < 3 ; i++) {
+ expectedDevAliases.add(expectedAliasDev + i);
+ }
+
+ // Mock Alias Service
+ final DefaultAliasService defaultAlias = EasyMock.createNiceMock(DefaultAliasService.class);
+ // Captures for validating the alias creation for a generated topology
+ final Capture<String> capturedCluster = EasyMock.newCapture();
+ final Capture<String> capturedAlias = EasyMock.newCapture();
+ final Capture<String> capturedPwd = EasyMock.newCapture();
+
+ defaultAlias.addAliasForCluster(capture(capturedCluster), capture(capturedAlias), capture(capturedPwd));
+ EasyMock.expectLastCall().anyTimes();
+
+ // defaultAlias.getAliasesForCluster() never returns null
+ EasyMock.expect(defaultAlias.getAliasesForCluster(expectedClusterName))
+ .andReturn(new ArrayList<>()).anyTimes();
+ EasyMock.expect(defaultAlias.getAliasesForCluster(expectedClusterNameDev))
+ .andReturn(new ArrayList<>()).anyTimes();
+
+ EasyMock.replay(defaultAlias);
+
+ final DefaultMasterService ms = EasyMock.createNiceMock(DefaultMasterService.class);
+ EasyMock.expect(ms.getMasterSecret()).andReturn("knox".toCharArray()).anyTimes();
+ EasyMock.replay(ms);
+
+ RemoteConfigurationRegistryClientService clientService = (new ZooKeeperClientServiceProvider()).newInstance();
+ clientService.setAliasService(defaultAlias);
+ clientService.init(gc, Collections.emptyMap());
+
+ final ZookeeperRemoteAliasService zkAlias = new ZookeeperRemoteAliasService(defaultAlias, ms, clientService);
+ zkAlias.init(gc, Collections.emptyMap());
+ zkAlias.start();
+
+ int originalSize = zkAlias.getAliasesForCluster(expectedClusterName).size();
+
+ // Put
+ for (String alias : expectedAliases) {
+ zkAlias.addAliasForCluster(expectedClusterName, alias, expectedPassword);
+ }
+ for (String alias : expectedDevAliases) {
+ zkAlias.addAliasForCluster(expectedClusterNameDev, alias, expectedPasswordDev);
+ }
+
+ Assert.assertEquals(originalSize + aliasCount, zkAlias.getAliasesForCluster(expectedClusterName).size());
+ Assert.assertEquals(devAliasCount, zkAlias.getAliasesForCluster(expectedClusterNameDev).size());
+
+ // Invoke the bulk removal method for the dev cluster
+ zkAlias.removeAliasesForCluster(expectedClusterNameDev, expectedDevAliases);
+ List<String> aliasesDev = zkAlias.getAliasesForCluster(expectedClusterNameDev);
+ Assert.assertEquals("Expected 'knox.test.alias.dev' aliases to have been removed.", 0, aliasesDev.size());
+
+ // Invoke the bulk removal method for the sandbox cluster
+ zkAlias.removeAliasesForCluster(expectedClusterName, expectedAliases);
+ List<String> aliases = zkAlias.getAliasesForCluster(expectedClusterName);
+ Assert.assertEquals("Expected 'knox.test.alias' aliases to have been removed.", originalSize, aliases.size());
+ }
+
+ @Test
+ @Ignore("should be executed manually in case you'd like to measure how much time alias addition/fetch takes")
+ public void testPerformance() throws Exception {
+ final MasterService masterService = EasyMock.createNiceMock(MasterService.class);
+ EasyMock.expect(masterService.getMasterSecret()).andReturn("ThisIsMyM4sterP4sW0r!d".toCharArray()).anyTimes();
+ EasyMock.replay(masterService);
+
+ final DefaultKeystoreService keystoreService = new DefaultKeystoreService();
+ keystoreService.init(gc, null);
+ keystoreService.setMasterService(masterService);
+
+ final int rounds = 11;
+ final int numOfAliases = 200;
+ final String cluster = "myTestCluster";
+ for (int round = 0; round < rounds; round++) {
+ //re-creating the alias service every time so that its cache is empty too
+ final DefaultAliasService aliasService = new DefaultAliasService();
+ aliasService.init(gc, null);
+ aliasService.setMasterService(masterService);
+ aliasService.setKeystoreService(keystoreService);
+
+ RemoteConfigurationRegistryClientService clientService = (new ZooKeeperClientServiceProvider()).newInstance();
+ clientService.setAliasService(aliasService);
+ clientService.init(gc, Collections.emptyMap());
+
+ final ZookeeperRemoteAliasService zkAlias = new ZookeeperRemoteAliasService(aliasService, masterService, clientService);
+ zkAlias.init(gc, Collections.emptyMap());
+ zkAlias.start();
+ final long start = System.currentTimeMillis();
+ for (int i = 0; i < numOfAliases; i++) {
+ zkAlias.addAliasForCluster(cluster, "alias" + i, "password" + i);
+ Assert.assertEquals("password" + i, new String(zkAlias.getPasswordFromAliasForCluster(cluster, "alias" + i)));
+ }
+ System.out.println(System.currentTimeMillis() - start);
+ }
+ }
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
index d982186..8254843 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
@@ -21,15 +21,370 @@
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.AliasServiceException;
import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.easymock.EasyMock;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.anyString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTest {
+ /**
+ * KNOX-2375
+ */
+ @Test
+ public void testBulkTokenStateEviction() throws Exception {
+ final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
+ final long maxTokenLifetime = evictionInterval * 3;
+
+ final Set<JWTToken> testTokens = new HashSet<>();
+ for (int i = 0; i < 10 ; i++) {
+ testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
+ }
+
+ List<String> testTokenStateAliases = new ArrayList<>();
+ for (JWTToken token : testTokens) {
+ String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM);
+ testTokenStateAliases.add(tokenId);
+ testTokenStateAliases.add(tokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
+ }
+
+ // Create a mock AliasService so we can verify that the expected bulk removal method is invoked when the token state
+ // reaper runs.
+ AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
+ EasyMock.expect(aliasService.getPasswordFromAliasForCluster(anyString(), anyString()))
+ .andReturn(String.valueOf(System.currentTimeMillis()).toCharArray())
+ .anyTimes();
+ EasyMock.expect(aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME)).andReturn(testTokenStateAliases).anyTimes();
+ // Expecting the bulk alias removal method to be invoked only once, rather than the individual alias removal method
+ // invoked twice for every expired token.
+ aliasService.removeAliasesForCluster(anyString(), anyObject());
+ EasyMock.expectLastCall().andVoid().once();
+
+ EasyMock.replay(aliasService);
+
+ AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+ tss.setAliasService(aliasService);
+ initTokenStateService(tss);
+
+ try {
+ tss.start();
+
+ // Add the expired tokens
+ for (JWTToken token : testTokens) {
+ tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
+ System.currentTimeMillis(),
+ token.getExpiresDate().getTime(),
+ maxTokenLifetime);
+ assertTrue("Expected the token to have expired.", tss.isExpired(token));
+ }
+
+ // Sleep to allow the eviction evaluation to be performed
+ Thread.sleep(evictionInterval + (evictionInterval / 2));
+ } finally {
+ tss.stop();
+ }
+
+ // Verify that the expected method was invoked
+ EasyMock.verify(aliasService);
+ }
+
+ @Test
+ public void testAddAndRemoveTokenIncludesCache() throws Exception {
+ final Set<JWTToken> testTokens = new HashSet<>();
+ for (int i = 0; i < 10 ; i++) {
+ testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
+ }
+
+ List<String> testTokenStateAliases = new ArrayList<>();
+ for (JWTToken token : testTokens) {
+ String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM);
+ testTokenStateAliases.add(tokenId);
+ testTokenStateAliases.add(tokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
+ }
+
+ // Create a mock AliasService so we can verify that the expected bulk removal method is invoked (and that the
+ // individual removal method is NOT invoked) when the token state reaper runs.
+ AliasService aliasService = EasyMock.createMock(AliasService.class);
+ EasyMock.expect(aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME)).andReturn(testTokenStateAliases).anyTimes();
+ // Expecting the bulk alias removal method to be invoked only once, rather than the individual alias removal method
+ // invoked twice for every expired token.
+ aliasService.removeAliasesForCluster(anyString(), anyObject());
+ EasyMock.expectLastCall().andVoid().once();
+
+ EasyMock.replay(aliasService);
+
+ AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+ tss.setAliasService(aliasService);
+ initTokenStateService(tss);
+
+ Field tokenExpirationsField = tss.getClass().getSuperclass().getDeclaredField("tokenExpirations");
+ tokenExpirationsField.setAccessible(true);
+ Map<String, Long> tokenExpirations = (Map<String, Long>) tokenExpirationsField.get(tss);
+
+ Field maxTokenLifetimesField = tss.getClass().getSuperclass().getDeclaredField("maxTokenLifetimes");
+ maxTokenLifetimesField.setAccessible(true);
+ Map<String, Long> maxTokenLifetimes = (Map<String, Long>) maxTokenLifetimesField.get(tss);
+
+ final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
+ final long maxTokenLifetime = evictionInterval * 3;
+
+ try {
+ tss.start();
+
+ // Add the expired tokens
+ for (JWTToken token : testTokens) {
+ tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
+ System.currentTimeMillis(),
+ token.getExpiresDate().getTime(),
+ maxTokenLifetime);
+ }
+
+ assertEquals("Expected the tokens to have been added in the base class cache.", 10, tokenExpirations.size());
+ assertEquals("Expected the tokens lifetimes to have been added in the base class cache.",
+ 10,
+ maxTokenLifetimes.size());
+
+ // Sleep to allow the eviction evaluation to be performed
+ Thread.sleep(evictionInterval + (evictionInterval / 4));
+ } finally {
+ tss.stop();
+ }
+
+ // Verify that the expected methods were invoked
+ EasyMock.verify(aliasService);
+
+ assertEquals("Expected the tokens to have been removed from the base class cache as a result of eviction.",
+ 0,
+ tokenExpirations.size());
+ assertEquals("Expected the tokens lifetimes to have been removed from the base class cache as a result of eviction.",
+ 0,
+ maxTokenLifetimes.size());
+ }
+
+ /**
+ * Verify that the token state reaper includes token state which has not been cached, so it's not left in the keystore
+ * forever.
+ */
+ @Test
+ public void testTokenEvictionIncludesUncachedAliases() throws Exception {
+ final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
+ final long maxTokenLifetime = evictionInterval * 3;
+
+ final Set<JWTToken> testTokens = new HashSet<>();
+ for (int i = 0; i < 10 ; i++) {
+ testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
+ }
+
+ List<String> testTokenStateAliases = new ArrayList<>();
+ for (JWTToken token : testTokens) {
+ testTokenStateAliases.add(token.getClaim(JWTToken.KNOX_ID_CLAIM));
+ testTokenStateAliases.add(token.getClaim(JWTToken.KNOX_ID_CLAIM) + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
+ }
+
+ // Add aliases for an uncached test token
+ final JWTToken uncachedToken = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
+ final String uncachedTokenId = uncachedToken.getClaim(JWTToken.KNOX_ID_CLAIM);
+ testTokenStateAliases.add(uncachedTokenId);
+ testTokenStateAliases.add(uncachedTokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
+ final long uncachedTokenExpiration = System.currentTimeMillis();
+ System.out.println("Uncached token ID: " + uncachedTokenId);
+
+ final Set<String> expectedTokensToEvict = new HashSet<>();
+ expectedTokensToEvict.addAll(testTokenStateAliases);
+ expectedTokensToEvict.add(uncachedTokenId);
+ expectedTokensToEvict.add(uncachedTokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
+
+ // Create a mock AliasService so we can verify that the expected bulk removal method is invoked (and that the
+ // individual removal method is NOT invoked) when the token state reaper runs.
+ AliasService aliasService = EasyMock.createMock(AliasService.class);
+ EasyMock.expect(aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME)).andReturn(testTokenStateAliases).anyTimes();
+ // Expecting the bulk alias removal method to be invoked only once, rather than the individual alias removal method
+ // invoked twice for every expired token.
+ aliasService.removeAliasesForCluster(anyString(), EasyMock.eq(expectedTokensToEvict));
+ EasyMock.expectLastCall().andVoid().once();
+ aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, uncachedTokenId);
+ EasyMock.expectLastCall().andReturn(String.valueOf(uncachedTokenExpiration).toCharArray()).once();
+
+ EasyMock.replay(aliasService);
+
+ AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+ tss.setAliasService(aliasService);
+ initTokenStateService(tss);
+
+ Field tokenExpirationsField = tss.getClass().getSuperclass().getDeclaredField("tokenExpirations");
+ tokenExpirationsField.setAccessible(true);
+ Map<String, Long> tokenExpirations = (Map<String, Long>) tokenExpirationsField.get(tss);
+
+ Field maxTokenLifetimesField = tss.getClass().getSuperclass().getDeclaredField("maxTokenLifetimes");
+ maxTokenLifetimesField.setAccessible(true);
+ Map<String, Long> maxTokenLifetimes = (Map<String, Long>) maxTokenLifetimesField.get(tss);
+
+ try {
+ tss.start();
+
+ // Add the expired tokens
+ for (JWTToken token : testTokens) {
+ tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
+ System.currentTimeMillis(),
+ token.getExpiresDate().getTime(),
+ maxTokenLifetime);
+ }
+
+ assertEquals("Expected the tokens to have been added in the base class cache.", 10, tokenExpirations.size());
+ assertEquals("Expected the tokens lifetimes to have been added in the base class cache.",
+ 10,
+ maxTokenLifetimes.size());
+
+ // Sleep to allow the eviction evaluation to be performed, but only one iteration
+ Thread.sleep(evictionInterval + (evictionInterval / 4));
+ } finally {
+ tss.stop();
+ }
+
+ // Verify that the expected methods were invoked
+ EasyMock.verify(aliasService);
+
+ assertEquals("Expected the tokens to have been removed from the base class cache as a result of eviction.",
+ 0,
+ tokenExpirations.size());
+ assertEquals("Expected the tokens lifetimes to have been removed from the base class cache as a result of eviction.",
+ 0,
+ maxTokenLifetimes.size());
+ }
+
+ @Test
+ public void testGetMaxLifetimeUsesCache() throws Exception {
+ AliasService aliasService = EasyMock.createMock(AliasService.class);
+
+ EasyMock.replay(aliasService);
+
+ AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+ tss.setAliasService(aliasService);
+ initTokenStateService(tss);
+
+ Field maxTokenLifetimesField = tss.getClass().getSuperclass().getDeclaredField("maxTokenLifetimes");
+ maxTokenLifetimesField.setAccessible(true);
+ Map<String, Long> maxTokenLifetimes = (Map<String, Long>) maxTokenLifetimesField.get(tss);
+
+ final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
+ final long maxTokenLifetime = evictionInterval * 3;
+
+ final Set<JWTToken> testTokens = new HashSet<>();
+ for (int i = 0; i < 10 ; i++) {
+ testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
+ }
+
+ try {
+ tss.start();
+
+ // Add the expired tokens
+ for (JWTToken token : testTokens) {
+ tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
+ System.currentTimeMillis(),
+ token.getExpiresDate().getTime(),
+ maxTokenLifetime);
+
+ }
+
+ assertEquals("Expected the tokens lifetimes to have been added in the base class cache.",
+ 10,
+ maxTokenLifetimes.size());
+
+ // Set the cache values to be different from the underlying alias value
+ final long updatedMaxLifetime = evictionInterval * 5;
+ for (Map.Entry<String, Long> entry : maxTokenLifetimes.entrySet()) {
+ entry.setValue(updatedMaxLifetime);
+ }
+
+ // Verify that we get the cache value back
+ for (String tokenId : maxTokenLifetimes.keySet()) {
+ assertEquals("Expected the cached max lifetime, rather than the alias value",
+ updatedMaxLifetime,
+ tss.getMaxLifetime(tokenId));
+ }
+ } finally {
+ tss.stop();
+ }
+
+ // Verify that the expected methods were invoked
+ EasyMock.verify(aliasService);
+ }
+
+ @Test
+ public void testUpdateExpirationUsesCache() throws Exception {
+ AliasService aliasService = EasyMock.createMock(AliasService.class);
+ aliasService.addAliasForCluster(anyString(), anyString(), anyString());
+ EasyMock.expectLastCall().andVoid().atLeastOnce();
+ aliasService.removeAliasForCluster(anyString(), anyObject());
+ EasyMock.expectLastCall().andVoid().atLeastOnce();
+
+ EasyMock.replay(aliasService);
+
+ AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+ tss.setAliasService(aliasService);
+ initTokenStateService(tss);
+
+ Field tokenExpirationsField = tss.getClass().getSuperclass().getDeclaredField("tokenExpirations");
+ tokenExpirationsField.setAccessible(true);
+ Map<String, Long> tokenExpirations = (Map<String, Long>) tokenExpirationsField.get(tss);
+
+ final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
+ final long maxTokenLifetime = evictionInterval * 3;
+
+ final Set<JWTToken> testTokens = new HashSet<>();
+ for (int i = 0; i < 10 ; i++) {
+ testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
+ }
+
+ try {
+ tss.start();
+
+ // Add the expired tokens
+ for (JWTToken token : testTokens) {
+ tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
+ System.currentTimeMillis(),
+ token.getExpiresDate().getTime(),
+ maxTokenLifetime);
+ }
+
+ assertEquals("Expected the tokens expirations to have been added in the base class cache.",
+ 10,
+ tokenExpirations.size());
+
+ // Set the cache values to be different from the underlying alias value
+ final long updatedExpiration = System.currentTimeMillis();
+ for (String tokenId : tokenExpirations.keySet()) {
+ tss.updateExpiration(tokenId, updatedExpiration);
+ }
+
+ for (String tokenId : tokenExpirations.keySet()) {
+ assertEquals("Expected the cached expiration to have been updated.",
+ updatedExpiration,
+ tss.getTokenExpiration(tokenId, false));
+ }
+ } finally {
+ tss.stop();
+ }
+
+ // Verify that the expected methods were invoked
+ EasyMock.verify(aliasService);
+ }
+
@Override
protected TokenStateService createTokenStateService() {
AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
@@ -69,6 +424,13 @@
}
@Override
+ public void addAliasesForCluster(String clusterName, Map<String, String> credentials) throws AliasServiceException {
+ for (Map.Entry<String, String> credential : credentials.entrySet()) {
+ addAliasForCluster(clusterName, credential.getKey(), credential.getValue());
+ }
+ }
+
+ @Override
public void removeAliasForCluster(String clusterName, String alias) throws AliasServiceException {
if (clusterAliases.containsKey(clusterName)) {
clusterAliases.get(clusterName).remove(alias);
@@ -76,6 +438,13 @@
}
@Override
+ public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
+ for (String alias : aliases) {
+ removeAliasForCluster(clusterName, alias);
+ }
+ }
+
+ @Override
public char[] getPasswordFromAliasForCluster(String clusterName, String alias) throws AliasServiceException {
char[] value = null;
if (clusterAliases.containsKey(clusterName)) {
@@ -143,4 +512,31 @@
}
}
+ @Override
+ protected void addToken(TokenStateService tss, String tokenId, long issueTime, long expiration, long maxLifetime) {
+ super.addToken(tss, tokenId, issueTime, expiration, maxLifetime);
+
+ // Persist any unpersisted token state aliases
+ triggerAliasPersistence(tss);
+ }
+
+ @Override
+ protected void addToken(TokenStateService tss, JWTToken token, long issueTime) {
+ super.addToken(tss, token, issueTime);
+
+ // Persist any unpersisted token state aliases
+ triggerAliasPersistence(tss);
+ }
+
+ private void triggerAliasPersistence(TokenStateService tss) {
+ if (tss instanceof AliasBasedTokenStateService) {
+ try {
+ Method m = tss.getClass().getDeclaredMethod("persistTokenState");
+ m.setAccessible(true);
+ m.invoke(tss);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java
index 1cbdf9b..84817aa 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java
@@ -80,6 +80,7 @@
ta.setKeystoreService(ks);
ta.init(config, new HashMap<>());
+ ta.start();
JWT token = ta.issueToken(principal, "RS256");
assertEquals("KNOXSSO", token.getIssuer());
@@ -126,6 +127,7 @@
ta.setKeystoreService(ks);
ta.init(config, new HashMap<>());
+ ta.start();
JWT token = ta.issueToken(principal, "https://login.example.com", "RS256");
assertEquals("KNOXSSO", token.getIssuer());
@@ -173,6 +175,7 @@
ta.setKeystoreService(ks);
ta.init(config, new HashMap<>());
+ ta.start();
JWT token = ta.issueToken(principal, null, "RS256");
assertEquals("KNOXSSO", token.getIssuer());
@@ -219,6 +222,7 @@
ta.setKeystoreService(ks);
ta.init(config, new HashMap<>());
+ ta.start();
JWT token = ta.issueToken(principal, "RS512");
assertEquals("KNOXSSO", token.getIssuer());
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
index 1935da8..27d38bf 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
@@ -61,7 +61,7 @@
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
- tss.addToken(token, System.currentTimeMillis());
+ addToken(tss, token, System.currentTimeMillis());
long expiration = tss.getTokenExpiration(TokenUtils.getTokenId(token));
assertEquals(token.getExpiresDate().getTime(), expiration);
}
@@ -91,7 +91,7 @@
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
- tss.addToken(token, System.currentTimeMillis());
+ addToken(tss, token, System.currentTimeMillis());
long expiration = tss.getTokenExpiration(TokenUtils.getTokenId(token));
assertEquals(token.getExpiresDate().getTime(), expiration);
@@ -105,7 +105,7 @@
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
- tss.addToken(token, System.currentTimeMillis());
+ addToken(tss, token, System.currentTimeMillis());
assertFalse(tss.isExpired(token));
}
@@ -114,38 +114,35 @@
final JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
- tss.addToken(token, System.currentTimeMillis());
+ addToken(tss, token, System.currentTimeMillis());
assertTrue(tss.isExpired(token));
}
-
@Test(expected = UnknownTokenException.class)
public void testIsExpired_Revoked() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
- tss.addToken(token, System.currentTimeMillis());
+ addToken(tss, token, System.currentTimeMillis());
assertFalse("Expected the token to be valid.", tss.isExpired(token));
tss.revokeToken(token);
tss.isExpired(token);
}
-
@Test
public void testRenewal() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
// Add the expired token
- tss.addToken(token, System.currentTimeMillis());
+ addToken(tss, token, System.currentTimeMillis());
assertTrue("Expected the token to have expired.", tss.isExpired(token));
tss.renewToken(token);
assertFalse("Expected the token to have been renewed.", tss.isExpired(token));
}
-
@Test
public void testRenewalBeyondMaxLifetime() throws Exception {
long maxLifetimeDuration = TimeUnit.SECONDS.toMillis(5);
@@ -176,10 +173,11 @@
final long maxTokenLifetime = TimeUnit.MINUTES.toMillis(2);
// Add the expired token
- tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
- System.currentTimeMillis(),
- token.getExpiresDate().getTime(),
- maxTokenLifetime);
+ addToken(tss,
+ token.getClaim(JWTToken.KNOX_ID_CLAIM),
+ System.currentTimeMillis(),
+ token.getExpiresDate().getTime(),
+ maxTokenLifetime);
assertTrue("Expected the token to have expired.", tss.isExpired(token));
// Sleep to allow the eviction evaluation to be performed prior to the maximum token lifetime
@@ -201,10 +199,11 @@
try {
tss.start();
// Add the expired token
- tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
- System.currentTimeMillis(),
- token.getExpiresDate().getTime(),
- maxTokenLifetime);
+ addToken(tss,
+ token.getClaim(JWTToken.KNOX_ID_CLAIM),
+ System.currentTimeMillis(),
+ token.getExpiresDate().getTime(),
+ maxTokenLifetime);
assertTrue("Expected the token to have expired.", tss.isExpired(token));
// Sleep to allow the eviction evaluation to be performed
@@ -299,4 +298,12 @@
token.sign(signer);
return token;
}
+
+ protected void addToken(TokenStateService tss, JWTToken token, long issueTime) {
+ tss.addToken(token, issueTime);
+ }
+
+ protected void addToken(TokenStateService tss, String tokenId, long issueTime, long expiration, long maxLifetime) {
+ tss.addToken(tokenId, issueTime, expiration, maxLifetime);
+ }
}
diff --git a/gateway-service-hashicorp-vault/src/main/java/org/apache/knox/gateway/backend/hashicorp/vault/HashicorpVaultAliasService.java b/gateway-service-hashicorp-vault/src/main/java/org/apache/knox/gateway/backend/hashicorp/vault/HashicorpVaultAliasService.java
index e9e3851..05bc3dd 100644
--- a/gateway-service-hashicorp-vault/src/main/java/org/apache/knox/gateway/backend/hashicorp/vault/HashicorpVaultAliasService.java
+++ b/gateway-service-hashicorp-vault/src/main/java/org/apache/knox/gateway/backend/hashicorp/vault/HashicorpVaultAliasService.java
@@ -38,6 +38,7 @@
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
+import java.util.Set;
public class HashicorpVaultAliasService implements AliasService {
public static final String TYPE = "hashicorp.vault";
@@ -102,6 +103,13 @@
}
@Override
+ public void addAliasesForCluster(String clusterName, Map<String, String> credentials) throws AliasServiceException {
+ for (Map.Entry<String, String> credential : credentials.entrySet()) {
+ addAliasForCluster(clusterName, credential.getKey(), credential.getValue());
+ }
+ }
+
+ @Override
public void removeAliasForCluster(String clusterName, String alias) throws AliasServiceException {
// Delete is by default a soft delete with versioned KV in Vault
// https://learn.hashicorp.com/vault/secrets-management/sm-versioned-kv#step-6-permanently-delete-data
@@ -120,6 +128,13 @@
}
@Override
+ public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
+ for (String alias : aliases) {
+ removeAliasForCluster(clusterName, alias);
+ }
+ }
+
+ @Override
public char[] getPasswordFromAliasForCluster(String clusterName, String alias) throws AliasServiceException {
try {
Versioned<Map<String, Object>> mapVersioned = vault.get(getPath(clusterName, alias));
diff --git a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
index e10459b..afc6738 100644
--- a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
+++ b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
@@ -158,7 +158,9 @@
assertTrue(authority.verifyToken(parsedToken));
}
- // KNOX-2266
+ /**
+ * KNOX-2266
+ */
@Test
public void testConcurrentGetToken() throws Exception {
@@ -1254,6 +1256,11 @@
}
@Override
+ public long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException {
+ return 0;
+ }
+
+ @Override
public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 2ebd9ae..7825e7b 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -674,6 +674,12 @@
long getKnoxTokenEvictionGracePeriod();
/**
+ * Return the configured token state alias persistence interval (in seconds).
+ * @return Token state alias persistence interval in seconds.
+ */
+ long getKnoxTokenStateAliasPersistenceInterval();
+
+ /**
* @return the list of topologies that should be hidden on Knox homepage
*/
Set<String> getHiddenTopologiesOnHomepage();
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/AliasService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/AliasService.java
index 79d69d1..730d932 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/AliasService.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/AliasService.java
@@ -19,6 +19,8 @@
import java.security.cert.Certificate;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.apache.knox.gateway.services.Service;
@@ -31,9 +33,15 @@
void addAliasForCluster(String clusterName, String alias,
String value) throws AliasServiceException;
+ void addAliasesForCluster(String clusterName,
+ Map<String, String> credentials) throws AliasServiceException;
+
void removeAliasForCluster(String clusterName, String alias)
throws AliasServiceException;
+ void removeAliasesForCluster(String clusterName, Set<String> aliases)
+ throws AliasServiceException;
+
char[] getPasswordFromAliasForCluster(String clusterName,
String alias) throws AliasServiceException;
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java
index d16766d..95c1f71 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java
@@ -21,6 +21,8 @@
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
+import java.util.Map;
+import java.util.Set;
public interface KeystoreService {
@@ -64,8 +66,12 @@
void addCredentialForCluster(String clusterName, String alias, String key) throws KeystoreServiceException;
+ void addCredentialsForCluster(String clusterName, Map<String, String> credentials) throws KeystoreServiceException;
+
void removeCredentialForCluster(String clusterName, String alias) throws KeystoreServiceException;
+ void removeCredentialsForCluster(String clusterName, Set<String> aliases) throws KeystoreServiceException;
+
char[] getCredentialForCluster(String clusterName, String alias) throws KeystoreServiceException;
String getKeystorePath();
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
index d89607c..602cf53 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
@@ -152,4 +152,14 @@
* @return The token's expiration time in milliseconds.
*/
long getTokenExpiration(String tokenId) throws UnknownTokenException;
+
+ /**
+ *
+ * @param tokenId The token unique identifier.
+ * @param validate Flag indicating whether the token needs to be validated.
+ *
+ * @return The token's expiration time in milliseconds.
+ */
+ long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException;
+
}
diff --git a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index 8c2a1da..e3dbeaa 100644
--- a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -791,6 +791,11 @@
}
@Override
+ public long getKnoxTokenStateAliasPersistenceInterval() {
+ return 0;
+ }
+
+ @Override
public Set<String> getHiddenTopologiesOnHomepage() {
return Collections.emptySet();
}