blob: 8254843d63bf37f0355179004b31f970fcd565f3 [file] [log] [blame]
/*
* 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.AliasService;
import org.apache.knox.gateway.services.security.AliasServiceException;
import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import org.easymock.EasyMock;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.anyString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTest {
/**
* KNOX-2375
*/
@Test
public void testBulkTokenStateEviction() throws Exception {
final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
final long maxTokenLifetime = evictionInterval * 3;
final Set<JWTToken> testTokens = new HashSet<>();
for (int i = 0; i < 10 ; i++) {
testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
}
List<String> testTokenStateAliases = new ArrayList<>();
for (JWTToken token : testTokens) {
String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM);
testTokenStateAliases.add(tokenId);
testTokenStateAliases.add(tokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
}
// Create a mock AliasService so we can verify that the expected bulk removal method is invoked when the token state
// reaper runs.
AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
EasyMock.expect(aliasService.getPasswordFromAliasForCluster(anyString(), anyString()))
.andReturn(String.valueOf(System.currentTimeMillis()).toCharArray())
.anyTimes();
EasyMock.expect(aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME)).andReturn(testTokenStateAliases).anyTimes();
// Expecting the bulk alias removal method to be invoked only once, rather than the individual alias removal method
// invoked twice for every expired token.
aliasService.removeAliasesForCluster(anyString(), anyObject());
EasyMock.expectLastCall().andVoid().once();
EasyMock.replay(aliasService);
AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
tss.setAliasService(aliasService);
initTokenStateService(tss);
try {
tss.start();
// Add the expired tokens
for (JWTToken token : testTokens) {
tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
System.currentTimeMillis(),
token.getExpiresDate().getTime(),
maxTokenLifetime);
assertTrue("Expected the token to have expired.", tss.isExpired(token));
}
// Sleep to allow the eviction evaluation to be performed
Thread.sleep(evictionInterval + (evictionInterval / 2));
} finally {
tss.stop();
}
// Verify that the expected method was invoked
EasyMock.verify(aliasService);
}
@Test
public void testAddAndRemoveTokenIncludesCache() throws Exception {
final Set<JWTToken> testTokens = new HashSet<>();
for (int i = 0; i < 10 ; i++) {
testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
}
List<String> testTokenStateAliases = new ArrayList<>();
for (JWTToken token : testTokens) {
String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM);
testTokenStateAliases.add(tokenId);
testTokenStateAliases.add(tokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
}
// Create a mock AliasService so we can verify that the expected bulk removal method is invoked (and that the
// individual removal method is NOT invoked) when the token state reaper runs.
AliasService aliasService = EasyMock.createMock(AliasService.class);
EasyMock.expect(aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME)).andReturn(testTokenStateAliases).anyTimes();
// Expecting the bulk alias removal method to be invoked only once, rather than the individual alias removal method
// invoked twice for every expired token.
aliasService.removeAliasesForCluster(anyString(), anyObject());
EasyMock.expectLastCall().andVoid().once();
EasyMock.replay(aliasService);
AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
tss.setAliasService(aliasService);
initTokenStateService(tss);
Field tokenExpirationsField = tss.getClass().getSuperclass().getDeclaredField("tokenExpirations");
tokenExpirationsField.setAccessible(true);
Map<String, Long> tokenExpirations = (Map<String, Long>) tokenExpirationsField.get(tss);
Field maxTokenLifetimesField = tss.getClass().getSuperclass().getDeclaredField("maxTokenLifetimes");
maxTokenLifetimesField.setAccessible(true);
Map<String, Long> maxTokenLifetimes = (Map<String, Long>) maxTokenLifetimesField.get(tss);
final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
final long maxTokenLifetime = evictionInterval * 3;
try {
tss.start();
// Add the expired tokens
for (JWTToken token : testTokens) {
tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
System.currentTimeMillis(),
token.getExpiresDate().getTime(),
maxTokenLifetime);
}
assertEquals("Expected the tokens to have been added in the base class cache.", 10, tokenExpirations.size());
assertEquals("Expected the tokens lifetimes to have been added in the base class cache.",
10,
maxTokenLifetimes.size());
// Sleep to allow the eviction evaluation to be performed
Thread.sleep(evictionInterval + (evictionInterval / 4));
} finally {
tss.stop();
}
// Verify that the expected methods were invoked
EasyMock.verify(aliasService);
assertEquals("Expected the tokens to have been removed from the base class cache as a result of eviction.",
0,
tokenExpirations.size());
assertEquals("Expected the tokens lifetimes to have been removed from the base class cache as a result of eviction.",
0,
maxTokenLifetimes.size());
}
/**
* Verify that the token state reaper includes token state which has not been cached, so it's not left in the keystore
* forever.
*/
@Test
public void testTokenEvictionIncludesUncachedAliases() throws Exception {
final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
final long maxTokenLifetime = evictionInterval * 3;
final Set<JWTToken> testTokens = new HashSet<>();
for (int i = 0; i < 10 ; i++) {
testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
}
List<String> testTokenStateAliases = new ArrayList<>();
for (JWTToken token : testTokens) {
testTokenStateAliases.add(token.getClaim(JWTToken.KNOX_ID_CLAIM));
testTokenStateAliases.add(token.getClaim(JWTToken.KNOX_ID_CLAIM) + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
}
// Add aliases for an uncached test token
final JWTToken uncachedToken = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
final String uncachedTokenId = uncachedToken.getClaim(JWTToken.KNOX_ID_CLAIM);
testTokenStateAliases.add(uncachedTokenId);
testTokenStateAliases.add(uncachedTokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
final long uncachedTokenExpiration = System.currentTimeMillis();
System.out.println("Uncached token ID: " + uncachedTokenId);
final Set<String> expectedTokensToEvict = new HashSet<>();
expectedTokensToEvict.addAll(testTokenStateAliases);
expectedTokensToEvict.add(uncachedTokenId);
expectedTokensToEvict.add(uncachedTokenId + AliasBasedTokenStateService.TOKEN_MAX_LIFETIME_POSTFIX);
// Create a mock AliasService so we can verify that the expected bulk removal method is invoked (and that the
// individual removal method is NOT invoked) when the token state reaper runs.
AliasService aliasService = EasyMock.createMock(AliasService.class);
EasyMock.expect(aliasService.getAliasesForCluster(AliasService.NO_CLUSTER_NAME)).andReturn(testTokenStateAliases).anyTimes();
// Expecting the bulk alias removal method to be invoked only once, rather than the individual alias removal method
// invoked twice for every expired token.
aliasService.removeAliasesForCluster(anyString(), EasyMock.eq(expectedTokensToEvict));
EasyMock.expectLastCall().andVoid().once();
aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, uncachedTokenId);
EasyMock.expectLastCall().andReturn(String.valueOf(uncachedTokenExpiration).toCharArray()).once();
EasyMock.replay(aliasService);
AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
tss.setAliasService(aliasService);
initTokenStateService(tss);
Field tokenExpirationsField = tss.getClass().getSuperclass().getDeclaredField("tokenExpirations");
tokenExpirationsField.setAccessible(true);
Map<String, Long> tokenExpirations = (Map<String, Long>) tokenExpirationsField.get(tss);
Field maxTokenLifetimesField = tss.getClass().getSuperclass().getDeclaredField("maxTokenLifetimes");
maxTokenLifetimesField.setAccessible(true);
Map<String, Long> maxTokenLifetimes = (Map<String, Long>) maxTokenLifetimesField.get(tss);
try {
tss.start();
// Add the expired tokens
for (JWTToken token : testTokens) {
tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
System.currentTimeMillis(),
token.getExpiresDate().getTime(),
maxTokenLifetime);
}
assertEquals("Expected the tokens to have been added in the base class cache.", 10, tokenExpirations.size());
assertEquals("Expected the tokens lifetimes to have been added in the base class cache.",
10,
maxTokenLifetimes.size());
// Sleep to allow the eviction evaluation to be performed, but only one iteration
Thread.sleep(evictionInterval + (evictionInterval / 4));
} finally {
tss.stop();
}
// Verify that the expected methods were invoked
EasyMock.verify(aliasService);
assertEquals("Expected the tokens to have been removed from the base class cache as a result of eviction.",
0,
tokenExpirations.size());
assertEquals("Expected the tokens lifetimes to have been removed from the base class cache as a result of eviction.",
0,
maxTokenLifetimes.size());
}
@Test
public void testGetMaxLifetimeUsesCache() throws Exception {
AliasService aliasService = EasyMock.createMock(AliasService.class);
EasyMock.replay(aliasService);
AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
tss.setAliasService(aliasService);
initTokenStateService(tss);
Field maxTokenLifetimesField = tss.getClass().getSuperclass().getDeclaredField("maxTokenLifetimes");
maxTokenLifetimesField.setAccessible(true);
Map<String, Long> maxTokenLifetimes = (Map<String, Long>) maxTokenLifetimesField.get(tss);
final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
final long maxTokenLifetime = evictionInterval * 3;
final Set<JWTToken> testTokens = new HashSet<>();
for (int i = 0; i < 10 ; i++) {
testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
}
try {
tss.start();
// Add the expired tokens
for (JWTToken token : testTokens) {
tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
System.currentTimeMillis(),
token.getExpiresDate().getTime(),
maxTokenLifetime);
}
assertEquals("Expected the tokens lifetimes to have been added in the base class cache.",
10,
maxTokenLifetimes.size());
// Set the cache values to be different from the underlying alias value
final long updatedMaxLifetime = evictionInterval * 5;
for (Map.Entry<String, Long> entry : maxTokenLifetimes.entrySet()) {
entry.setValue(updatedMaxLifetime);
}
// Verify that we get the cache value back
for (String tokenId : maxTokenLifetimes.keySet()) {
assertEquals("Expected the cached max lifetime, rather than the alias value",
updatedMaxLifetime,
tss.getMaxLifetime(tokenId));
}
} finally {
tss.stop();
}
// Verify that the expected methods were invoked
EasyMock.verify(aliasService);
}
@Test
public void testUpdateExpirationUsesCache() throws Exception {
AliasService aliasService = EasyMock.createMock(AliasService.class);
aliasService.addAliasForCluster(anyString(), anyString(), anyString());
EasyMock.expectLastCall().andVoid().atLeastOnce();
aliasService.removeAliasForCluster(anyString(), anyObject());
EasyMock.expectLastCall().andVoid().atLeastOnce();
EasyMock.replay(aliasService);
AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
tss.setAliasService(aliasService);
initTokenStateService(tss);
Field tokenExpirationsField = tss.getClass().getSuperclass().getDeclaredField("tokenExpirations");
tokenExpirationsField.setAccessible(true);
Map<String, Long> tokenExpirations = (Map<String, Long>) tokenExpirationsField.get(tss);
final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
final long maxTokenLifetime = evictionInterval * 3;
final Set<JWTToken> testTokens = new HashSet<>();
for (int i = 0; i < 10 ; i++) {
testTokens.add(createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60)));
}
try {
tss.start();
// Add the expired tokens
for (JWTToken token : testTokens) {
tss.addToken(token.getClaim(JWTToken.KNOX_ID_CLAIM),
System.currentTimeMillis(),
token.getExpiresDate().getTime(),
maxTokenLifetime);
}
assertEquals("Expected the tokens expirations to have been added in the base class cache.",
10,
tokenExpirations.size());
// Set the cache values to be different from the underlying alias value
final long updatedExpiration = System.currentTimeMillis();
for (String tokenId : tokenExpirations.keySet()) {
tss.updateExpiration(tokenId, updatedExpiration);
}
for (String tokenId : tokenExpirations.keySet()) {
assertEquals("Expected the cached expiration to have been updated.",
updatedExpiration,
tss.getTokenExpiration(tokenId, false));
}
} finally {
tss.stop();
}
// Verify that the expected methods were invoked
EasyMock.verify(aliasService);
}
@Override
protected TokenStateService createTokenStateService() {
AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
tss.setAliasService(new TestAliasService());
initTokenStateService(tss);
return tss;
}
/**
* A dumbed-down AliasService implementation for testing purposes only.
*/
private static final class TestAliasService implements AliasService {
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();
}
}
}
}