Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix token caching for multi-cluster multi-namespace environments #261

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import com.bettercloud.vault.VaultException;
import com.cloudbees.plugins.credentials.CredentialsScope;
import java.util.Calendar;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand All @@ -14,8 +17,105 @@ public abstract class AbstractVaultTokenCredentialWithExpiration
private final static Logger LOGGER = Logger
.getLogger(AbstractVaultTokenCredentialWithExpiration.class.getName());

private Calendar tokenExpiry;
private String currentClientToken;
public static class CacheKey {
private final String vaultUrl;
private final String namespace;

public CacheKey(String vaultUrl, String namespace) {
this.vaultUrl = vaultUrl;
this.namespace = namespace;
}

@Override
public String toString() {
return String.format("vaultUrl=%s, namespace=%s", vaultUrl, namespace);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CacheKey)) {
return false;
}
CacheKey cacheKey = (CacheKey) o;
return Objects.equals(vaultUrl, cacheKey.vaultUrl) && Objects.equals(
namespace, cacheKey.namespace);
}

@Override
public int hashCode() {
return Objects.hash(vaultUrl, namespace);
}
}

public static class TokenHolder {
private final CacheKey key;
private Calendar tokenExpiry;
private String token;

public TokenHolder(CacheKey key) {
this.key = key;
}

public synchronized Vault authorizeWithVault(
AbstractVaultTokenCredentialWithExpiration credentials, VaultConfig config) {
Vault vault = credentials.getVault(config);
if (tokenExpired()) {
token = credentials.getToken(vault);
config.token(token);
setTokenExpiry(vault);
} else {
config.token(token);
}
return vault;
}

private void setTokenExpiry(Vault vault) {
int tokenTTL = 0;
try {
tokenTTL = (int) vault.auth().lookupSelf().getTTL();
} catch (VaultException e) {
LOGGER.log(Level.WARNING,
String.format("Could not determine token expiration (for key %s). ", key) +
"Check if token is allowed to access auth/token/lookup-self. " +
"Assuming token TTL expired.", e);
}
tokenExpiry = Calendar.getInstance();
tokenExpiry.add(Calendar.SECOND, tokenTTL);
}

private boolean tokenExpired() {
if (tokenExpiry == null) {
return true;
}

boolean result = true;
Calendar now = Calendar.getInstance();
long timeDiffInMillis = now.getTimeInMillis() - tokenExpiry.getTimeInMillis();
if (timeDiffInMillis < -2000L) {
// token will be valid for at least another 2s
result = false;
LOGGER.log(Level.FINE, String.format("Auth token (for key %s) is still valid", key));
} else {
LOGGER.log(Level.FINE,
String.format("Auth token (for key %s) has to be re-issued (%d)",
key, timeDiffInMillis));
}

return result;
}
}

private ConcurrentMap<CacheKey, TokenHolder> cache = new ConcurrentHashMap<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of ConcurrentMap we might as well pull in caffeine.

    <dependency>
      <groupId>io.jenkins.plugins</groupId>
      <artifactId>caffeine-api</artifactId>
    </dependency>


private synchronized ConcurrentMap<CacheKey, TokenHolder> getCache() {
if (cache == null) {
cache = new ConcurrentHashMap<>();
}
return cache;
}

protected AbstractVaultTokenCredentialWithExpiration(CredentialsScope scope, String id,
String description) {
Expand All @@ -26,50 +126,14 @@ protected AbstractVaultTokenCredentialWithExpiration(CredentialsScope scope, Str

@Override
public Vault authorizeWithVault(VaultConfig config) {
Vault vault = getVault(config);
if (tokenExpired()) {
currentClientToken = getToken(vault);
config.token(currentClientToken);
setTokenExpiry(vault);
} else {
config.token(currentClientToken);
}
return vault;
ConcurrentMap<CacheKey, TokenHolder> cache = getCache();
CacheKey key = new CacheKey(config.getAddress(), config.getNameSpace());
cache.putIfAbsent(key, new TokenHolder(key));
TokenHolder holder = cache.get(key);
return holder.authorizeWithVault(this, config);
}

protected Vault getVault(VaultConfig config) {
return new Vault(config);
}

private void setTokenExpiry(Vault vault) {
int tokenTTL = 0;
try {
tokenTTL = (int) vault.auth().lookupSelf().getTTL();
} catch (VaultException e) {
LOGGER.log(Level.WARNING, "Could not determine token expiration. " +
"Check if token is allowed to access auth/token/lookup-self. " +
"Assuming token TTL expired.", e);
}
tokenExpiry = Calendar.getInstance();
tokenExpiry.add(Calendar.SECOND, tokenTTL);
}

private boolean tokenExpired() {
if (tokenExpiry == null) {
return true;
}

boolean result = true;
Calendar now = Calendar.getInstance();
long timeDiffInMillis = now.getTimeInMillis() - tokenExpiry.getTimeInMillis();
if (timeDiffInMillis < -2000L) {
// token will be valid for at least another 2s
result = false;
LOGGER.log(Level.FINE, "Auth token is still valid");
} else {
LOGGER.log(Level.FINE, "Auth token has to be re-issued" + timeDiffInMillis);
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,36 @@ public void shouldBeAbleToFetchTokenOnInit() throws VaultException {

@Test
public void shouldReuseTheExistingTokenIfNotExpired() throws VaultException {
when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2");
when(auth.lookupSelf()).thenReturn(lookupResponse);
when(lookupResponse.getTTL()).thenReturn(5L);

vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);

verify(vaultConfig, times(2)).token("fakeToken1");
}

@Test
public void shouldCacheDifferentTokensPerServer() throws VaultException {
when(vaultConfig.getAddress()).thenReturn("http://first");
when(vaultConfig.getNameSpace()).thenReturn(null);

VaultConfig secondVaultConfig = mock(VaultConfig.class);
when(secondVaultConfig.getAddress()).thenReturn("http://second");
when(secondVaultConfig.getNameSpace()).thenReturn("second");

when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2", "shouldNeverBeRequested");
when(auth.lookupSelf()).thenReturn(lookupResponse);
when(lookupResponse.getTTL()).thenReturn(5L);

vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
vaultTokenCredentialWithExpiration.authorizeWithVault(secondVaultConfig);
vaultTokenCredentialWithExpiration.authorizeWithVault(secondVaultConfig);

verify(vaultConfig, times(2)).token("fakeToken");
verify(vaultConfig, times(2)).token("fakeToken1");
verify(secondVaultConfig, times(2)).token("fakeToken2");
}

@Test
Expand Down