Skip to content

Commit

Permalink
Aquire new token if current token is about to expire
Browse files Browse the repository at this point in the history
This fixes the edge case when the communication to vault is very slow
and the passed token is about to expire, it can happen that
the call to lookup self returns success but the following vault calls
still run into an error as the token just expired.

This change will fix this by calculating the maximal time, a call to
vault will take. This is capped to 60 seconds as the documentation
for the read and open timeout parameters were wrong. As the previous
documentation stated that the timout paramter are ms valued, they
are actually seconds. This could cause users to have high values
in these paramters. To prevent continues token refreshing in that case,
the maximal guaranteed token validity is capped to 60 seconds.
  • Loading branch information
anrock-sc committed Nov 18, 2021
1 parent adc8ab5 commit a5d91dd
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 5 deletions.
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ dependencies {
testCompile(
[group: 'junit', name: 'junit', version: '4.12', ext: 'jar'],
[group: 'org.hamcrest', name: 'hamcrest-core', version: '1.3', ext: 'jar'],
[group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3', ext: 'jar']
[group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3', ext: 'jar'],
[group: 'org.mockito', name: 'mockito-core', version: '4.0.0', ext: 'jar'],
[group: 'net.bytebuddy', name: 'byte-buddy', version: '1.12.1'],
[group: 'net.bytebuddy', name: 'byte-buddy-agent', version: '1.12.1'],
[group: 'org.objenesis', name: 'objenesis', version: '3.2']
)

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.api.Logical;
import com.bettercloud.vault.response.LookupResponse;
import com.bettercloud.vault.response.VaultResponse;
import com.dtolabs.rundeck.core.plugins.Plugin;
import com.dtolabs.rundeck.core.plugins.configuration.Configurable;
Expand All @@ -31,7 +32,6 @@
* @author ValFadeev
* @since 2017-09-18
*/

@Plugin(name = "vault-storage", service = ServiceNameConstants.Storage)
public class VaultStoragePlugin implements StoragePlugin, Configurable, Describable {

Expand All @@ -47,10 +47,12 @@ public VaultStoragePlugin() {}
protected static final String PUBLIC_KEY_MIME_TYPE = "application/pgp-keys";
protected static final String PASSWORD_MIME_TYPE = "application/x-rundeck-data-password";

public static final int MAX_GUARANTEED_VALIDITY_SECONDS = 60;

private String vaultPrefix;
private String vaultSecretBackend;
private Logical vault;
private int guaranteedTokenValidity;
//if is true, objects will be saved with rundeck default headers behaivour
private boolean rundeckObject=true;
private VaultClientProvider clientProvider;
Expand All @@ -66,14 +68,30 @@ public Description getDescription() {
public void configure(Properties configuration) throws ConfigurationException {
vaultPrefix = configuration.getProperty(VAULT_PREFIX);
vaultSecretBackend = configuration.getProperty(VAULT_SECRET_BACKEND);
clientProvider = new VaultClientProvider(configuration);
clientProvider = getVaultClientProvider(configuration);
loginVault(clientProvider);

//check storage behaivour
String storageBehaviour=configuration.getProperty(VAULT_STORAGE_BEHAVIOUR);
if(storageBehaviour!=null && storageBehaviour.equals("vault")){
rundeckObject=false;
}

guaranteedTokenValidity = calculateGuaranteedTokenValidity(configuration);
}

protected VaultClientProvider getVaultClientProvider(Properties configuration) {
return new VaultClientProvider(configuration);
}

protected int calculateGuaranteedTokenValidity(Properties configuration) {
return Integer.min(
Integer.parseInt(configuration.getProperty(VAULT_MAX_RETRIES))
* (Integer.parseInt(configuration.getProperty(VAULT_READ_TIMEOUT))
+ Integer.parseInt(configuration.getProperty(VAULT_OPEN_TIMEOUT))
+ Integer.parseInt(configuration.getProperty(VAULT_RETRY_INTERVAL_MILLISECONDS)) / 1000),
MAX_GUARANTEED_VALIDITY_SECONDS
);
}

public static String getVaultPath(String rawPath, String vaultSecretBackend, String vaultPrefix) {
Expand All @@ -85,9 +103,11 @@ private boolean isDir(String key) {
return key.endsWith("/");
}

private void lookup(){
protected void lookup(){
try {
vaultClient.auth().lookupSelf();
if (vaultClient.auth().lookupSelf().getTTL() <= guaranteedTokenValidity) {
loginVault(clientProvider);
}
} catch (VaultException e) {
if(e.getHttpStatusCode() == 403){//try login again
loginVault(clientProvider);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package io.github.valfadeev.rundeck.plugin.vault;

import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.api.Auth;
import com.bettercloud.vault.api.Logical;
import com.bettercloud.vault.response.LookupResponse;
import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException;
import org.junit.Test;
import org.mockito.Spy;

import java.util.Properties;

import static io.github.valfadeev.rundeck.plugin.vault.ConfigOptions.*;
import static org.mockito.Mockito.*;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class VaultStoragePluginTest {

@Spy
private VaultStoragePlugin vaultStoragePlugin = new VaultStoragePlugin();

@Test
public void guaranteedTokenValidity_returnsCalculatedValue() {
Properties properties = mock(Properties.class);

doReturn("5").when(properties).getProperty(VAULT_MAX_RETRIES);
doReturn("2").when(properties).getProperty(VAULT_READ_TIMEOUT);
doReturn("2").when(properties).getProperty(VAULT_OPEN_TIMEOUT);
doReturn("1000").when(properties).getProperty(VAULT_RETRY_INTERVAL_MILLISECONDS);

int validity = vaultStoragePlugin.calculateGuaranteedTokenValidity(properties);

assertThat(validity, is(25));
}

@Test
public void guaranteedTokenValidity_returnMaxCapedValue() {
Properties properties = mock(Properties.class);

doReturn("5").when(properties).getProperty(VAULT_MAX_RETRIES);
doReturn("20").when(properties).getProperty(VAULT_READ_TIMEOUT);
doReturn("20").when(properties).getProperty(VAULT_OPEN_TIMEOUT);
doReturn("1000").when(properties).getProperty(VAULT_RETRY_INTERVAL_MILLISECONDS);

int validity = vaultStoragePlugin.calculateGuaranteedTokenValidity(properties);

assertThat(validity, is(VaultStoragePlugin.MAX_GUARANTEED_VALIDITY_SECONDS));
}

@Test
public void lookUp_passes_when_tokenIsValid() throws ConfigurationException, VaultException {
VaultStoragePlugin vaultStoragePlugin = spy(new VaultStoragePlugin());
Properties properties = mock(Properties.class);
VaultClientProvider vaultClientProvider = mock(VaultClientProvider.class);
Vault vault = mock(Vault.class);
Logical logical = mock(Logical.class);
Auth auth = mock(Auth.class);
LookupResponse response = mock(LookupResponse.class);

doReturn(vaultClientProvider).when(vaultStoragePlugin).getVaultClientProvider(properties);
doReturn(vault).when(vaultClientProvider).getVaultClient();
doReturn(logical).when(vault).logical();

doReturn(auth).when(vault).auth();
doReturn(response).when(auth).lookupSelf();
doReturn(1312L).when(response).getTTL();

doReturn("rundeck").when(properties).getProperty(VAULT_PREFIX);
doReturn("approle").when(properties).getProperty(VAULT_SECRET_BACKEND);
doReturn("vault").when(properties).getProperty(VAULT_STORAGE_BEHAVIOUR);

doReturn("5").when(properties).getProperty(VAULT_MAX_RETRIES);
doReturn("2").when(properties).getProperty(VAULT_READ_TIMEOUT);
doReturn("2").when(properties).getProperty(VAULT_OPEN_TIMEOUT);
doReturn("1000").when(properties).getProperty(VAULT_RETRY_INTERVAL_MILLISECONDS);

vaultStoragePlugin.configure(properties);
clearInvocations(vaultClientProvider);

vaultStoragePlugin.lookup();

verify(vaultClientProvider, times(0)).getVaultClient();
}

@Test
public void lookUp_refreshesToken_when_currentTokenIsAboutToExpire() throws ConfigurationException, VaultException {
VaultStoragePlugin vaultStoragePlugin = spy(new VaultStoragePlugin());
Properties properties = mock(Properties.class);
VaultClientProvider vaultClientProvider = mock(VaultClientProvider.class);
Vault vault = mock(Vault.class);
Logical logical = mock(Logical.class);
Auth auth = mock(Auth.class);
LookupResponse response = mock(LookupResponse.class);

doReturn(vaultClientProvider).when(vaultStoragePlugin).getVaultClientProvider(properties);
doReturn(vault).when(vaultClientProvider).getVaultClient();
doReturn(logical).when(vault).logical();

doReturn(auth).when(vault).auth();
doReturn(response).when(auth).lookupSelf();
doReturn(20L).when(response).getTTL();

doReturn("rundeck").when(properties).getProperty(VAULT_PREFIX);
doReturn("approle").when(properties).getProperty(VAULT_SECRET_BACKEND);
doReturn("vault").when(properties).getProperty(VAULT_STORAGE_BEHAVIOUR);

doReturn("5").when(properties).getProperty(VAULT_MAX_RETRIES);
doReturn("2").when(properties).getProperty(VAULT_READ_TIMEOUT);
doReturn("2").when(properties).getProperty(VAULT_OPEN_TIMEOUT);
doReturn("1000").when(properties).getProperty(VAULT_RETRY_INTERVAL_MILLISECONDS);

vaultStoragePlugin.configure(properties);
clearInvocations(vaultClientProvider);

vaultStoragePlugin.lookup();

verify(vaultClientProvider).getVaultClient();
}

@Test
public void lookUp_refreshesToken_when_tokenIsExpired() throws ConfigurationException, VaultException {
VaultStoragePlugin vaultStoragePlugin = spy(new VaultStoragePlugin());
Properties properties = mock(Properties.class);
VaultClientProvider vaultClientProvider = mock(VaultClientProvider.class);
Vault vault = mock(Vault.class);
Logical logical = mock(Logical.class);
Auth auth = mock(Auth.class);

doReturn(vaultClientProvider).when(vaultStoragePlugin).getVaultClientProvider(properties);
doReturn(vault).when(vaultClientProvider).getVaultClient();
doReturn(logical).when(vault).logical();

doReturn(auth).when(vault).auth();
doThrow(new VaultException("Forbidden", 403)).when(auth).lookupSelf();

doReturn("rundeck").when(properties).getProperty(VAULT_PREFIX);
doReturn("approle").when(properties).getProperty(VAULT_SECRET_BACKEND);
doReturn("vault").when(properties).getProperty(VAULT_STORAGE_BEHAVIOUR);

doReturn("5").when(properties).getProperty(VAULT_MAX_RETRIES);
doReturn("2").when(properties).getProperty(VAULT_READ_TIMEOUT);
doReturn("2").when(properties).getProperty(VAULT_OPEN_TIMEOUT);
doReturn("1000").when(properties).getProperty(VAULT_RETRY_INTERVAL_MILLISECONDS);

vaultStoragePlugin.configure(properties);
clearInvocations(vaultClientProvider);

vaultStoragePlugin.lookup();

verify(vaultClientProvider).getVaultClient();
}
}

0 comments on commit a5d91dd

Please sign in to comment.