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(); + } +}