This is an automated email from the ASF dual-hosted git repository. smolnar82 pushed a commit to branch knox_idf in repository https://gitbox.apache.org/repos/asf/knox.git
commit 02b354c1e541718c92330c6bf1978a10ee0b982f Author: Sandor Molnar <[email protected]> AuthorDate: Wed Jul 1 21:13:37 2026 +0200 KNOX-3370: Eliminate deprecated TokenStateService implementations (#1289) (cherry picked from commit 8f65731a5be596fbdd80217f25b69d04fd395902) --- .../services/factory/TokenStateServiceFactory.java | 25 +- .../token/impl/AliasBasedTokenStateService.java | 721 ---------------- .../token/impl/JournalBasedTokenStateService.java | 221 ----- .../token/impl/ZookeeperTokenStateService.java | 185 ---- .../knox/gateway/util/TokenMigrationTool.java | 9 +- .../factory/TokenStateServiceFactoryTest.java | 26 - .../impl/AliasBasedTokenStateServiceTest.java | 943 --------------------- .../impl/JournalBasedTokenStateServiceTest.java | 331 -------- .../token/impl/ZookeeperTokenStateServiceTest.java | 242 ------ 9 files changed, 12 insertions(+), 2691 deletions(-) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java index 4934c1f5f..332d257d3 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java @@ -17,12 +17,6 @@ */ package org.apache.knox.gateway.services.factory; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; - -import java.util.Collection; -import java.util.Map; - import org.apache.knox.gateway.GatewayMessages; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; @@ -30,12 +24,15 @@ import org.apache.knox.gateway.services.GatewayServices; import org.apache.knox.gateway.services.Service; import org.apache.knox.gateway.services.ServiceLifecycleException; import org.apache.knox.gateway.services.ServiceType; -import org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService; import org.apache.knox.gateway.services.token.impl.DefaultTokenStateService; import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService; import org.apache.knox.gateway.services.token.impl.JDBCTokenStateService; -import org.apache.knox.gateway.services.token.impl.JournalBasedTokenStateService; -import org.apache.knox.gateway.services.token.impl.ZookeeperTokenStateService; + +import java.util.Collection; +import java.util.Map; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; public class TokenStateServiceFactory extends AbstractServiceFactory { @@ -50,13 +47,6 @@ public class TokenStateServiceFactory extends AbstractServiceFactory { service = useDerbyDatabaseTokenStateService(gatewayServices, gatewayConfig, options); } else if (matchesImplementation(implementation, DefaultTokenStateService.class)) { service = new DefaultTokenStateService(); - } else if (matchesImplementation(implementation, AliasBasedTokenStateService.class)) { - service = new AliasBasedTokenStateService(); - ((AliasBasedTokenStateService) service).setAliasService(getAliasService(gatewayServices)); - } else if (matchesImplementation(implementation, JournalBasedTokenStateService.class)) { - service = new JournalBasedTokenStateService(); - } else if (matchesImplementation(implementation, ZookeeperTokenStateService.class)) { - service = new ZookeeperTokenStateService(gatewayServices); } else if (matchesImplementation(implementation, JDBCTokenStateService.class)) { try { service = new JDBCTokenStateService(); @@ -95,7 +85,6 @@ public class TokenStateServiceFactory extends AbstractServiceFactory { @Override protected Collection<String> getKnownImplementations() { - return unmodifiableList(asList(DefaultTokenStateService.class.getName(), AliasBasedTokenStateService.class.getName(), JournalBasedTokenStateService.class.getName(), - ZookeeperTokenStateService.class.getName(), JDBCTokenStateService.class.getName(), DerbyDBTokenStateService.class.getName())); + return unmodifiableList(asList(DefaultTokenStateService.class.getName(), JDBCTokenStateService.class.getName(), DerbyDBTokenStateService.class.getName())); } } 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 deleted file mode 100644 index 18902eed8..000000000 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java +++ /dev/null @@ -1,721 +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.knox.gateway.services.token.impl; - -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -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 java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.apache.knox.gateway.config.GatewayConfig; -import org.apache.knox.gateway.services.ServiceLifecycleException; -import org.apache.knox.gateway.services.security.AliasService; -import org.apache.knox.gateway.services.security.AliasServiceException; -import org.apache.knox.gateway.services.security.impl.DefaultKeystoreService; -import org.apache.knox.gateway.services.security.token.TokenMetadata; -import org.apache.knox.gateway.services.security.token.UnknownTokenException; -import org.apache.knox.gateway.services.token.TokenStateServiceStatistics; -import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory; -import org.apache.knox.gateway.services.token.state.JournalEntry; -import org.apache.knox.gateway.services.token.state.TokenStateJournal; -import org.apache.knox.gateway.util.ExecutorServiceUtils; -import org.apache.knox.gateway.util.Tokens; - -/** - * A TokenStateService implementation based on the AliasService. - * - * @deprecated Since 2.1.0 - */ -public class AliasBasedTokenStateService extends AbstractPersistentTokenStateService implements TokenStatePeristerMonitorListener { - - static final String TOKEN_ALIAS_SUFFIX_DELIM = "--"; - public static final String TOKEN_ISSUE_TIME_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM + "iss"; - public static final String TOKEN_MAX_LIFETIME_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM + "max"; - public static final String TOKEN_META_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM + "meta"; - - protected AliasService aliasService; - - protected long statePersistenceInterval = TimeUnit.SECONDS.toSeconds(15); - - private ScheduledExecutorService statePersistenceScheduler; - - private final Set<TokenState> unpersistedState = new HashSet<>(); - - private final AtomicBoolean readyForEviction = new AtomicBoolean(false); - - private TokenStateJournal journal; - - private Path gatewayCredentialsFilePath; - - public void setAliasService(AliasService aliasService) { - this.aliasService = aliasService; - } - - @Override - public void init(final GatewayConfig config, final Map<String, String> options) throws ServiceLifecycleException { - log.deprecatedServiceUsage(this.getClass().getCanonicalName()); - super.init(config, options); - if (aliasService == null) { - throw new ServiceLifecycleException("The required AliasService reference has not been set."); - } - - try { - // Initialize the token state journal - journal = TokenStateJournalFactory.create(config); - - // Load any persisted journal entries, and add them to the unpersisted state collection - List<JournalEntry> entries = journal.get(); - for (JournalEntry entry : entries) { - String id = entry.getTokenId(); - try { - long issueTime = Long.parseLong(entry.getIssueTime()); - long expiration = Long.parseLong(entry.getExpiration()); - long maxLifetime = Long.parseLong(entry.getMaxLifetime()); - - // Add the token state to memory - super.addToken(id, issueTime, expiration, maxLifetime); - - synchronized (unpersistedState) { - // The max lifetime entry is added by way of the call to super.addToken(), - // so only need to add the expiration entry here. - unpersistedState.add(new TokenExpiration(id, expiration)); - } - } catch (Exception e) { - log.failedToLoadJournalEntry(Tokens.getTokenIDDisplayText(id), e); - } - } - } catch (IOException e) { - throw new ServiceLifecycleException("Failed to load persisted state from the token state journal", e); - } - - statePersistenceInterval = config.getKnoxTokenStateAliasPersistenceInterval(); - - if (tokenStateServiceStatistics != null) { - this.gatewayCredentialsFilePath = Paths.get(config.getGatewayKeystoreDir()).resolve(AliasService.NO_CLUSTER_NAME + DefaultKeystoreService.CREDENTIALS_SUFFIX + config.getCredentialStoreType().toLowerCase(Locale.ROOT)); - tokenStateServiceStatistics.setGatewayCredentialsFileSize(this.gatewayCredentialsFilePath.toFile().length()); - } - } - - @Override - public void start() throws ServiceLifecycleException { - super.start(); - if (statePersistenceInterval > 0) { - //first schedule event; only happen if the feature is not disabled via persistence interval settings - scheduleTokenStatePersistence(); - } - - // Loading ALL entries from __gateway-credentials.jceks could be VERY time-consuming (it took a bit more than 19 minutes to load 12k aliases - // during my tests). - // Therefore, it's safer to do it in a background thread than just make the service start hang until it's finished - final ExecutorService gatewayCredentialsLoader = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("PersistenceStoreLoader").build()); - gatewayCredentialsLoader.execute(this::loadTokenAliasesFromPersistenceStore); - } - - protected void loadTokenAliasesFromPersistenceStore() { - try { - log.loadingTokenAliasesFromPersistenceStore(); - final long start = System.currentTimeMillis(); - final Map<String, char[]> passwordAliasMap = aliasService.getPasswordsForGateway(); - String alias, tokenId; - long expiration, maxLifeTime; - int count = 0; - for (Map.Entry<String, char[]> passwordAliasMapEntry : passwordAliasMap.entrySet()) { - alias = passwordAliasMapEntry.getKey(); - if (alias.endsWith(TOKEN_MAX_LIFETIME_POSTFIX)) { - // This token state service implementation persists 4 aliases in __gateway-credentials.jceks (see persistTokenState below): - // - an alias which maps a token ID to its expiration time - // - an alias with '--max' postfix which maps the maximum lifetime of the token identified by the 1st alias - // - an alias with '--iss' postfix which maps the issue time of the token - // - an alias with '-meta' postfix which maps an arbitrary metadata of the token - // Given this, we should check aliases ending with '--max' and calculate the token ID from this alias. - // If all aliases were blindly processed we would end-up handling aliases that were not persisted via this token state service - // implementation -> facing error(s) when trying to parse the expiration/maxLifeTime values and irrelevant data would be loaded in the - // in-memory collections in the parent class - tokenId = alias.substring(0, alias.indexOf(TOKEN_MAX_LIFETIME_POSTFIX)); - expiration = convertCharArrayToLong(passwordAliasMap.get(tokenId)); - maxLifeTime = convertCharArrayToLong(passwordAliasMapEntry.getValue()); - super.updateExpiration(tokenId, expiration); - super.setMaxLifetime(tokenId, maxLifeTime); - count+=2; - } else if (alias.endsWith(TOKEN_META_POSTFIX)) { - tokenId = alias.substring(0, alias.indexOf(TOKEN_META_POSTFIX)); - super.addMetadata(tokenId, TokenMetadata.fromJSON(new String(passwordAliasMapEntry.getValue()))); - } else if (alias.endsWith(TOKEN_ISSUE_TIME_POSTFIX)) { - tokenId = alias.substring(0, alias.indexOf(TOKEN_ISSUE_TIME_POSTFIX)); - setIssueTimeInMemory(tokenId, convertCharArrayToLong(passwordAliasMapEntry.getValue())); - } - - // log some progress (it's very useful in case a huge amount of token related aliases in __gateway-credentials.jceks) - if (count % 100 == 0) { - log.loadedTokenAliasesFromPersistenceStore(count, System.currentTimeMillis() - start); - } - } - log.loadedTokenAliasesFromPersistenceStore(count * 2, System.currentTimeMillis() - start); //count is multiplied by two: tokenId + tokenId--max - } catch (AliasServiceException e) { - log.errorWhileLoadingTokenAliasesFromPersistenceStore(e.getMessage(), e); - } finally { - readyForEviction.set(true); - } - } - - @Override - protected boolean readyForEviction() { - return readyForEviction.get(); - } - - @Override - public void stop() throws ServiceLifecycleException { - super.stop(); - if (statePersistenceScheduler != null) { - statePersistenceScheduler.shutdown(); - } - - // Make an attempt to persist any unpersisted token state before shutting down - persistTokenState(); - } - - private void scheduleTokenStatePersistence() { - if (statePersistenceScheduler != null) { - ExecutorServiceUtils.shutdownAndAwaitTermination(statePersistenceScheduler, 10, TimeUnit.SECONDS); - } - statePersistenceScheduler = Executors.newSingleThreadScheduledExecutor(new BasicThreadFactory.Builder().namingPattern("TokenStatePerister-%d").build()); - final ScheduledFuture<?> persistTokenStateTask = statePersistenceScheduler.scheduleAtFixedRate(this::persistTokenState, statePersistenceInterval, statePersistenceInterval, TimeUnit.SECONDS); - log.runningTokenStateAliasePersisterTask(statePersistenceInterval, TimeUnit.SECONDS.toString()); - final TokenStatePersisterMonitor taskMonitor = new TokenStatePersisterMonitor(persistTokenStateTask, this); - taskMonitor.startMonitor(); - } - - @Override - public void onTokenStatePeristerTaskError(Throwable error) { - scheduleTokenStatePersistence(); - } - - 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()); - } - - for (String tokenId: tokenIds) { - log.creatingTokenStateAliases(Tokens.getTokenIDDisplayText(tokenId)); - } - - // Write aliases in a batch - if (!aliases.isEmpty()) { - log.creatingTokenStateAliases(); - - try { - aliasService.addAliasesForCluster(AliasService.NO_CLUSTER_NAME, aliases); - if (tokenStateServiceStatistics != null) { - tokenStateServiceStatistics.interactKeystore(TokenStateServiceStatistics.KeystoreInteraction.SAVE_ALIAS); - tokenStateServiceStatistics.setGatewayCredentialsFileSize(this.gatewayCredentialsFilePath.toFile().length()); - } - for (String tokenId : tokenIds) { - log.createdTokenStateAliases(Tokens.getTokenIDDisplayText(tokenId)); - // After the aliases have been successfully persisted, remove their associated state from the journal - try { - journal.remove(tokenId); - } catch (IOException e) { - log.failedToRemoveJournalEntry(Tokens.getTokenIDDisplayText(tokenId), e); - } - } - } catch (AliasServiceException e) { - log.failedToCreateTokenStateAliases(e); - synchronized (unpersistedState) { - unpersistedState.addAll(processing); // Restore the unpersisted state objects so they can be attempted later - } - } - } - } - - @Override - public void addToken(final String tokenId, - long issueTime, - long expiration, - long maxLifetimeDuration) { - super.addToken(tokenId, issueTime, expiration, maxLifetimeDuration); - - synchronized (unpersistedState) { - unpersistedState.add(new TokenExpiration(tokenId, expiration)); - } - - try { - journal.add(tokenId, issueTime, expiration, maxLifetimeDuration, null); - } catch (IOException e) { - log.failedToAddJournalEntry(Tokens.getTokenIDDisplayText(tokenId), e); - } - } - - @Override - protected void setIssueTime(String tokenId, long issueTime) { - synchronized (unpersistedState) { - unpersistedState.add(new TokenIssueTime(tokenId, issueTime)); - } - setIssueTimeInMemory(tokenId, issueTime); - } - - protected void setIssueTimeInMemory(String tokenId, long issueTime) { - super.setIssueTime(tokenId, issueTime); - } - - @Override - protected void setMaxLifetime(final String tokenId, long issueTime, long maxLifetimeDuration) { - super.setMaxLifetime(tokenId, issueTime, maxLifetimeDuration); - synchronized (unpersistedState) { - unpersistedState.add(new TokenMaxLifetime(tokenId, issueTime, maxLifetimeDuration)); - } - } - - @Override - protected long getMaxLifetime(final String tokenId) { - 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 = getPasswordUsingAliasService(tokenId + TOKEN_MAX_LIFETIME_POSTFIX); - if (maxLifetimeStr != null) { - result = convertCharArrayToLong(maxLifetimeStr); - } - } catch (AliasServiceException e) { - log.errorAccessingTokenState(Tokens.getTokenIDDisplayText(tokenId), e); - } - } - return result; - } - - protected char[] getPasswordUsingAliasService(String alias) throws AliasServiceException { - char[] password = aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, alias); - if (tokenStateServiceStatistics != null) { - tokenStateServiceStatistics.interactKeystore(TokenStateServiceStatistics.KeystoreInteraction.GET_PASSWORD); - } - return password; - } - - protected long convertCharArrayToLong(char[] charArray) { - return Long.parseLong(new String(charArray)); - } - - @Override - public long getTokenIssueTime(String tokenId) throws UnknownTokenException { - // Check the in-memory collection first, to avoid costly keystore access when possible - try { - // check the in-memory cache first - return super.getTokenIssueTime(tokenId); - } catch (UnknownTokenException e) { - // It's not in memory - } - - // If there is no associated state in the in-memory cache, proceed to check the alias service - long issueTime = 0; - try { - char[] issueTimeStr = getPasswordUsingAliasService(tokenId + TOKEN_ISSUE_TIME_POSTFIX); - if (issueTimeStr == null) { - throw new UnknownTokenException(tokenId); - } - issueTime = convertCharArrayToLong(issueTimeStr); - // Update the in-memory cache to avoid subsequent keystore look-ups for the same state - setIssueTimeInMemory(tokenId, issueTime); - } catch (UnknownTokenException e) { - throw e; - } catch (Exception e) { - log.errorAccessingTokenState(Tokens.getTokenIDDisplayText(tokenId), e); - } - - return issueTime; - } - - @Override - public long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException { - // Check the in-memory collection first, to avoid costly keystore access when possible - try { - // If the token identifier is valid, and the associated state is available from the in-memory cache, then - // return the expiration from there. - return super.getTokenExpiration(tokenId, validate); - } catch (UnknownTokenException e) { - // It's not in memory - } - - if (validate) { - validateToken(tokenId); - } - - // If there is no associated state in the in-memory cache, proceed to check the alias service - long expiration = 0; - try { - char[] expStr = getPasswordUsingAliasService(tokenId); - if (expStr == null) { - throw new UnknownTokenException(tokenId); - } - expiration = Long.parseLong(new String(expStr)); - // Update the in-memory cache to avoid subsequent keystore look-ups for the same state - super.updateExpiration(tokenId, expiration); - } catch (UnknownTokenException e) { - throw e; - } catch (Exception e) { - log.errorAccessingTokenState(Tokens.getTokenIDDisplayText(tokenId), e); - } - - return expiration; - } - - @Override - protected boolean isUnknown(final String tokenId) { - boolean isUnknown = super.isUnknown(tokenId); - - // If it's not in the cache, then check the underlying alias - if (isUnknown) { - try { - isUnknown = (getPasswordUsingAliasService(tokenId) == null); - } catch (AliasServiceException e) { - log.errorAccessingTokenState(Tokens.getTokenIDDisplayText(tokenId), e); - } - } - return isUnknown; - } - - @Override - protected void removeTokens(Set<String> tokenIds) { - - // If any of the token IDs is represented among the unpersisted state, remove the associated state - synchronized (unpersistedState) { - List<TokenState> unpersistedToRemove = new ArrayList<>(); - for (TokenState state : unpersistedState) { - if (tokenIds.contains(state.getTokenId())) { - unpersistedToRemove.add(state); - } - } - unpersistedState.removeAll(unpersistedToRemove); - } - - // Add the max lifetime, metadata and issue time 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); - aliasesToRemove.add(tokenId + TOKEN_META_POSTFIX); - aliasesToRemove.add(tokenId + TOKEN_ISSUE_TIME_POSTFIX); - } - - if (!aliasesToRemove.isEmpty()) { - log.removingTokenStateAliases(); - try { - aliasService.removeAliasesForCluster(AliasService.NO_CLUSTER_NAME, aliasesToRemove); - if (tokenStateServiceStatistics != null) { - tokenStateServiceStatistics.interactKeystore(TokenStateServiceStatistics.KeystoreInteraction.REMOVE_ALIAS); - tokenStateServiceStatistics.setGatewayCredentialsFileSize(this.gatewayCredentialsFilePath.toFile().length()); - } - log.removedTokenStateAliases(String.join(", ", Tokens.getDisplayableTokenIDsText(tokenIds))); - } catch (AliasServiceException e) { - log.failedToRemoveTokenStateAliases(e); - } - } - - removeTokensFromMemory(tokenIds); - } - - protected void removeTokensFromMemory(Set<String> tokenIds) { - super.removeTokens(tokenIds); - } - - @Override - protected void updateExpiration(final String tokenId, long expiration) { - //Update in-memory - updateExpirationInMemory(tokenId, expiration); - - //Update the in-memory representation of unpersisted states that will be processed by the state persistence thread - synchronized (unpersistedState) { - unpersistedState.add(new TokenExpiration(tokenId, expiration)); - } - } - - protected void updateExpirationInMemory(final String tokenId, long expiration) { - super.updateExpiration(tokenId, expiration); - } - - @Override - public void addMetadata(String tokenId, TokenMetadata metadata) { - addMetadataInMemory(tokenId, metadata); - try { - final JournalEntry entry = journal.get(tokenId); - if (entry != null) { - journal.add(entry.getTokenId(), Long.parseLong(entry.getIssueTime()), Long.parseLong(entry.getExpiration()), Long.parseLong(entry.getMaxLifetime()), metadata); - } - } catch (IOException e) { - log.failedToAddJournalEntry(Tokens.getTokenIDDisplayText(tokenId), e); - } - - synchronized (unpersistedState) { - unpersistedState.add(new TokenMetadataState(tokenId, metadata)); - } - } - - protected void addMetadataInMemory(String tokenId, TokenMetadata metadata) { - super.addMetadata(tokenId, metadata); - } - - @Override - public TokenMetadata getTokenMetadata(String tokenId) throws UnknownTokenException { - TokenMetadata tokenMetadata = null; - try { - tokenMetadata = super.getTokenMetadata(tokenId); - } catch (UnknownTokenException e) { - // This is expected if the metadata is not yet part of the in-memory record. In this case, the metadata will - // be retrieved from the alias store. - } - - if (tokenMetadata == null) { - try { - final char[] tokenMetadataAliasValue = getPasswordUsingAliasService(tokenId + TOKEN_META_POSTFIX); - if (tokenMetadataAliasValue != null) { - tokenMetadata = TokenMetadata.fromJSON(new String(tokenMetadataAliasValue)); - } else { - throw new UnknownTokenException(tokenId); - } - } catch (AliasServiceException e) { - log.errorAccessingTokenState(Tokens.getTokenIDDisplayText(tokenId), e); - } - } - return tokenMetadata; - } - - enum TokenStateType { - EXP(1), MAX(2), META(3), ISS(4); - - private final int id; - - TokenStateType(int id) { - this.id = id; - } - } - - interface TokenState { - String getTokenId(); - String getAlias(); - String getAliasValue(); - TokenStateType getType(); - } - - 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); - } - - @Override - public TokenStateType getType() { - return TokenStateType.MAX; - } - - @Override - public int hashCode() { - return new HashCodeBuilder().append(tokenId).append(getType().id).toHashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (obj == this) { - return true; - } - if (obj.getClass() != getClass()) { - return false; - } - final TokenMaxLifetime rhs = (TokenMaxLifetime) obj; - return new EqualsBuilder().append(this.tokenId, rhs.tokenId).append(this.getType().id, rhs.getType().id).isEquals(); - } - } - - 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); - } - - @Override - public TokenStateType getType() { - return TokenStateType.EXP; - } - - @Override - public int hashCode() { - return new HashCodeBuilder().append(tokenId).append(getType().id).toHashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (obj == this) { - return true; - } - if (obj.getClass() != getClass()) { - return false; - } - final TokenExpiration rhs = (TokenExpiration) obj; - return new EqualsBuilder().append(this.tokenId, rhs.tokenId).append(this.getType().id, rhs.getType().id).isEquals(); - } - } - - private static final class TokenIssueTime implements TokenState { - private String tokenId; - private long issueTime; - - TokenIssueTime(String tokenId, long issueTime) { - this.tokenId = tokenId; - this.issueTime = issueTime; - } - - @Override - public String getTokenId() { - return tokenId; - } - - @Override - public String getAlias() { - return tokenId + TOKEN_ISSUE_TIME_POSTFIX; - } - - @Override - public String getAliasValue() { - return String.valueOf(issueTime); - } - - @Override - public TokenStateType getType() { - return TokenStateType.ISS; - } - - @Override - public int hashCode() { - return new HashCodeBuilder().append(tokenId).append(getType().id).toHashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (obj == this) { - return true; - } - if (obj.getClass() != getClass()) { - return false; - } - final TokenIssueTime rhs = (TokenIssueTime) obj; - return new EqualsBuilder().append(this.tokenId, rhs.tokenId).append(this.getType().id, rhs.getType().id).isEquals(); - } - } - - private static final class TokenMetadataState implements TokenState { - - private final String tokenId; - private final TokenMetadata metadata; - - TokenMetadataState(String tokenId, TokenMetadata metadata) { - this.tokenId = tokenId; - this.metadata = metadata; - } - - @Override - public String getTokenId() { - return tokenId; - } - - @Override - public String getAlias() { - return tokenId + TOKEN_META_POSTFIX; - } - - @Override - public String getAliasValue() { - return metadata.toJSON(); - } - - @Override - public TokenStateType getType() { - return TokenStateType.META; - } - } - -} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java deleted file mode 100644 index e21852b14..000000000 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java +++ /dev/null @@ -1,221 +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.knox.gateway.services.token.impl; - -import org.apache.knox.gateway.config.GatewayConfig; -import org.apache.knox.gateway.services.ServiceLifecycleException; -import org.apache.knox.gateway.services.security.token.TokenMetadata; -import org.apache.knox.gateway.services.security.token.UnknownTokenException; -import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory; -import org.apache.knox.gateway.services.token.state.JournalEntry; -import org.apache.knox.gateway.services.token.state.TokenStateJournal; -import org.apache.knox.gateway.util.Tokens; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * @deprecated Since 2.1.0 - */ -public class JournalBasedTokenStateService extends AbstractPersistentTokenStateService { - - private TokenStateJournal journal; - - @Override - public void init(final GatewayConfig config, final Map<String, String> options) throws ServiceLifecycleException { - log.deprecatedServiceUsage(this.getClass().getCanonicalName()); - super.init(config, options); - - try { - // Initialize the token state journal - journal = TokenStateJournalFactory.create(config); - - // Load any persisted journal entries, and add them to the in-memory collection - List<JournalEntry> entries = journal.get(); - for (JournalEntry entry : entries) { - String id = entry.getTokenId(); - try { - long issueTime = Long.parseLong(entry.getIssueTime()); - long expiration = Long.parseLong(entry.getExpiration()); - long maxLifetime = Long.parseLong(entry.getMaxLifetime()); - - // Add the token state to memory - super.addToken(id, issueTime, expiration, maxLifetime); - - } catch (Exception e) { - log.failedToLoadJournalEntry(Tokens.getTokenIDDisplayText(id), e); - } - } - } catch (IOException e) { - throw new ServiceLifecycleException("Failed to load persisted state from the token state journal", e); - } - } - - @Override - public void addToken(final String tokenId, long issueTime, long expiration, long maxLifetimeDuration) { - super.addToken(tokenId, issueTime, expiration, maxLifetimeDuration); - - try { - journal.add(tokenId, issueTime, expiration, maxLifetimeDuration, null); - } catch (IOException e) { - log.failedToAddJournalEntry(Tokens.getTokenIDDisplayText(tokenId), e); - } - } - - @Override - public long getTokenIssueTime(String tokenId) throws UnknownTokenException { - try { - // Check the in-memory collection first, to avoid file access when possible - return super.getTokenIssueTime(tokenId); - } catch (UnknownTokenException e) { - // It's not in memory - } - - validateToken(tokenId); - - // If there is no associated state in the in-memory cache, proceed to check the journal - long issueTime = 0; - try { - JournalEntry entry = journal.get(tokenId); - if (entry == null) { - throw new UnknownTokenException(tokenId); - } - - issueTime = Long.parseLong(entry.getIssueTime()); - } catch (IOException e) { - log.failedToLoadJournalEntry(e); - } - - return issueTime; - } - - @Override - public long getTokenExpiration(final String tokenId, boolean validate) throws UnknownTokenException { - // Check the in-memory collection first, to avoid file access when possible - try { - // If the token identifier is valid, and the associated state is available from the in-memory cache, then - // return the expiration from there. - return super.getTokenExpiration(tokenId, validate); - } catch (UnknownTokenException e) { - // It's not in memory - } - - if (validate) { - validateToken(tokenId); - } - - // If there is no associated state in the in-memory cache, proceed to check the journal - long expiration = 0; - try { - JournalEntry entry = journal.get(tokenId); - if (entry == null) { - throw new UnknownTokenException(tokenId); - } - - expiration = Long.parseLong(entry.getExpiration()); - super.addToken(tokenId, - Long.parseLong(entry.getIssueTime()), - expiration, - Long.parseLong(entry.getMaxLifetime())); - } catch (IOException e) { - log.failedToLoadJournalEntry(e); - } - - return expiration; - } - - @Override - protected long getMaxLifetime(final String tokenId) { - long result = super.getMaxLifetime(tokenId); - - // If there is no result from the in-memory collection, proceed to check the journal - if (result < 1L) { - try { - JournalEntry entry = journal.get(tokenId); - if (entry == null) { - throw new UnknownTokenException(tokenId); - } - result = Long.parseLong(entry.getMaxLifetime()); - super.setMaxLifetime(tokenId, Long.parseLong(entry.getIssueTime()), result); - } catch (Exception e) { - log.failedToLoadJournalEntry(e); - } - } - return result; - } - - @Override - protected void removeTokens(final Set<String> tokenIds) { - super.removeTokens(tokenIds); - try { - journal.remove(tokenIds); - } catch (IOException e) { - log.failedToRemoveJournalEntries(e); - } - } - - @Override - protected void updateExpiration(final String tokenId, long expiration) { - super.updateExpiration(tokenId, expiration); - try { - JournalEntry entry = journal.get(tokenId); - if (entry == null) { - log.journalEntryNotFound(Tokens.getTokenIDDisplayText(tokenId)); - } else { - // Adding will overwrite the existing journal entry, thus updating it with the new expiration - journal.add(entry.getTokenId(), - Long.parseLong(entry.getIssueTime()), - expiration, - Long.parseLong(entry.getMaxLifetime()), - entry.getTokenMetadata()); - } - } catch (IOException e) { - log.errorAccessingTokenState(e); - } - } - - @Override - protected boolean isUnknown(final String tokenId) { - JournalEntry entry = null; - try { - entry = journal.get(tokenId); - } catch (IOException e) { - log.errorAccessingTokenState(e); - } - - return (entry == null); - } - - @Override - public void addMetadata(String tokenId, TokenMetadata metadata) { - super.addMetadata(tokenId, metadata); - try { - JournalEntry entry = journal.get(tokenId); - if (entry == null) { - log.journalEntryNotFound(Tokens.getTokenIDDisplayText(tokenId)); - } else { - journal.add(entry.getTokenId(), Long.parseLong(entry.getIssueTime()), Long.parseLong(entry.getExpiration()), Long.parseLong(entry.getMaxLifetime()), metadata); - } - } catch (IOException e) { - log.errorAccessingTokenState(e); - } - } -} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java deleted file mode 100644 index 7fdc3000d..000000000 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java +++ /dev/null @@ -1,185 +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.knox.gateway.services.token.impl; - -import static org.apache.knox.gateway.services.ServiceType.ALIAS_SERVICE; - -import java.time.Instant; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.apache.knox.gateway.config.GatewayConfig; -import org.apache.knox.gateway.services.GatewayServices; -import org.apache.knox.gateway.services.ServiceLifecycleException; -import org.apache.knox.gateway.services.factory.AliasServiceFactory; -import org.apache.knox.gateway.services.security.AliasServiceException; -import org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService; -import org.apache.knox.gateway.services.security.token.TokenMetadata; -import org.apache.knox.gateway.services.token.RemoteTokenStateChangeListener; -import org.apache.knox.gateway.util.Tokens; - -/** - * A Zookeeper Token State Service is actually an Alias based TSS where the 'alias service' happens to be the 'zookeeper' implementation. - * This means the only important thing that should be overridden here is the init method where the underlying alias service is configured - * properly. - * - * @deprecated Since 2.1.0 - */ -public class ZookeeperTokenStateService extends AliasBasedTokenStateService implements RemoteTokenStateChangeListener { - - // Constants for token aliases - needed for test compatibility - public static final String TOKEN_MAX_LIFETIME_POSTFIX = AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX; - public static final String TOKEN_META_POSTFIX = AliasBasedTokenStateService.TOKEN_META_POSTFIX; - - private final GatewayServices gatewayServices; - private final AliasServiceFactory aliasServiceFactory; - - public ZookeeperTokenStateService(GatewayServices gatewayServices) { - this(gatewayServices, new AliasServiceFactory()); - } - - public ZookeeperTokenStateService(GatewayServices gatewayServices, AliasServiceFactory aliasServiceFactory) { - this.gatewayServices = gatewayServices; - this.aliasServiceFactory = aliasServiceFactory; - } - - @Override - public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException { - log.deprecatedServiceUsage(this.getClass().getCanonicalName()); - - final Object createdService = aliasServiceFactory.create(gatewayServices, ALIAS_SERVICE, config, options, - ZookeeperRemoteAliasService.class.getName()); - - if (createdService == null) { - throw new ServiceLifecycleException("aliasServiceFactory.create() returned null - cannot create ZookeeperRemoteAliasService"); - } - - if (!(createdService instanceof ZookeeperRemoteAliasService)) { - throw new ServiceLifecycleException("aliasServiceFactory.create() returned unexpected type: " + createdService.getClass().getName() + - ", expected: ZookeeperRemoteAliasService"); - } - - final ZookeeperRemoteAliasService zookeeperAliasService = (ZookeeperRemoteAliasService) createdService; - - - options.put(ZookeeperRemoteAliasService.OPTION_NAME_SHOULD_CREATE_TOKENS_SUB_NODE, "true"); - options.put(ZookeeperRemoteAliasService.OPTION_NAME_SHOULD_USE_LOCAL_ALIAS, "false"); - - - zookeeperAliasService.registerRemoteTokenStateChangeListener(this); - zookeeperAliasService.init(config, options); - super.setAliasService(zookeeperAliasService); - super.init(config, options); - - options.remove(ZookeeperRemoteAliasService.OPTION_NAME_SHOULD_CREATE_TOKENS_SUB_NODE); - options.remove(ZookeeperRemoteAliasService.OPTION_NAME_SHOULD_USE_LOCAL_ALIAS); - } - - @Override - protected void loadTokenAliasesFromPersistenceStore() { - // NOP : registering 'knox/security/topology' child entry listener in ZKRemoteAliasService ends-up reading existing ZK nodes - // and with the help of RemoteTokenStateChangeListener notifications in-memory collections will be populated - // without loading them here directly - } - - @Override - protected boolean readyForEviction() { - return true; - } - - @Override - protected char[] getPasswordUsingAliasService(String alias) throws AliasServiceException { - char[] password = super.getPasswordUsingAliasService(alias); - - if (password == null) { - password = retry(alias); - } - return password; - } - - /* - * In HA scenarios, it might happen, that node1 generated a token but the state - * persister thread saves that token in ZK a bit later. If there is a subsequent - * call to this token on another node - e.g. node2 - before it's persisted in ZK - * the token would be considered unknown. (see CDPD-22225) - * - * To avoid this issue, the ZK token state service should retry to fetch the - * token from ZK in every second until the token is found or the number of - * retries exceeded the configured persistence interval - */ - private char[] retry(String alias) throws AliasServiceException { - char[] password = null; - final Instant timeLimit = Instant.now().plusSeconds(statePersistenceInterval).plusSeconds(1); // an addition of 1 second as grace period - - while (password == null && timeLimit.isAfter(Instant.now())) { - try { - TimeUnit.SECONDS.sleep(1); - log.retryZkFetchAlias(getDisplayableAliasText(alias)); - password = super.getPasswordUsingAliasService(alias); - } catch (InterruptedException e) { - log.failedRetryZkFetchAlias(getDisplayableAliasText(alias), e.getMessage(), e); - } - } - return password; - } - - @Override - public void onChanged(String alias, String updatedState) { - processAlias(alias, updatedState); - log.onRemoteTokenStateChanged(getDisplayableAliasText(alias)); - } - - @Override - public void onRemoved(String alias) { - final String tokenId = getTokenIdFromAlias(alias); - removeTokensFromMemory(Collections.singleton(tokenId)); - log.onRemoteTokenStateRemoval(getDisplayableAliasText(alias)); - } - - private void processAlias(String alias, String value) { - if (!ZookeeperRemoteAliasService.TOKENS_SUB_NODE_NAME.equals(alias)) { - try { - final String tokenId = getTokenIdFromAlias(alias); - if (alias.endsWith(TOKEN_MAX_LIFETIME_POSTFIX)) { - final long maxLifeTime = Long.parseLong(value); - setMaxLifetime(tokenId, maxLifeTime); - } else if (alias.endsWith(TOKEN_META_POSTFIX)) { - addMetadataInMemory(tokenId, TokenMetadata.fromJSON(value)); - } else if (alias.endsWith(TOKEN_ISSUE_TIME_POSTFIX)) { - setIssueTimeInMemory(tokenId, Long.parseLong(value)); - } else { - final long expiration = Long.parseLong(value); - updateExpirationInMemory(tokenId, expiration); - } - } catch (Throwable e) { - log.errorWhileProcessingTokenAlias(getDisplayableAliasText(alias), e.getMessage(), e); - } - } - } - - private String getTokenIdFromAlias(final String alias) { - return alias.contains(TOKEN_ALIAS_SUFFIX_DELIM) ? alias.substring(0, alias.indexOf(TOKEN_ALIAS_SUFFIX_DELIM)) : alias; - } - - private String getDisplayableAliasText(final String alias) { - String tokenId = getTokenIdFromAlias(alias); - String suffix = alias.length() > tokenId.length() ? alias.substring(tokenId.length()) : ""; - return Tokens.getTokenIDDisplayText(tokenId) + suffix; - } -} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/util/TokenMigrationTool.java b/gateway-server/src/main/java/org/apache/knox/gateway/util/TokenMigrationTool.java index 78dda075e..33d4bdc83 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/util/TokenMigrationTool.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/util/TokenMigrationTool.java @@ -17,10 +17,6 @@ */ package org.apache.knox.gateway.util; -import static org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService.TOKEN_ISSUE_TIME_POSTFIX; -import static org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX; -import static org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService.TOKEN_META_POSTFIX; - import java.io.PrintStream; import java.util.Arrays; import java.util.HashMap; @@ -42,6 +38,11 @@ public class TokenMigrationTool { private static final TokenStateServiceMessages LOG = MessagesFactory.get(TokenStateServiceMessages.class); + private static final String TOKEN_ALIAS_SUFFIX_DELIM = "--"; + private static final String TOKEN_ISSUE_TIME_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM + "iss"; + private static final String TOKEN_MAX_LIFETIME_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM + "max"; + private static final String TOKEN_META_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM + "meta"; + private final AliasService aliasService; private final TokenStateService tokenStateService; private final PrintStream out; diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactoryTest.java index c33ecd22a..a412aa429 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactoryTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactoryTest.java @@ -21,11 +21,8 @@ import static org.junit.Assert.assertTrue; import org.apache.knox.gateway.services.ServiceType; import org.apache.knox.gateway.services.security.token.TokenStateService; -import org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService; import org.apache.knox.gateway.services.token.impl.DefaultTokenStateService; import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService; -import org.apache.knox.gateway.services.token.impl.JournalBasedTokenStateService; -import org.apache.knox.gateway.services.token.impl.ZookeeperTokenStateService; import org.junit.Test; public class TokenStateServiceFactoryTest extends ServiceFactoryTest { @@ -60,29 +57,6 @@ public class TokenStateServiceFactoryTest extends ServiceFactoryTest { } } - @Test - public void shouldReturnAliasBasedTokenStateService() throws Exception { - initConfig(); - final TokenStateService tokenStateService = (TokenStateService) serviceFactory.create(gatewayServices, ServiceType.TOKEN_STATE_SERVICE, gatewayConfig, - options, AliasBasedTokenStateService.class.getName()); - assertTrue(tokenStateService instanceof AliasBasedTokenStateService); - assertTrue(isAliasServiceSet(tokenStateService)); - } - - @Test - public void shouldReturnJournalTokenStateService() throws Exception { - initConfig(); - assertTrue(serviceFactory.create(gatewayServices, ServiceType.TOKEN_STATE_SERVICE, gatewayConfig, options, - JournalBasedTokenStateService.class.getName()) instanceof JournalBasedTokenStateService); - } - - @Test - public void shouldReturnZookeeperTokenStateService() throws Exception { - initConfig(); - assertTrue(serviceFactory.create(gatewayServices, ServiceType.TOKEN_STATE_SERVICE, gatewayConfig, options, - ZookeeperTokenStateService.class.getName()) instanceof ZookeeperTokenStateService); - } - @Test public void shouldReturnDerbyDatabaseTokenStateService() throws Exception { TokenStateService tokenStateService = null; 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 deleted file mode 100644 index f6971f483..000000000 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java +++ /dev/null @@ -1,943 +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.knox.gateway.services.token.impl; - -import org.apache.knox.gateway.config.GatewayConfig; -import org.apache.knox.gateway.services.ServiceLifecycleException; -import org.apache.knox.gateway.services.security.AbstractAliasService; -import org.apache.knox.gateway.services.security.AliasService; -import org.apache.knox.gateway.services.security.AliasServiceException; -import org.apache.knox.gateway.services.security.token.TokenMetadata; -import org.apache.knox.gateway.services.security.token.TokenStateService; -import org.apache.knox.gateway.services.security.token.impl.JWTToken; -import org.apache.knox.gateway.services.token.state.JournalEntry; -import org.apache.knox.gateway.services.token.state.TokenStateJournal; -import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory; -import org.easymock.EasyMock; -import org.junit.Ignore; -import org.junit.Test; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -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.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import static org.easymock.EasyMock.anyObject; -import static org.easymock.EasyMock.anyString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTest { - - private Long tokenStatePersistenceInterval = TimeUnit.SECONDS.toMillis(15); - - @Override - protected long getTokenStatePersistenceInterval() { - return (tokenStatePersistenceInterval != null) ? tokenStatePersistenceInterval : super.getTokenStatePersistenceInterval(); - } - - /* - * 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(); - - //expecting this call when loading credentials from the keystore on startup - EasyMock.expect(aliasService.getPasswordsForGateway()).andReturn(Collections.emptyMap()).anyTimes(); - - 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); - tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), new TokenMetadata("alice")); - 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 int TOKEN_COUNT = 10; - - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; i++) { - testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60))); - } - - Set<String> testTokenStateAliases = new HashSet<>(); - for (JWTToken token : testTokens) { - String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM); - testTokenStateAliases.add(tokenId); - testTokenStateAliases.add(tokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX); - testTokenStateAliases.add(tokenId + AliasBasedTokenStateService.TOKEN_META_POSTFIX); - testTokenStateAliases.add(tokenId + AliasBasedTokenStateService.TOKEN_ISSUE_TIME_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(new ArrayList<>(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((EasyMock.eq(AliasService.NO_CLUSTER_NAME)), EasyMock.eq(testTokenStateAliases)); - EasyMock.expectLastCall().andVoid().once(); - - //expecting this call when loading credentials from the keystore on startup - EasyMock.expect(aliasService.getPasswordsForGateway()).andReturn(Collections.emptyMap()).anyTimes(); - - EasyMock.replay(aliasService); - - AliasBasedTokenStateService tss = new AliasBasedTokenStateService(); - tss.setAliasService(aliasService); - initTokenStateService(tss); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(tss, 2); - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss, 2); - Map<String, Map<String, TokenMetadata>> metadata = getMetadataMapField(tss, 2); - Map<String, Long> tokenIssueTimes = getTokenIssueTimesField(tss, 2); - - 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); - tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), new TokenMetadata("alice")); - } - - assertEquals("Expected the tokens to have been added in the base class cache.", TOKEN_COUNT, tokenExpirations.size()); - assertEquals("Expected the tokens lifetimes to have been added in the base class cache.", - TOKEN_COUNT, - maxTokenLifetimes.size()); - assertEquals("Expected the token metadata to have been added in the base class cache.", TOKEN_COUNT, metadata.size()); - assertEquals("Expected the token issue times to have been added in the base class cache.", TOKEN_COUNT, tokenIssueTimes.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()); - assertEquals("Expected the token metadata to have been removed from the base class cache as a result of eviction.", - 0, - metadata.size()); - assertEquals("Expected the token issue times to have been removed from the base class cache as a result of eviction.", - 0, - tokenIssueTimes.size()); - } - - /* - * Verify that the token state reaper includes token state which has not been cached, so it's not left in the keystore - * forever. - */ - @Ignore("I'm not sure if this is a valid use case since we have everything in the cache when eviction takes place") - @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(); - //expecting this call when loading credentials from the keystore on startup - EasyMock.expect(aliasService.getPasswordsForGateway()).andReturn(Collections.emptyMap()).anyTimes(); - - EasyMock.replay(aliasService); - - AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); - tss.setAliasService(aliasService); - initTokenStateService(tss); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(tss); - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(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); - aliasService.addAliasesForCluster(anyString(), anyObject()); - EasyMock.expectLastCall().once(); // Expecting this during shutdown - - //expecting this call when loading credentials from the keystore on startup - EasyMock.expect(aliasService.getPasswordsForGateway()).andReturn(Collections.emptyMap()).anyTimes(); - - EasyMock.replay(aliasService); - - AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); - tss.setAliasService(aliasService); - initTokenStateService(tss); - - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(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 { - final AliasService aliasService = EasyMock.createMock(AliasService.class); - // Neither addAliasForCluster nor removeAliasForCluster should be called because updating expiration should happen in memory and let the - // background persistence job done its job - aliasService.addAliasesForCluster(anyString(), anyObject()); - EasyMock.expectLastCall().andVoid().once(); // Expecting this during shutdown - - //expecting this call when loading credentials from the keystore on startup - EasyMock.expect(aliasService.getPasswordsForGateway()).andReturn(Collections.emptyMap()).anyTimes(); - EasyMock.replay(aliasService); - - AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); - tss.setAliasService(aliasService); - initTokenStateService(tss); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(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); - } - - // Invoking with true/false validation flags as it should not affect if values are coming from the cache - int count = 0; - for (String tokenId : tokenExpirations.keySet()) { - assertEquals("Expected the cached expiration to have been updated.", updatedExpiration, tss.getTokenExpiration(tokenId, count++ % 2 == 0)); - } - - } finally { - tss.stop(); - } - - // Verify that the expected methods were invoked - EasyMock.verify(aliasService); - } - - @Test - public void testTokenStateJournaling() throws Exception { - AliasService aliasService = EasyMock.createMock(AliasService.class); - aliasService.getAliasesForCluster(anyString()); - EasyMock.expectLastCall().andReturn(Collections.emptyList()).anyTimes(); - aliasService.addAliasesForCluster(anyString(), anyObject()); - EasyMock.expectLastCall().once(); - - //expecting this call when loading credentials from the keystore on startup - EasyMock.expect(aliasService.getPasswordsForGateway()).andReturn(Collections.emptyMap()).anyTimes(); - - EasyMock.replay(aliasService); - - tokenStatePersistenceInterval = 1L; // Override the persistence interval for this test - - AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); - tss.setAliasService(aliasService); - initTokenStateService(tss); - - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss); - - Path journalDir = Paths.get(getGatewaySecurityDir(), "token-state"); - - final long evictionInterval = TimeUnit.SECONDS.toMillis(3); - final long maxTokenLifetime = evictionInterval * 3; - - final List<String> tokenIds = new ArrayList<>(); - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < 10 ; i++) { - JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)); - testTokens.add(token); - tokenIds.add(token.getClaim(JWTToken.KNOX_ID_CLAIM)); - } - - 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()); - - // Check for the expected number of files corresponding to journal entries - List<Path> listing = Files.list(journalDir).collect(Collectors.toList()); - assertFalse(listing.isEmpty()); - assertEquals(10, listing.size()); - - // Validate the journal entry file names - for (Path p : listing) { - Path filename = p.getFileName(); - String filenameString = filename.toString(); - assertTrue(filenameString.endsWith(".ts")); - String tokenId = filenameString.substring(0, filenameString.length() - 3); - assertTrue(tokenIds.contains(tokenId)); - } - - // Sleep to allow the persistence to be performed - Thread.sleep(TimeUnit.SECONDS.toMillis(tokenStatePersistenceInterval) * 2); - - } finally { - tss.stop(); - tokenStatePersistenceInterval = null; - } - - // Verify that the expected methods were invoked - EasyMock.verify(aliasService); - - // Verify that the journal entries were removed when the aliases were created - List<Path> listing = Files.list(journalDir).collect(Collectors.toList()); - assertTrue(listing.isEmpty()); - } - - @Test - public void testLoadTokenStateJournalDuringInit() throws Exception { - final int TOKEN_COUNT = 10; - - AliasService aliasService = EasyMock.createMock(AliasService.class); - aliasService.getAliasesForCluster(anyString()); - EasyMock.expectLastCall().andReturn(Collections.emptyList()).anyTimes(); - EasyMock.replay(aliasService); - - // Create some test tokens - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; i++) { - JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)); - testTokens.add(token); - } - - // Persist the token state journal entries before initializing the TokenStateService - TokenStateJournal journal = TokenStateJournalFactory.create(createMockGatewayConfig(false)); - for (JWTToken token : testTokens) { - journal.add(token.getClaim(JWTToken.KNOX_ID_CLAIM), - System.currentTimeMillis(), - token.getExpiresDate().getTime(), - System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24), - null); - } - - AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); - tss.setAliasService(aliasService); - - // Initialize the service, and presumably load the previously-persisted journal entries - initTokenStateService(tss); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(tss); - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss); - Map<String, Long> tokenIssueTimes = getTokenIssueTimesField(tss, 3); - - Set<AliasBasedTokenStateService.TokenState> unpersistedState = getUnpersistedStateField(tss); - - assertEquals("Expected the tokens expirations to have been added in the base class cache.", - TOKEN_COUNT, - tokenExpirations.size()); - - assertEquals("Expected the tokens lifetimes to have been added in the base class cache.", - TOKEN_COUNT, - maxTokenLifetimes.size()); - - assertEquals("Expected the tokens issue times to have been added in the base class cache.", - TOKEN_COUNT, - tokenIssueTimes.size()); - - assertEquals("Expected the unpersisted state to have been added.", - (TOKEN_COUNT * 3), // Two TokenState entries per token (expiration, max lifetime, issue time) - unpersistedState.size()); - - // Verify that the expected methods were invoked - EasyMock.verify(aliasService); - } - - @Test - public void testLoadTokenStateJournalDuringInitWithInvalidEntries() throws Exception { - final int TOKEN_COUNT = 5; - - AliasService aliasService = EasyMock.createMock(AliasService.class); - aliasService.getAliasesForCluster(anyString()); - EasyMock.expectLastCall().andReturn(Collections.emptyList()).anyTimes(); - EasyMock.replay(aliasService); - - // Create some test tokens - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; i++) { - JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)); - testTokens.add(token); - } - - // Persist the token state journal entries before initializing the TokenStateService - TokenStateJournal journal = TokenStateJournalFactory.create(createMockGatewayConfig(false)); - for (JWTToken token : testTokens) { - journal.add(token.getClaim(JWTToken.KNOX_ID_CLAIM), - System.currentTimeMillis(), - token.getExpiresDate().getTime(), - System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24), - null); - } - - // Add an entry with an invalid token identifier - journal.add(" ", - System.currentTimeMillis(), - System.currentTimeMillis(), - System.currentTimeMillis(), - null); - - // Add an entry with an invalid issue time - journal.add(new TestJournalEntry(UUID.randomUUID().toString(), - "invalidLongValue", - String.valueOf(System.currentTimeMillis()), - String.valueOf(System.currentTimeMillis()), - new TokenMetadata("testUser"))); - - // Add an entry with an invalid expiration time - journal.add(new TestJournalEntry(UUID.randomUUID().toString(), - String.valueOf(System.currentTimeMillis()), - "invalidLongValue", - String.valueOf(System.currentTimeMillis()), - new TokenMetadata("testUser"))); - - // Add an entry with an invalid max lifetime - journal.add(new TestJournalEntry(UUID.randomUUID().toString(), - String.valueOf(System.currentTimeMillis()), - String.valueOf(System.currentTimeMillis()), - "invalidLongValue", - new TokenMetadata("testUser"))); - - AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); - tss.setAliasService(aliasService); - - // Initialize the service, and presumably load the previously-persisted journal entries - initTokenStateService(tss); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(tss); - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss); - Map<String, Long> tokenIssueTimes = getTokenIssueTimesField(tss, 3); - - Set<AliasBasedTokenStateService.TokenState> unpersistedState = getUnpersistedStateField(tss); - - assertEquals("Expected the tokens expirations to have been added in the base class cache.", - TOKEN_COUNT, - tokenExpirations.size()); - - assertEquals("Expected the tokens lifetimes to have been added in the base class cache.", - TOKEN_COUNT, - maxTokenLifetimes.size()); - - assertEquals("Expected the tokens issue times to have been added in the base class cache.", - TOKEN_COUNT, - tokenIssueTimes.size()); - - assertEquals("Expected the unpersisted state to have been added.", - (TOKEN_COUNT * 3), // Two TokenState entries per token (expiration, max lifetime, issue time) - unpersistedState.size()); - - // Verify that the expected methods were invoked - EasyMock.verify(aliasService); - } - - @Test - public void ensureAliases() throws Exception { - final int tokenCount = 1000; - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < tokenCount ; i++) { - JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)); - testTokens.add(token); - } - - final AliasBasedTokenStateService tss = (AliasBasedTokenStateService) createTokenStateService(); - final long issueTime = System.currentTimeMillis(); - for (JWTToken token : testTokens) { - tss.addToken(token, issueTime); - tss.renewToken(token); - } - - final List<AliasBasedTokenStateService.TokenState> unpersistedTokenStates = new ArrayList<>(getUnpersistedStateField(tss, 0)); - final int expectedAliasCount = 3 * tokenCount; //expiration + max + issue time for each token - assertEquals(expectedAliasCount, unpersistedTokenStates.size()); - for (JWTToken token : testTokens) { - String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM); - assertTrue(containsAlias(unpersistedTokenStates, tokenId)); - assertTrue(containsAlias(unpersistedTokenStates, tokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX)); - } - } - - private boolean containsAlias(List<AliasBasedTokenStateService.TokenState> unpersistedTokenStates, String expectedAlias) { - for(AliasBasedTokenStateService.TokenState tokenState : unpersistedTokenStates) { - if (tokenState.getAlias().equals(expectedAlias)) { - return true; - } - } - return false; - } - - @Override - protected TokenStateService createTokenStateService() throws Exception { - AliasBasedTokenStateService tss = new AliasBasedTokenStateService(); - tss.setAliasService(new TestAliasService()); - initTokenStateService(tss); - return tss; - } - - /** - * A dumbed-down AliasService implementation for testing purposes only. - */ - private static final class TestAliasService extends AbstractAliasService { - - private final Map<String, Map<String, String>> clusterAliases= new HashMap<>(); - - - @Override - public List<String> getAliasesForCluster(String clusterName) throws AliasServiceException { - List<String> aliases = new ArrayList<>(); - - if (clusterAliases.containsKey(clusterName)) { - aliases.addAll(clusterAliases.get(clusterName).keySet()); - } - return aliases; - } - - @Override - public void addAliasForCluster(String clusterName, String alias, String value) throws AliasServiceException { - Map<String, String> aliases = null; - if (clusterAliases.containsKey(clusterName)) { - aliases = clusterAliases.get(clusterName); - } else { - aliases = new HashMap<>(); - clusterAliases.put(clusterName, aliases); - } - aliases.put(alias, value); - } - - @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); - } - } - - @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)) { - String valString = clusterAliases.get(clusterName).get(alias); - if (valString != null) { - value = valString.toCharArray(); - } - } - return value; - } - - @Override - public char[] getPasswordFromAliasForCluster(String clusterName, String alias, boolean generate) throws AliasServiceException { - return new char[0]; - } - - @Override - public void generateAliasForCluster(String clusterName, String alias) throws AliasServiceException { - } - - @Override - public char[] getPasswordFromAliasForGateway(String alias) throws AliasServiceException { - return getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, alias); - } - - @Override - public char[] getGatewayIdentityPassphrase() throws AliasServiceException { - return new char[0]; - } - - @Override - public char[] getGatewayIdentityKeystorePassword() throws AliasServiceException { - return new char[0]; - } - - @Override - public char[] getSigningKeyPassphrase() throws AliasServiceException { - return new char[0]; - } - - @Override - public char[] getSigningKeystorePassword() throws AliasServiceException { - return new char[0]; - } - - @Override - public void generateAliasForGateway(String alias) throws AliasServiceException { - } - - @Override - public Certificate getCertificateForGateway(String alias) throws AliasServiceException { - return null; - } - - @Override - public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException { - } - - @Override - public void start() throws ServiceLifecycleException { - } - - @Override - public void stop() throws ServiceLifecycleException { - } - } - - @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(); - } - } - } - - private static Map<String, Long> getTokenExpirationsField(TokenStateService tss) throws Exception { - return getTokenExpirationsField(tss, 3); - } - - private static Map<String, Long> getTokenExpirationsField(TokenStateService tss, int level) throws Exception { - return (Map<String, Long>) getField(tss, level, "tokenExpirations"); - } - - private static Object getField(TokenStateService tss, int level, String fieldName) throws Exception { - final Field field = getParentClass(tss, level).getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(tss); - } - - private static Class<TokenStateService> getParentClass(TokenStateService tss, int level) { - Class<TokenStateService> clazz = (Class<TokenStateService>) tss.getClass(); - for (int i = 1; i <= level; i++) { - clazz = (Class<TokenStateService>) clazz.getSuperclass(); - } - return clazz; - } - - private static Map<String, Long> getMaxTokenLifetimesField(TokenStateService tss) throws Exception { - return getMaxTokenLifetimesField(tss, 3); - } - - private static Map<String, Long> getMaxTokenLifetimesField(TokenStateService tss, int level) throws Exception { - return (Map<String, Long>) getField(tss, level, "maxTokenLifetimes"); - } - - private static Map<String, Long> getTokenIssueTimesField(TokenStateService tss, int level) throws Exception { - return (Map<String, Long>) getField(tss, level, "tokenIssueTimes"); - } - - private static Map<String, Map<String, TokenMetadata>> getMetadataMapField(TokenStateService tss, int level) throws Exception { - return (Map<String, Map<String, TokenMetadata>>) getField(tss, level, "metadataMap"); - } - - private static Set<AliasBasedTokenStateService.TokenState> getUnpersistedStateField(TokenStateService tss) throws Exception { - return getUnpersistedStateField(tss, 1); - } - - private static Set<AliasBasedTokenStateService.TokenState> getUnpersistedStateField(TokenStateService tss, int level) throws Exception { - return (Set<AliasBasedTokenStateService.TokenState>) getField(tss, level, "unpersistedState"); - } - - private static class TestJournalEntry implements JournalEntry { - - private String tokenId; - private String issueTime; - private String expiration; - private String maxLifetime; - private TokenMetadata tokenMetadata; - - TestJournalEntry(String tokenId, String issueTime, String expiration, String maxLifetime, TokenMetadata tokenMetadata) { - this.tokenId = tokenId; - this.issueTime = issueTime; - this.expiration = expiration; - this.maxLifetime = maxLifetime; - this.tokenMetadata = tokenMetadata; - } - - @Override - public String getTokenId() { - return tokenId; - } - - @Override - public String getIssueTime() { - return issueTime; - } - - @Override - public String getExpiration() { - return expiration; - } - - @Override - public String getMaxLifetime() { - return maxLifetime; - } - - @Override - public TokenMetadata getTokenMetadata() { - return tokenMetadata; - } - - @Override - public String toString() { - return tokenId + "," + issueTime + "," + expiration + "," + maxLifetime; - } - } - - private static class NoEvictionAliasBasedTokenStateService extends AliasBasedTokenStateService { - - @Override - protected boolean readyForEviction() { - return false; - } - - } - -} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java deleted file mode 100644 index 82ecbe8ef..000000000 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java +++ /dev/null @@ -1,331 +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.knox.gateway.services.token.impl; - -import org.apache.knox.gateway.services.security.token.TokenStateService; -import org.apache.knox.gateway.services.security.token.impl.JWTToken; -import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory; -import org.apache.knox.gateway.services.token.state.TokenStateJournal; -import org.junit.Test; - -import java.lang.reflect.Field; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class JournalBasedTokenStateServiceTest extends DefaultTokenStateServiceTest { - - @Override - protected TokenStateService createTokenStateService() throws Exception { - TokenStateService tss = new JournalBasedTokenStateService(); - initTokenStateService(tss); - return tss; - } - - - @Test - public void testBulkTokenStateEviction() throws Exception { - final int TOKEN_COUNT = 5; - final long evictionInterval = TimeUnit.SECONDS.toMillis(3); - final long maxTokenLifetime = evictionInterval * 3; - - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; i++) { - testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60))); - } - - TokenStateService tss = createTokenStateService(); - - TokenStateJournal journal = getJournalField(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)); - } - - assertEquals(TOKEN_COUNT, journal.get().size()); - - // Sleep to allow the eviction evaluation to be performed - Thread.sleep(evictionInterval + (evictionInterval / 2)); - } finally { - tss.stop(); - } - - assertEquals(0, journal.get().size()); - } - - @Test - public void testAddAndRemoveTokenIncludesCache() throws Exception { - final int TOKEN_COUNT = 5; - - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; i++) { - testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60))); - } - - TokenStateService tss = createTokenStateService(); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(tss); - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(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.", - TOKEN_COUNT, - tokenExpirations.size()); - assertEquals("Expected the tokens lifetimes to have been added in the base class cache.", - TOKEN_COUNT, - maxTokenLifetimes.size()); - - // Sleep to allow the eviction evaluation to be performed - Thread.sleep(evictionInterval + (evictionInterval / 4)); - - } finally { - tss.stop(); - } - - 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 previously-persisted token state, so it's not left in the file - * system forever. - */ - @Test - public void testTokenEvictionIncludesPreviouslyPersistedJournalEntries() throws Exception { - final int TOKEN_COUNT = 5; - final long evictionInterval = TimeUnit.SECONDS.toMillis(3); - final long maxTokenLifetime = evictionInterval * 3; - - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; i++) { - testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60))); - } - - TokenStateJournal testJournal = - TokenStateJournalFactory.create(createMockGatewayConfig(false, - getGatewaySecurityDir(), - getTokenStatePersistenceInterval())); - - // Add a journal entry prior to initializing the TokenStateService - final JWTToken uncachedToken = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)); - final String uncachedTokenId = uncachedToken.getClaim(JWTToken.KNOX_ID_CLAIM); - testJournal.add(uncachedTokenId, - System.currentTimeMillis(), - uncachedToken.getExpiresDate().getTime(), - maxTokenLifetime, - null); - assertEquals("Expected the uncached journal entry", 1, testJournal.get().size()); - - // Create and initialize the TokenStateService - TokenStateService tss = createTokenStateService(); - TokenStateJournal journal = getJournalField(tss); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(tss); - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss); - - assertEquals("Expected the previously-persisted journal entry to have been loaded into the cache.", - 1, - tokenExpirations.size()); - assertEquals("Expected the previously-persisted journal entry to have been loaded into the cache.", - 1, - maxTokenLifetimes.size()); - - 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.", - TOKEN_COUNT + 1, - tokenExpirations.size()); - assertEquals("Expected the tokens lifetimes to have been added in the base class cache.", - TOKEN_COUNT + 1, - maxTokenLifetimes.size()); - assertEquals("Expected the uncached journal entry in addition to the cached tokens", - TOKEN_COUNT + 1, - journal.get().size()); - - - // Sleep to allow the eviction evaluation to be performed, but only one iteration - Thread.sleep(evictionInterval + (evictionInterval / 4)); - } finally { - tss.stop(); - } - - 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()); - assertEquals("Expected the journal entries to have been removed as a result of the eviction", - 0, - journal.get().size()); - } - - @Test - public void testGetMaxLifetimeUsesCache() throws Exception { - final int TOKEN_COUNT = 10; - TokenStateService tss = createTokenStateService(); - - Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss); - - final long evictionInterval = TimeUnit.SECONDS.toMillis(3); - final long maxTokenLifetime = evictionInterval * 3; - - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; 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.", - TOKEN_COUNT, - maxTokenLifetimes.size()); - - // Set the cache values to be different from the underlying journal entry 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 journal entry value", - updatedMaxLifetime, - ((JournalBasedTokenStateService) tss).getMaxLifetime(tokenId)); - } - } finally { - tss.stop(); - } - } - - @Test - public void testUpdateExpirationUsesCache() throws Exception { - final int TOKEN_COUNT = 10; - TokenStateService tss = createTokenStateService(); - - Map<String, Long> tokenExpirations = getTokenExpirationsField(tss); - - final long evictionInterval = TimeUnit.SECONDS.toMillis(3); - final long maxTokenLifetime = evictionInterval * 3; - - final Set<JWTToken> testTokens = new HashSet<>(); - for (int i = 0; i < TOKEN_COUNT ; 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.", - TOKEN_COUNT, - tokenExpirations.size()); - - // Set the cache values to be different from the underlying journal entry value - final long updatedExpiration = System.currentTimeMillis(); - for (String tokenId : tokenExpirations.keySet()) { - ((JournalBasedTokenStateService) tss).updateExpiration(tokenId, updatedExpiration); - } - - // Invoking with true/false validation flags as it should not affect if values are coming from the cache - int count = 0; - for (String tokenId : tokenExpirations.keySet()) { - assertEquals("Expected the cached expiration to have been updated.", - updatedExpiration, - tss.getTokenExpiration(tokenId, count++ % 2 == 0)); - } - - } finally { - tss.stop(); - } - } - - private static TokenStateJournal getJournalField(TokenStateService tss) throws Exception { - Field journalField = JournalBasedTokenStateService.class.getDeclaredField("journal"); - journalField.setAccessible(true); - return (TokenStateJournal) journalField.get(tss); - } - - private static Map<String, Long> getTokenExpirationsField(TokenStateService tss) throws Exception { - Field tokenExpirationsField = tss.getClass().getSuperclass().getSuperclass().getDeclaredField("tokenExpirations"); - tokenExpirationsField.setAccessible(true); - return (Map<String, Long>) tokenExpirationsField.get(tss); - } - - private static Map<String, Long> getMaxTokenLifetimesField(TokenStateService tss) throws Exception { - Field maxTokenLifetimesField = tss.getClass().getSuperclass().getSuperclass().getDeclaredField("maxTokenLifetimes"); - maxTokenLifetimesField.setAccessible(true); - return (Map<String, Long>) maxTokenLifetimesField.get(tss); - } -} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java deleted file mode 100644 index 2b8e32d1f..000000000 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java +++ /dev/null @@ -1,242 +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.knox.gateway.services.token.impl; - -import static org.apache.knox.gateway.config.GatewayConfig.REMOTE_CONFIG_REGISTRY_ADDRESS; -import static org.apache.knox.gateway.config.GatewayConfig.REMOTE_CONFIG_REGISTRY_TYPE; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.lang.reflect.Method; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.HashMap; -import java.util.Properties; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; - -import com.google.common.io.Files; -import org.apache.commons.io.FileUtils; -import org.apache.knox.gateway.config.GatewayConfig; -import org.apache.knox.gateway.service.config.remote.zk.ZooKeeperClientService; -import org.apache.knox.gateway.service.config.remote.zk.ZooKeeperClientServiceProvider; -import org.apache.knox.gateway.services.GatewayServices; -import org.apache.knox.gateway.services.ServiceType; -import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClientService; -import org.apache.knox.gateway.services.security.AliasService; -import org.apache.knox.gateway.services.security.KeystoreService; -import org.apache.knox.gateway.services.security.MasterService; -import org.apache.knox.gateway.services.security.token.TokenMetadata; -import org.apache.knox.test.TestUtils; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.server.embedded.ExitHandler; -import org.apache.zookeeper.server.embedded.ZooKeeperServerEmbedded; -import org.easymock.EasyMock; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class ZookeeperTokenStateServiceTest { - - @ClassRule - public static final TemporaryFolder testFolder = new TemporaryFolder(); - private static final String CONFIG_MONITOR_NAME = "remoteConfigMonitorClient"; - private static final long SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL = 2L; - private static final long LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL = 5L; - private static ZooKeeperServerEmbedded zkServer; - private static ZooKeeper zkClient; - private static File tempDir; - - @BeforeClass - public static void configureAndStartZKCluster() throws Exception { - tempDir = Files.createTempDir(); - Properties configuration = new Properties(); - int port = TestUtils.findFreePort(); - configuration.setProperty("clientPort", String.valueOf(port)); - // Removed SASL authentication to fix JDK 17 compatibility issues - // configuration.put("authProvider.1", "org.apache.zookeeper.server.auth.SASLAuthenticationProvider"); - // configuration.put("requireClientAuthScheme", "sasl"); - configuration.put("admin.enableServer", "false"); - zkServer = ZooKeeperServerEmbedded - .builder() - .exitHandler(ExitHandler.LOG_ONLY) - .baseDir(tempDir.toPath()) - .configuration(configuration) - .build(); - zkServer.start(); - - CountDownLatch latch = new CountDownLatch(1); - zkClient = new ZooKeeper("localhost:" + port, 40000, event -> { - System.out.println("event " + event); - latch.countDown(); - }); - latch.await(); - } - - @AfterClass - public static void tearDownSuite() throws Exception { - if (tempDir != null) { - FileUtils.deleteDirectory(tempDir); - } - if (zkClient != null) { - zkClient.close(); - } - if (zkServer != null) { - zkServer.close(); - } - } - - @Test - public void testStoringTokenAliasesInZookeeper() throws Exception { - final ZookeeperTokenStateService zktokenStateService = setupZkTokenStateService(SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL); - - assertFalse(zkNodeExists("/knox/security/topology/__gateway/tokens/a0/a0-token1")); - assertFalse(zkNodeExists("/knox/security/topology/__gateway/tokens/a0/a0-token1" + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX)); - - zktokenStateService.addToken("a0-token1", 1L, 2L); - - // give some time for the token state service to persist the token aliases in ZK (doubled the persistence interval) - Thread.sleep(2 * SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1000); - - assertTrue(zkNodeExists("/knox/security/topology/__gateway/tokens/a0/a0-token1")); - assertTrue(zkNodeExists("/knox/security/topology/__gateway/tokens/a0/a0-token1" + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX)); - } - - @Test - public void testRetry() throws Exception { - final ZookeeperTokenStateService zktokenStateServiceNode1 = setupZkTokenStateService(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL); - final ZookeeperTokenStateService zktokenStateServiceNode2 = setupZkTokenStateService(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL); - final String tokenId = UUID.randomUUID().toString(); - zktokenStateServiceNode1.addToken(tokenId, 10L, 2000L); - final long expiration = zktokenStateServiceNode2.getTokenExpiration(tokenId); - Thread.sleep(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1000); - assertEquals(2000L, expiration); - - final String userName = "testUser"; - final String comment = "This is my test comment"; - zktokenStateServiceNode1.addMetadata(tokenId, new TokenMetadata(userName, comment, true)); - Thread.sleep(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1000); - assertEquals(userName, zktokenStateServiceNode2.getTokenMetadata(tokenId).getUserName()); - assertEquals(comment, zktokenStateServiceNode2.getTokenMetadata(tokenId).getComment()); - assertTrue(zktokenStateServiceNode2.getTokenMetadata(tokenId).isEnabled()); - } - - @Test - public void testRenewal() throws Exception { - final ZookeeperTokenStateService zktokenStateServiceNode1 = setupZkTokenStateService(SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL); - final ZookeeperTokenStateService zktokenStateServiceNode2 = setupZkTokenStateService(SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL); - final String tokenId = "a1-token"; - final long issueTime = System.currentTimeMillis(); - final long tokenTTL = 1000L; - final long renewInterval = 2000L; - - zktokenStateServiceNode1.addToken(tokenId, issueTime, issueTime + tokenTTL); - Thread.sleep(SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1500); - assertEquals(zktokenStateServiceNode1.getTokenExpiration(tokenId), zktokenStateServiceNode2.getTokenExpiration(tokenId)); - - //now renew token on node 1 and check if renewal is reflected on node2 - zktokenStateServiceNode1.renewToken(tokenId, renewInterval); - Thread.sleep(SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1500); - assertEquals(zktokenStateServiceNode1.getTokenExpiration(tokenId), zktokenStateServiceNode2.getTokenExpiration(tokenId)); - } - - @Test - public void testTokenIDDisplayText() throws Exception { - ZookeeperTokenStateService tss = setupZkTokenStateService(SHORT_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL); - Method m = tss.getClass().getDeclaredMethod("getDisplayableAliasText", String.class); - m.setAccessible(true); - final String uuid = UUID.randomUUID().toString(); - final String maxAlias = uuid + ZookeeperTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX; - final String metaAlias = uuid + ZookeeperTokenStateService.TOKEN_META_POSTFIX; - - // Check an expiration alias - String displayableUUID = (String) m.invoke(tss, uuid); - assertTrue(displayableUUID.length() < uuid.length()); - assertEquals(8, displayableUUID.indexOf("...")); - - // Check a max lifetime alias - String displayableMaxAlias = (String) m.invoke(tss, maxAlias); - assertFalse(displayableMaxAlias.contains(uuid)); - assertTrue(displayableMaxAlias.length() < maxAlias.length()); - assertEquals(8, displayableMaxAlias.indexOf("...")); - assertTrue(displayableMaxAlias.endsWith(ZookeeperTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX)); - - // Check a metadata alias - String displayableMetaAlias = (String) m.invoke(tss, metaAlias); - assertFalse(displayableMetaAlias.contains(uuid)); - assertTrue(displayableMetaAlias.length() < metaAlias.length()); - assertEquals(8, displayableMetaAlias.indexOf("...")); - assertTrue(displayableMetaAlias.endsWith(ZookeeperTokenStateService.TOKEN_META_POSTFIX)); - - } - - private ZookeeperTokenStateService setupZkTokenStateService(long persistenceInterval) throws Exception { - // mocking GatewayConfig - final GatewayConfig gc = EasyMock.createNiceMock(GatewayConfig.class); - expect(gc.getRemoteRegistryConfigurationNames()).andReturn(Collections.singletonList(CONFIG_MONITOR_NAME)).anyTimes(); - // Add null check to prevent NullPointerException if Zookeeper server failed to start - if (zkServer == null) { - throw new IllegalStateException("Zookeeper server failed to start in @BeforeClass"); - } - final String registryConfig = REMOTE_CONFIG_REGISTRY_TYPE + "=" + ZooKeeperClientService.TYPE + ";" + REMOTE_CONFIG_REGISTRY_ADDRESS + "=" + zkServer.getConnectionString(); - expect(gc.getRemoteRegistryConfiguration(CONFIG_MONITOR_NAME)).andReturn(registryConfig).anyTimes(); - expect(gc.getRemoteConfigurationMonitorClientName()).andReturn(CONFIG_MONITOR_NAME).anyTimes(); - expect(gc.getAlgorithm()).andReturn("AES").anyTimes(); - expect(gc.isRemoteAliasServiceEnabled()).andReturn(true).anyTimes(); - expect(gc.getKnoxTokenStateAliasPersistenceInterval()).andReturn(persistenceInterval).anyTimes(); - final Path baseFolder = Paths.get(testFolder.newFolder().getAbsolutePath()); - expect(gc.getGatewayDataDir()).andReturn(Paths.get(baseFolder.toString(), "data").toString()).anyTimes(); - expect(gc.getGatewayKeystoreDir()).andReturn(Paths.get(baseFolder.toString(), "data", "keystores").toString()).anyTimes(); - expect(gc.getGatewaySecurityDir()).andReturn(Paths.get(baseFolder.toString(), "security").toString()).anyTimes(); - replay(gc); - - // mocking GatewayServices - final GatewayServices gatewayServices = EasyMock.createNiceMock(GatewayServices.class); - final char[] masterSecret = "ThisIsMySup3rS3cr3tM4sterPassW0rd!".toCharArray(); - final MasterService masterService = EasyMock.createNiceMock(MasterService.class); - expect(masterService.getMasterSecret()).andReturn(masterSecret).anyTimes(); - expect(gatewayServices.getService(ServiceType.MASTER_SERVICE)).andReturn(masterService).anyTimes(); - final KeystoreService keystoreservice = EasyMock.createNiceMock(KeystoreService.class); - expect(keystoreservice.getCredentialStoreForCluster(AliasService.NO_CLUSTER_NAME)).andReturn(null).anyTimes(); - expect(gatewayServices.getService(ServiceType.KEYSTORE_SERVICE)).andReturn(keystoreservice).anyTimes(); - final AliasService aliasService = EasyMock.createNiceMock(AliasService.class); - expect(gatewayServices.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes(); - final RemoteConfigurationRegistryClientService clientService = (new ZooKeeperClientServiceProvider()).newInstance(); - clientService.setAliasService(aliasService); - clientService.init(gc, Collections.emptyMap()); - expect(gatewayServices.getService(ServiceType.REMOTE_REGISTRY_CLIENT_SERVICE)).andReturn(clientService).anyTimes(); - replay(gatewayServices, masterService, keystoreservice); - - final ZookeeperTokenStateService zktokenStateService = new ZookeeperTokenStateService(gatewayServices); - zktokenStateService.init(gc, new HashMap<>()); - zktokenStateService.start(); - return zktokenStateService; - } - - private boolean zkNodeExists(String nodeName) throws InterruptedException, KeeperException { - return zkClient.exists(nodeName, false) != null; - } -}
