Skip to content

Commit

Permalink
Merge pull request #38 from anrock-sc/fix-forbidden-errors-when-token…
Browse files Browse the repository at this point in the history
…-is-about-to-expire

Fix forbidden errors when token is about to expire
  • Loading branch information
ltamaster authored Nov 19, 2021
2 parents b84a456 + a5d91dd commit 651618b
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 10 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,14 @@ rundeck.storage.provider.[index].config.retryIntervalMilliseconds=1000

Default value: 1000

* **openTimeout**: Open timeout. Connection opening timeout, ms
* **openTimeout**: Open timeout. Connection opening timeout, in seconds
```
rundeck.storage.provider.[index].config.openTimeout=5
```

Default value: 5

* **readTimeout**: Read timeout. Response read timeout, ms
* **readTimeout**: Read timeout. Response read timeout, in seconds

```
rundeck.storage.provider.[index].config.readTimeout=20
Expand Down
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 @@ -133,19 +133,19 @@ static Description getDescription() {
.property(PropertyBuilder.builder()
.string(VAULT_RETRY_INTERVAL_MILLISECONDS)
.title("Retry interval")
.description("Connection retry interval, ms")
.description("Connection retry interval, in ms")
.defaultValue("1000")
)
.property(PropertyBuilder.builder()
.string(VAULT_OPEN_TIMEOUT)
.title("Open timeout")
.description("Connection opening timeout, ms")
.description("Connection opening timeout, in seconds")
.defaultValue("5")
)
.property(PropertyBuilder.builder()
.string(VAULT_READ_TIMEOUT)
.title("Read timeout")
.description("Response read timeout, ms")
.description("Response read timeout, in seconds")
.defaultValue("20")
)
.property(PropertyBuilder.builder()
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 651618b

Please sign in to comment.