From a5d91dd70b506ba8965bbedf11e26b7d60fde52c Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Thu, 18 Nov 2021 16:29:07 +0100 Subject: [PATCH] Aquire new token if current token is about to expire 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. --- build.gradle | 6 +- .../plugin/vault/VaultStoragePlugin.java | 28 +++- .../plugin/vault/VaultStoragePluginTest.java | 155 ++++++++++++++++++ 3 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 src/test/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePluginTest.java diff --git a/build.gradle b/build.gradle index e4ed431..2633633 100644 --- a/build.gradle +++ b/build.gradle @@ -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'] ) } diff --git a/src/main/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePlugin.java b/src/main/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePlugin.java index 2d1ffe7..cf63cab 100644 --- a/src/main/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePlugin.java +++ b/src/main/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePlugin.java @@ -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; @@ -31,7 +32,6 @@ * @author ValFadeev * @since 2017-09-18 */ - @Plugin(name = "vault-storage", service = ServiceNameConstants.Storage) public class VaultStoragePlugin implements StoragePlugin, Configurable, Describable { @@ -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; @@ -66,7 +68,7 @@ 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 @@ -74,6 +76,22 @@ public void configure(Properties configuration) throws ConfigurationException { 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) { @@ -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); diff --git a/src/test/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePluginTest.java b/src/test/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePluginTest.java new file mode 100644 index 0000000..72e4f3f --- /dev/null +++ b/src/test/java/io/github/valfadeev/rundeck/plugin/vault/VaultStoragePluginTest.java @@ -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(); + } +}