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 forbidden errors when token is about to expire #38

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}