From b4b4edd02f2b39dce323e59d6fee6112deda00f5 Mon Sep 17 00:00:00 2001 From: saville Date: Mon, 20 Jun 2022 17:38:18 -0600 Subject: [PATCH 1/6] Fixes #214, adds support for separating job policies --- README.md | 17 +++ .../datapipe/jenkins/vault/VaultAccessor.java | 45 +++++++- .../configuration/VaultConfiguration.java | 15 +++ ...actAuthenticatingVaultTokenCredential.java | 9 +- .../AbstractVaultTokenCredential.java | 3 +- ...actVaultTokenCredentialWithExpiration.java | 109 ++++++++++++++---- .../vault/credentials/VaultCredential.java | 3 +- .../VaultConfiguration/config.jelly | 3 + .../VaultConfiguration/help-policies.html | 39 +++++++ .../jenkins/vault/VaultAccessorTest.java | 44 +++++++ ...uthenticatingVaultTokenCredentialTest.java | 8 ++ ...aultTokenCredentialWithExpirationTest.java | 82 +++++++++++-- .../vault/it/VaultConfigurationIT.java | 3 +- 13 files changed, 344 insertions(+), 36 deletions(-) create mode 100644 src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html create mode 100644 src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java diff --git a/README.md b/README.md index d3649648..5f3c3e78 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,23 @@ When registering the approle backend you can set a couple of different parameter * many more This is just a short introduction, please refer to [Hashicorp itself](https://www.vaultproject.io/docs/auth/approle.html) to get detailed information. + +### Isolating policies for different jobs +It may be desirable to have jobs or folders with separate Vault policies allocated. This may be done +with the optional `policies` configuration option combined with AppRole authentication. The workflow +would look like this: +* The Jenkins job attempts to retrieve a secret from Vault +* The AppRole authentication is used to retrieve a new token (if the old one has not expired yet) +* The Vault plugin then uses the `policies` configuration value with job info to come up with a list of policies +* If this list is not empty, the AppRole token is used to retrieve a new token that only has the specified policies applied +* This token is then used for all Vault plugin operations in the job + +The policies list may be templatized with values that can come from each job in order to customize +policies per job or folder. See the `policies` configuration help for more information on available +tokens to use in the configuration. Please note that the AppRole should have all policies configured +as `token_policies` and not `identity_policies`, as job-specific tokens inherit all +`identity_policies` automatically. + ### What about other backends? Hashicorp explicitly recommends the AppRole Backend for machine-to-machine authentication. Token based auth is mainly supported for backward compatibility. Other backends that might make sense are the AWS EC2 backend, the Azure backend, and the Kubernetes backend. But we do not support these yet. Feel free to contribute! diff --git a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java index 3fa216e3..510763fe 100644 --- a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java +++ b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java @@ -27,6 +27,7 @@ import java.io.PrintStream; import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -42,6 +43,7 @@ public class VaultAccessor implements Serializable { private VaultConfig config; private VaultCredential credential; + private List policies; private int maxRetries = 0; private int retryIntervalMilliseconds = 1000; @@ -63,7 +65,7 @@ public VaultAccessor init() { if (credential == null) { vault = new Vault(config); } else { - vault = credential.authorizeWithVault(config); + vault = credential.authorizeWithVault(config, policies); } vault.withRetries(maxRetries, retryIntervalMilliseconds); @@ -89,6 +91,14 @@ public void setCredential(VaultCredential credential) { this.credential = credential; } + public List getPolicies() { + return policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } + public int getMaxRetries() { return maxRetries; } @@ -130,6 +140,38 @@ public VaultResponse revoke(String leaseId) { } } + public static String replacePolicyTokens(String policy, EnvVars envVars) { + if (!policy.contains("{")) { + return policy; + } + String jobName = envVars.get("JOB_NAME"); + String jobBaseName = envVars.get("JOB_BASE_NAME"); + String folder = ""; + if (!jobName.equals(jobBaseName) && jobName.contains("/")) { + String[] jobElements = jobName.split("/"); + folder = Arrays.stream(jobElements) + .limit(jobElements.length - 1) + .collect(Collectors.joining("/")); + } + return policy + .replaceAll("\\{job_base_name}", jobBaseName) + .replaceAll("\\{job_name}", jobName) + .replaceAll("\\{job_name_us}", jobName.replaceAll("/", "_")) + .replaceAll("\\{job_folder}", folder) + .replaceAll("\\{job_folder_us}", folder.replaceAll("/", "_")) + .replaceAll("\\{node_name}", envVars.get("NODE_NAME")); + } + + public static List generatePolicies(String policies, EnvVars envVars) { + if (StringUtils.isBlank(policies)) { + return null; + } + return Arrays.stream(policies.split("\n")) + .filter(StringUtils::isNotBlank) + .map(policy -> replacePolicyTokens(policy.trim(), envVars)) + .collect(Collectors.toList()); + } + public static Map retrieveVaultSecrets(Run run, PrintStream logger, EnvVars envVars, VaultAccessor vaultAccessor, VaultConfiguration initialConfiguration, List vaultSecrets) { Map overrides = new HashMap<>(); @@ -156,6 +198,7 @@ public static Map retrieveVaultSecrets(Run run, PrintStream } vaultAccessor.setConfig(vaultConfig); vaultAccessor.setCredential(credential); + vaultAccessor.setPolicies(generatePolicies(config.getPolicies(), envVars)); vaultAccessor.setMaxRetries(config.getMaxRetries()); vaultAccessor.setRetryIntervalMilliseconds(config.getRetryIntervalMilliseconds()); vaultAccessor.init(); diff --git a/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java b/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java index da649eac..b8e03fbd 100644 --- a/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java +++ b/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java @@ -50,6 +50,8 @@ public class VaultConfiguration extends AbstractDescribableImpl policies) { Vault vault = new Vault(config); return new Vault(config.token(getToken(vault))); } diff --git a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java index 0667ac2e..a15323b8 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java @@ -3,36 +3,102 @@ import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.api.Auth; +import com.bettercloud.vault.api.Auth.TokenRequest; import com.cloudbees.plugins.credentials.CredentialsScope; +import com.datapipe.jenkins.vault.exception.VaultPluginException; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; public abstract class AbstractVaultTokenCredentialWithExpiration extends AbstractVaultTokenCredential { - private final static Logger LOGGER = Logger + protected final static Logger LOGGER = Logger .getLogger(AbstractVaultTokenCredentialWithExpiration.class.getName()); - private Calendar tokenExpiry; - private String currentClientToken; + private Map tokenExpiry; + private Map tokenCache; protected AbstractVaultTokenCredentialWithExpiration(CredentialsScope scope, String id, String description) { super(scope, id, description); + tokenExpiry = new HashMap<>(); + tokenCache = new HashMap<>(); } protected abstract String getToken(Vault vault); + /** + * Retrieve the Vault auth client. May be overridden in subclasses. + * @param vault the Vault instance + * @return the Vault auth client + */ + protected Auth getVaultAuth(@NonNull Vault vault) { + return vault.auth(); + } + + /** + * Retrieves a new token with specific policies if a list of requested policies is provided. + * @param vault the vault instance + * @param policies the policies list + * @return the new token or null if no policies are defined + */ + protected String getTokenWithPolicies(Vault vault, List policies) { + if (policies == null || policies.isEmpty()) { + return null; + } + Auth auth = getVaultAuth(vault); + try { + TokenRequest tokenRequest = (new TokenRequest()).polices(policies); + LOGGER.log(Level.FINE, "Requesting child token with policies {0}", + new Object[] {policies}); + return auth.createToken(tokenRequest).getAuthClientToken(); + } catch (VaultException e) { + throw new VaultPluginException("Could not retrieve token with policies from vault", e); + } + } + + /** + * Retrieves a key to be used for the token cache based on a list of policies. + * @param policies the list of policies + * @return the key to use for the map, either an empty string or a comma-separated list of policies + */ + private String getCacheKey(List policies) { + if (policies == null || policies.isEmpty()) { + return ""; + } + return String.join(",", policies); + } + @Override - public Vault authorizeWithVault(VaultConfig config) { + public Vault authorizeWithVault(VaultConfig config, List policies) { + // Upgraded instances can have these not initialized in the constructor (serialized jobs possibly) + if (tokenCache == null) { + tokenCache = new HashMap<>(); + tokenExpiry = new HashMap<>(); + } + + String cacheKey = getCacheKey(policies); Vault vault = getVault(config); - if (tokenExpired()) { - currentClientToken = getToken(vault); - config.token(currentClientToken); - setTokenExpiry(vault); + if (tokenExpired(cacheKey)) { + tokenCache.put(cacheKey, getToken(vault)); + config.token(tokenCache.get(cacheKey)); + + // After current token is configured, try to retrieve a new child token with limited policies + String childToken = getTokenWithPolicies(vault, policies); + if (childToken != null) { + // A new token was generated, put it in the cache and configure vault + tokenCache.put(cacheKey, childToken); + config.token(childToken); + } + setTokenExpiry(vault, cacheKey); } else { - config.token(currentClientToken); + config.token(tokenCache.get(cacheKey)); } return vault; } @@ -41,33 +107,36 @@ protected Vault getVault(VaultConfig config) { return new Vault(config); } - private void setTokenExpiry(Vault vault) { + private void setTokenExpiry(Vault vault, String cacheKey) { int tokenTTL = 0; try { - tokenTTL = (int) vault.auth().lookupSelf().getTTL(); + tokenTTL = (int) getVaultAuth(vault).lookupSelf().getTTL(); } catch (VaultException e) { - LOGGER.log(Level.WARNING, "Could not determine token expiration. " + - "Check if token is allowed to access auth/token/lookup-self. " + + LOGGER.log(Level.WARNING, "Could not determine token expiration for policies '" + + cacheKey + "'. Check if token is allowed to access auth/token/lookup-self. " + "Assuming token TTL expired.", e); } - tokenExpiry = Calendar.getInstance(); - tokenExpiry.add(Calendar.SECOND, tokenTTL); + Calendar expiry = Calendar.getInstance(); + expiry.add(Calendar.SECOND, tokenTTL); + tokenExpiry.put(cacheKey, expiry); } - private boolean tokenExpired() { - if (tokenExpiry == null) { + private boolean tokenExpired(String cacheKey) { + Calendar expiry = tokenExpiry.get(cacheKey); + if (expiry == null) { return true; } boolean result = true; Calendar now = Calendar.getInstance(); - long timeDiffInMillis = now.getTimeInMillis() - tokenExpiry.getTimeInMillis(); + long timeDiffInMillis = now.getTimeInMillis() - expiry.getTimeInMillis(); if (timeDiffInMillis < -10000L) { // token will be valid for at least another 10s result = false; - LOGGER.log(Level.FINE, "Auth token is still valid"); + LOGGER.log(Level.FINE, "Auth token is still valid for policies '" + cacheKey + "'"); } else { - LOGGER.log(Level.FINE, "Auth token has to be re-issued" + timeDiffInMillis); + LOGGER.log(Level.FINE,"Auth token has to be re-issued for policies '" + cacheKey + + "' (" + timeDiffInMillis + "ms difference)"); } return result; diff --git a/src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java b/src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java index f4891edb..bffc212e 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java @@ -7,11 +7,12 @@ import com.cloudbees.plugins.credentials.common.StandardCredentials; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.Serializable; +import java.util.List; @NameWith(VaultCredential.NameProvider.class) public interface VaultCredential extends StandardCredentials, Serializable { - Vault authorizeWithVault(VaultConfig config); + Vault authorizeWithVault(VaultConfig config, List policies); class NameProvider extends CredentialsNameProvider { diff --git a/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/config.jelly b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/config.jelly index 7b99e9e9..3bbd2f3a 100644 --- a/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/config.jelly +++ b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/config.jelly @@ -17,6 +17,9 @@ + + + diff --git a/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html new file mode 100644 index 00000000..917e2096 --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html @@ -0,0 +1,39 @@ +
+ The Vault policies to use when requesting a token for a job, separated by newlines. If left empty, + this will use all policies from the configured authentication. This is useful for + AppRole authentication where the AppRole can have many policies attached it and divide + up the policies per job based on the job folder or name. This allows you to restrict access on + specific jobs or folders. Each policy can use the following tokens to templatize the policies: +
    +
  • {job_base_name} - equal to the JOB_BASE_NAME env var
  • +
  • {job_name} - equal to the JOB_NAME env var
  • +
  • {job_name_us} - same as {job_name} with slashes converted to underscores
  • +
  • {job_folder} - the folder of the job (JOB_NAME - JOB_BASE_NAME without the trailing slash)
  • +
  • {job_folder_us} - same as {job_folder} with slashes converted to underscores
  • +
  • {node_name} - equal to the NODE_NAME env var
  • +
+ + For example, a policy list such as: +
    +
  • pol_jenkins_base
  • +
  • pol_jenkins_job_base_{job_base_name}
  • +
  • pol_jenkins_folder_us_{job_name_folder_us}
  • +
  • pol_jenkins/folder/{job_folder}
  • +
  • pol_jenkins_job_us_{job_name_us}
  • +
  • pol_jenkins/job/{job_name}
  • +
+ + Would result in six policies being applied to each job run. If the JOB_NAME was + "folder1/folder2/job1" and the JOB_BASE_NAME was "job1", the policies applied would be: +
    +
  • pol_jenkins_base
  • +
  • pol_jenkins_job_base_job1
  • +
  • pol_jenkins_folder_us_folder1_folder2
  • +
  • pol_jenkins/folder/folder1/folder2
  • +
  • pol_jenkins_job_us_folder1_folder2_job1
  • +
  • pol_jenkins/job/folder1/folder2/job1
  • +
+ + Please note that the AppRole should have all policies configured as token_policies and not + identity_policies, as job-specific tokens inherit all identity_policies automatically. +
diff --git a/src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java b/src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java new file mode 100644 index 00000000..96650c36 --- /dev/null +++ b/src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java @@ -0,0 +1,44 @@ +package com.datapipe.jenkins.vault; + +import hudson.EnvVars; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class VaultAccessorTest { + + private static final String POLICIES_STR = + "\npol1\n\nbase_{job_base_name}\njob/{job_name}\n job_{job_name_us}\nfolder/{job_folder}\nfolder_{job_folder_us}\nnode_{node_name}\n"; + + @Test + public void testGeneratePolicies() { + EnvVars envVars = mock(EnvVars.class); + when(envVars.get("JOB_NAME")).thenReturn("job1"); + when(envVars.get("JOB_BASE_NAME")).thenReturn("job1"); + when(envVars.get("NODE_NAME")).thenReturn("node1"); + + List policies = VaultAccessor.generatePolicies(POLICIES_STR, envVars); + assertThat(policies, equalTo(Arrays.asList( + "pol1", "base_job1", "job/job1", "job_job1", "folder/", "folder_", "node_node1" + ))); + } + + @Test + public void testGeneratePoliciesWithFolder() { + EnvVars envVars = mock(EnvVars.class); + when(envVars.get("JOB_NAME")).thenReturn("folder1/folder2/job1"); + when(envVars.get("JOB_BASE_NAME")).thenReturn("job1"); + when(envVars.get("NODE_NAME")).thenReturn("node1"); + + List policies = VaultAccessor.generatePolicies(POLICIES_STR, envVars); + assertThat(policies, equalTo(Arrays.asList( + "pol1", "base_job1", "job/folder1/folder2/job1", "job_folder1_folder2_job1", + "folder/folder1/folder2", "folder_folder1_folder2", "node_node1" + ))); + } +} diff --git a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractAuthenticatingVaultTokenCredentialTest.java b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractAuthenticatingVaultTokenCredentialTest.java index c70b3aca..dc40af0e 100644 --- a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractAuthenticatingVaultTokenCredentialTest.java +++ b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractAuthenticatingVaultTokenCredentialTest.java @@ -34,6 +34,14 @@ public void setUp() throws Exception { when(authResponse.getAuthClientToken()).thenReturn("12345"); } + @Test + public void nonRootNamespaceFromGetVaultAuth() { + ExampleVaultTokenCredential cred = new ExampleVaultTokenCredential(); + cred.setNamespace("foo"); + Auth authRet = cred.getVaultAuth(vault); + verify(authRet).withNameSpace("foo"); + } + @Test public void nonRootNamespace() { ExampleVaultTokenCredential cred = new ExampleVaultTokenCredential(); diff --git a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java index af43f458..89b13c23 100644 --- a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java +++ b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java @@ -4,14 +4,20 @@ import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; import com.bettercloud.vault.api.Auth; +import com.bettercloud.vault.api.Auth.TokenRequest; import com.bettercloud.vault.response.AuthResponse; import com.bettercloud.vault.response.LookupResponse; import com.cloudbees.plugins.credentials.CredentialsScope; import com.datapipe.jenkins.vault.exception.VaultPluginException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.junit.Before; import org.junit.Test; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -22,22 +28,27 @@ public class AbstractVaultTokenCredentialWithExpirationTest { private Vault vault; private VaultConfig vaultConfig; private Auth auth; - private AuthResponse authResponse; + private AuthResponse authResponse, childAuthResponse; private LookupResponse lookupResponse; private ExampleVaultTokenCredentialWithExpiration vaultTokenCredentialWithExpiration; + private List policies; @Before public void setUp() throws VaultException { + policies = Arrays.asList("pol1", "pol2"); vault = mock(Vault.class); vaultConfig = mock(VaultConfig.class); auth = mock(Auth.class); authResponse = mock(AuthResponse.class); + childAuthResponse = mock(AuthResponse.class); + when(auth.createToken(any(TokenRequest.class))).thenReturn(childAuthResponse); lookupResponse = mock(LookupResponse.class); vaultTokenCredentialWithExpiration = new ExampleVaultTokenCredentialWithExpiration(vault); when(vault.auth()).thenReturn(auth); when(auth.loginByCert()).thenReturn(authResponse); when(authResponse.getAuthClientToken()).thenReturn("fakeToken"); + when(childAuthResponse.getAuthClientToken()).thenReturn("childToken"); } @Test @@ -45,34 +56,85 @@ public void shouldBeAbleToFetchTokenOnInit() throws VaultException { when(auth.lookupSelf()).thenReturn(lookupResponse); when(lookupResponse.getTTL()).thenReturn(5L); - vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); verify(vaultConfig).token("fakeToken"); } + @Test + public void shouldFetchNewTokenForDifferentPolicies() throws VaultException { + when(auth.lookupSelf()).thenReturn(lookupResponse); + when(lookupResponse.getTTL()).thenReturn(5L); + when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2"); + when(childAuthResponse.getAuthClientToken()).thenReturn("childToken1", "childToken2"); + + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); + verify(vaultConfig).token("fakeToken1"); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies); + verify(vaultConfig).token("childToken1"); + } + + @Test + public void shouldNotFetchChildTokenIfEmptyPoliciesSpecified() throws VaultException { + when(authResponse.getAuthClientToken()).thenReturn("fakeToken"); + when(auth.lookupSelf()).thenReturn(lookupResponse); + when(lookupResponse.getTTL()).thenReturn(0L); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, new ArrayList<>()); + + verify(vaultConfig, times(1)).token(anyString()); + verify(vaultConfig).token("fakeToken"); + } + + @Test + public void shouldFetchChildTokenIfPoliciesSpecified() throws VaultException { + TokenRequest tokenRequest = (new TokenRequest()).polices(policies); + when(auth.createToken(argThat((TokenRequest tr) -> tokenRequest.getPolices() == policies))) + .thenReturn(childAuthResponse); + when(auth.lookupSelf()).thenReturn(lookupResponse); + when(lookupResponse.getTTL()).thenReturn(0L); + + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies); + + verify(vaultConfig, times(2)).token(anyString()); + verify(vaultConfig).token("fakeToken"); + verify(vaultConfig).token("childToken"); + } + @Test public void shouldReuseTheExistingTokenIfNotExpired() throws VaultException { + when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2"); + when(childAuthResponse.getAuthClientToken()).thenReturn("childToken1", "childToken2"); when(auth.lookupSelf()).thenReturn(lookupResponse); when(lookupResponse.getTTL()).thenReturn(5L); - vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig); - vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); + verify(vaultConfig, times(2)).token("fakeToken1"); - verify(vaultConfig, times(2)).token("fakeToken"); + // Different policies results in a new token + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies); + verify(vaultConfig, times(2)).token("childToken1"); } @Test public void shouldFetchNewTokenIfExpired() throws VaultException { when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2"); + when(childAuthResponse.getAuthClientToken()).thenReturn("childToken1", "childToken2"); when(auth.lookupSelf()).thenReturn(lookupResponse); when(lookupResponse.getTTL()).thenReturn(0L); - vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig); - vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig); - + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); verify(vaultConfig, times(2)).token(anyString()); verify(vaultConfig).token("fakeToken1"); verify(vaultConfig).token("fakeToken2"); + + // Different policies results in a new token + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies); + verify(vaultConfig).token("childToken1"); + verify(vaultConfig).token("childToken2"); } @Test @@ -80,8 +142,8 @@ public void shouldExpireTokenImmediatelyIfExceptionFetchingTTL() throws VaultExc when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2"); when(auth.lookupSelf()).thenThrow(new VaultException("Fail for testing")); - vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig); - vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); + vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); verify(vaultConfig, times(2)).token(anyString()); verify(vaultConfig).token("fakeToken1"); diff --git a/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java b/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java index 4f96c57f..5d393be1 100644 --- a/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java +++ b/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java @@ -51,6 +51,7 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; @@ -430,7 +431,7 @@ public static VaultAppRoleCredential createTokenCredential(final String credenti when(cred.getDescription()).thenReturn("description"); when(cred.getRoleId()).thenReturn("role-id-" + credentialId); when(cred.getSecretId()).thenReturn(Secret.fromString("secret-id-" + credentialId)); - when(cred.authorizeWithVault(any())).thenReturn(vault); + when(cred.authorizeWithVault(any(), eq(null))).thenReturn(vault); return cred; } From 3c99d78a193c79f5ee06a42c85be08c96524020a Mon Sep 17 00:00:00 2001 From: saville Date: Mon, 20 Jun 2022 20:43:49 -0600 Subject: [PATCH 2/6] Add configuration to credentials to enable using limited policies --- README.md | 9 ++--- ...actVaultTokenCredentialWithExpiration.java | 36 +++++++++++++++---- .../VaultAppRoleCredential/credentials.jelly | 3 ++ .../help-usePolicies.html | 5 +++ .../VaultAwsIamCredential/credentials.jelly | 3 ++ .../help-usePolicies.html | 5 +++ .../VaultGCPCredential/credentials.jelly | 3 ++ .../VaultGCPCredential/help-usePolicies.html | 5 +++ .../credentials.jelly | 3 ++ .../help-usePolicies.html | 5 +++ .../credentials.jelly | 3 ++ .../help-usePolicies.html | 5 +++ ...aultTokenCredentialWithExpirationTest.java | 1 + 13 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/help-usePolicies.html create mode 100644 src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/help-usePolicies.html create mode 100644 src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/help-usePolicies.html create mode 100644 src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/help-usePolicies.html create mode 100644 src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/help-usePolicies.html diff --git a/README.md b/README.md index 5f3c3e78..962453cf 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ This is just a short introduction, please refer to [Hashicorp itself](https://ww ### Isolating policies for different jobs It may be desirable to have jobs or folders with separate Vault policies allocated. This may be done -with the optional `policies` configuration option combined with AppRole authentication. The workflow -would look like this: +with the optional `policies` configuration option combined with authentication such as the AppRole +credential. The process is the following: * The Jenkins job attempts to retrieve a secret from Vault * The AppRole authentication is used to retrieve a new token (if the old one has not expired yet) * The Vault plugin then uses the `policies` configuration value with job info to come up with a list of policies @@ -34,8 +34,9 @@ would look like this: The policies list may be templatized with values that can come from each job in order to customize policies per job or folder. See the `policies` configuration help for more information on available -tokens to use in the configuration. Please note that the AppRole should have all policies configured -as `token_policies` and not `identity_policies`, as job-specific tokens inherit all +tokens to use in the configuration. The `Limit Token Policies` option must also be enabled on the +auth credential. Please note that the AppRole (or other authentication method) should have all policies +configured as `token_policies` and not `identity_policies`, as job-specific tokens inherit all `identity_policies` automatically. ### What about other backends? diff --git a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java index a15323b8..26fb6ccc 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java @@ -7,6 +7,7 @@ import com.bettercloud.vault.api.Auth.TokenRequest; import com.cloudbees.plugins.credentials.CredentialsScope; import com.datapipe.jenkins.vault.exception.VaultPluginException; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Calendar; import java.util.HashMap; @@ -14,6 +15,7 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import org.kohsuke.stapler.DataBoundSetter; public abstract class AbstractVaultTokenCredentialWithExpiration extends AbstractVaultTokenCredential { @@ -21,6 +23,27 @@ public abstract class AbstractVaultTokenCredentialWithExpiration protected final static Logger LOGGER = Logger .getLogger(AbstractVaultTokenCredentialWithExpiration.class.getName()); + @CheckForNull + private Boolean usePolicies; + + /** + * Get if the configured policies should be used or not. + * @return true if the policies should be used, false or null otherwise + */ + @CheckForNull + public Boolean getUsePolicies() { + return usePolicies; + } + + /** + * Set if the configured policies are used or not. + * @param usePolicies true if policies should be used, false otherwise + */ + @DataBoundSetter + public void setUsePolicies(Boolean usePolicies) { + this.usePolicies = usePolicies; + } + private Map tokenExpiry; private Map tokenCache; @@ -43,13 +66,14 @@ protected Auth getVaultAuth(@NonNull Vault vault) { } /** - * Retrieves a new token with specific policies if a list of requested policies is provided. + * Retrieves a new child token with specific policies if this credential is configured to use + * policies and a list of requested policies is provided. * @param vault the vault instance * @param policies the policies list - * @return the new token or null if no policies are defined + * @return the new token or null if it cannot be provisioned */ - protected String getTokenWithPolicies(Vault vault, List policies) { - if (policies == null || policies.isEmpty()) { + protected String getChildToken(Vault vault, List policies) { + if (usePolicies == null || !usePolicies || policies == null || policies.isEmpty()) { return null; } Auth auth = getVaultAuth(vault); @@ -59,7 +83,7 @@ protected String getTokenWithPolicies(Vault vault, List policies) { new Object[] {policies}); return auth.createToken(tokenRequest).getAuthClientToken(); } catch (VaultException e) { - throw new VaultPluginException("Could not retrieve token with policies from vault", e); + throw new VaultPluginException("Could not retrieve token with policies from Vault", e); } } @@ -90,7 +114,7 @@ public Vault authorizeWithVault(VaultConfig config, List policies) { config.token(tokenCache.get(cacheKey)); // After current token is configured, try to retrieve a new child token with limited policies - String childToken = getTokenWithPolicies(vault, policies); + String childToken = getChildToken(vault, policies); if (childToken != null) { // A new token was generated, put it in the cache and configure vault tokenCache.put(cacheKey, childToken); diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/credentials.jelly b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/credentials.jelly index 678f41e7..48324f51 100644 --- a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/credentials.jelly +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/credentials.jelly @@ -13,5 +13,8 @@ + + + diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/help-usePolicies.html b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/help-usePolicies.html new file mode 100644 index 00000000..5a31f35d --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/help-usePolicies.html @@ -0,0 +1,5 @@ +
+ If checked and policies are defined in the Vault plugin configuration, a child token will be + provisioned after authenticating with Vault with only the configured policies. See the Vault + plugin configuration policies for more information. +
diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/credentials.jelly b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/credentials.jelly index bd772bd9..05ad09a4 100644 --- a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/credentials.jelly +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/credentials.jelly @@ -13,6 +13,9 @@ + + + diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/help-usePolicies.html b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/help-usePolicies.html new file mode 100644 index 00000000..5a31f35d --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/help-usePolicies.html @@ -0,0 +1,5 @@ +
+ If checked and policies are defined in the Vault plugin configuration, a child token will be + provisioned after authenticating with Vault with only the configured policies. See the Vault + plugin configuration policies for more information. +
diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/credentials.jelly b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/credentials.jelly index e316b4db..62d36c75 100644 --- a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/credentials.jelly +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/credentials.jelly @@ -10,5 +10,8 @@ + + + diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/help-usePolicies.html b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/help-usePolicies.html new file mode 100644 index 00000000..5a31f35d --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/help-usePolicies.html @@ -0,0 +1,5 @@ +
+ If checked and policies are defined in the Vault plugin configuration, a child token will be + provisioned after authenticating with Vault with only the configured policies. See the Vault + plugin configuration policies for more information. +
diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/credentials.jelly b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/credentials.jelly index c29a7919..003e6b36 100644 --- a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/credentials.jelly +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/credentials.jelly @@ -10,5 +10,8 @@ + + + diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/help-usePolicies.html b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/help-usePolicies.html new file mode 100644 index 00000000..5a31f35d --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/help-usePolicies.html @@ -0,0 +1,5 @@ +
+ If checked and policies are defined in the Vault plugin configuration, a child token will be + provisioned after authenticating with Vault with only the configured policies. See the Vault + plugin configuration policies for more information. +
diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/credentials.jelly b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/credentials.jelly index effb1fdd..b9b15ce6 100644 --- a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/credentials.jelly +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/credentials.jelly @@ -10,6 +10,9 @@ + + + diff --git a/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/help-usePolicies.html b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/help-usePolicies.html new file mode 100644 index 00000000..5a31f35d --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/help-usePolicies.html @@ -0,0 +1,5 @@ +
+ If checked and policies are defined in the Vault plugin configuration, a child token will be + provisioned after authenticating with Vault with only the configured policies. See the Vault + plugin configuration policies for more information. +
diff --git a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java index 89b13c23..5de5926b 100644 --- a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java +++ b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java @@ -158,6 +158,7 @@ static class ExampleVaultTokenCredentialWithExpiration extends protected ExampleVaultTokenCredentialWithExpiration(Vault vault) { super(CredentialsScope.GLOBAL, "id", "description"); this.vault = vault; + this.setUsePolicies(true); } @Override From ed920da475bc543eae4cfeb7aa25ebe0a81cc6ab Mon Sep 17 00:00:00 2001 From: saville Date: Fri, 30 Sep 2022 10:42:46 -0600 Subject: [PATCH 3/6] Fix handling of TTL in child tokens --- ...tractVaultTokenCredentialWithExpiration.java | 17 +++++++++++++---- ...tVaultTokenCredentialWithExpirationTest.java | 9 +++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java index 26fb6ccc..2ff289f3 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java @@ -78,9 +78,13 @@ protected String getChildToken(Vault vault, List policies) { } Auth auth = getVaultAuth(vault); try { - TokenRequest tokenRequest = (new TokenRequest()).polices(policies); - LOGGER.log(Level.FINE, "Requesting child token with policies {0}", - new Object[] {policies}); + String ttl = String.format("%ds", getTokenTTL(vault)); + TokenRequest tokenRequest = (new TokenRequest()) + .polices(policies) + // Set the TTL to the parent token TTL + .ttl(ttl); + LOGGER.log(Level.FINE, "Requesting child token with policies {0} and TTL {1}", + new Object[] {policies, ttl}); return auth.createToken(tokenRequest).getAuthClientToken(); } catch (VaultException e) { throw new VaultPluginException("Could not retrieve token with policies from Vault", e); @@ -131,10 +135,14 @@ protected Vault getVault(VaultConfig config) { return new Vault(config); } + private long getTokenTTL(Vault vault) throws VaultException { + return getVaultAuth(vault).lookupSelf().getTTL(); + } + private void setTokenExpiry(Vault vault, String cacheKey) { int tokenTTL = 0; try { - tokenTTL = (int) getVaultAuth(vault).lookupSelf().getTTL(); + tokenTTL = (int) getTokenTTL(vault); } catch (VaultException e) { LOGGER.log(Level.WARNING, "Could not determine token expiration for policies '" + cacheKey + "'. Check if token is allowed to access auth/token/lookup-self. " + @@ -154,6 +162,7 @@ private boolean tokenExpired(String cacheKey) { boolean result = true; Calendar now = Calendar.getInstance(); long timeDiffInMillis = now.getTimeInMillis() - expiry.getTimeInMillis(); + LOGGER.log(Level.FINE, "Expiration for " + cacheKey + " is " + expiry + ", diff: " + timeDiffInMillis); if (timeDiffInMillis < -10000L) { // token will be valid for at least another 10s result = false; diff --git a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java index 5de5926b..716fef31 100644 --- a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java +++ b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java @@ -87,11 +87,12 @@ public void shouldNotFetchChildTokenIfEmptyPoliciesSpecified() throws VaultExcep @Test public void shouldFetchChildTokenIfPoliciesSpecified() throws VaultException { - TokenRequest tokenRequest = (new TokenRequest()).polices(policies); - when(auth.createToken(argThat((TokenRequest tr) -> tokenRequest.getPolices() == policies))) - .thenReturn(childAuthResponse); + when(auth.createToken(argThat((TokenRequest tr) -> + tr.getPolices() == policies && tr.getTtl().equals("30s") + ))).thenReturn(childAuthResponse); when(auth.lookupSelf()).thenReturn(lookupResponse); - when(lookupResponse.getTTL()).thenReturn(0L); + // First response is for parent, second is for child + when(lookupResponse.getTTL()).thenReturn(30L, 0L); vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies); From 2c55e5e5b26203a666fc89ea52e6ddce281c1b0e Mon Sep 17 00:00:00 2001 From: saville Date: Mon, 20 Nov 2023 08:46:53 -0700 Subject: [PATCH 4/6] Add ability to disable folders or jobs from overriding policies --- .../configuration/VaultConfiguration.java | 17 +++++- .../VaultConfiguration/config.jelly | 3 ++ .../help-disableChildPoliciesOverride.html | 4 ++ .../vault/it/VaultConfigurationIT.java | 52 +++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-disableChildPoliciesOverride.html diff --git a/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java b/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java index b8e03fbd..0d287798 100644 --- a/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java +++ b/src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java @@ -52,6 +52,8 @@ public class VaultConfiguration extends AbstractDescribableImpl + + + diff --git a/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-disableChildPoliciesOverride.html b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-disableChildPoliciesOverride.html new file mode 100644 index 00000000..9c75469a --- /dev/null +++ b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-disableChildPoliciesOverride.html @@ -0,0 +1,4 @@ +
+ If set, this will disable any child folder or job from overriding the job policies. + This prevents the escalation of privileges by subfolders or jobs. +
diff --git a/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java b/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java index 5d393be1..3fb6c56b 100644 --- a/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java +++ b/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java @@ -4,12 +4,14 @@ import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.response.LogicalResponse; import com.bettercloud.vault.rest.RestResponse; +import com.cloudbees.hudson.plugins.folder.Folder; import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.domains.Domain; import com.datapipe.jenkins.vault.VaultAccessor; import com.datapipe.jenkins.vault.VaultBuildWrapper; +import com.datapipe.jenkins.vault.configuration.FolderVaultConfiguration; import com.datapipe.jenkins.vault.configuration.GlobalVaultConfiguration; import com.datapipe.jenkins.vault.configuration.VaultConfiguration; import com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential; @@ -49,6 +51,8 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -153,6 +157,33 @@ public static String getVariable(String v) { return isWindows() ? "%" + v + "%" : "$" + v; } + private void assertOverridePolicies(String globalPolicies, Boolean globalDisableOverride, Boolean folderDisableOverride, + String policiesResult) throws Exception { + VaultConfiguration globalConfig = GlobalVaultConfiguration.get().getConfiguration(); + globalConfig.setPolicies(globalPolicies); + globalConfig.setDisableChildPoliciesOverride(globalDisableOverride); + + Folder folder = jenkins.createProject(Folder.class, "sub1"); + VaultConfiguration folderConfig = new VaultConfiguration(); + folderConfig.setPolicies("folder-policies"); + folderConfig.setDisableChildPoliciesOverride(folderDisableOverride); + folder.addProperty(new FolderVaultConfiguration(folderConfig)); + + FreeStyleProject project = folder.createProject(FreeStyleProject.class, "test"); + FreeStyleBuild build = mock(FreeStyleBuild.class); + when(build.getParent()).thenReturn(project); + VaultConfiguration vaultConfig = new VaultConfiguration(); + vaultConfig.setPolicies("job-policies"); + + assertThat(VaultAccessor.pullAndMergeConfiguration(build, vaultConfig).getPolicies(), + equalTo(policiesResult)); + } + + private void assertOverridePolicies(Boolean globalDisableOverride, Boolean folderDisableOverride, + String policiesResult) throws Exception { + assertOverridePolicies("global-policies", globalDisableOverride, folderDisableOverride, policiesResult); + } + @Test public void shouldUseGlobalConfiguration() throws Exception { List secrets = standardSecrets(); @@ -221,6 +252,27 @@ public void shouldUseJobConfiguration() throws Exception { verify(mockAccessor, times(1)).read("secret/path1", GLOBAL_ENGINE_VERSION_2); jenkins.assertLogContains("echo ****", build); jenkins.assertLogNotContains("some-secret", build); + assertThat(VaultAccessor.pullAndMergeConfiguration(build, vaultConfig).getPolicies(), nullValue()); + } + + @Test + public void shouldUseJobConfigurationWithoutDisableOverrides() throws Exception { + assertOverridePolicies(false, false, "job-policies"); + } + + @Test + public void shouldUseFolderConfigurationWithDisableOverrides() throws Exception { + assertOverridePolicies(false, true, "folder-policies"); + } + + @Test + public void shouldUseGlobalConfigurationWithDisableOverrides() throws Exception { + assertOverridePolicies(true, false, "global-policies"); + } + + @Test + public void shouldUseEmptyGlobalConfigurationWithDisableOverrides() throws Exception { + assertOverridePolicies(null, true, true, null); } @Test From 8d00c713530d8db1c5009ef6a64cf5fea3a9c93f Mon Sep 17 00:00:00 2001 From: saville Date: Mon, 20 Nov 2023 09:10:54 -0700 Subject: [PATCH 5/6] Use StringSubstitutor for templating policies --- .../datapipe/jenkins/vault/VaultAccessor.java | 27 +++++++++---------- .../VaultConfiguration/help-policies.html | 22 +++++++-------- .../jenkins/vault/VaultAccessorTest.java | 2 +- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java index 510763fe..c09b3f89 100644 --- a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java +++ b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java @@ -36,6 +36,7 @@ import java.util.stream.Collectors; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; +import org.apache.commons.text.StringSubstitutor; public class VaultAccessor implements Serializable { @@ -140,10 +141,7 @@ public VaultResponse revoke(String leaseId) { } } - public static String replacePolicyTokens(String policy, EnvVars envVars) { - if (!policy.contains("{")) { - return policy; - } + private static StringSubstitutor getPolicyTokenSubstitutor(EnvVars envVars) { String jobName = envVars.get("JOB_NAME"); String jobBaseName = envVars.get("JOB_BASE_NAME"); String folder = ""; @@ -153,22 +151,23 @@ public static String replacePolicyTokens(String policy, EnvVars envVars) { .limit(jobElements.length - 1) .collect(Collectors.joining("/")); } - return policy - .replaceAll("\\{job_base_name}", jobBaseName) - .replaceAll("\\{job_name}", jobName) - .replaceAll("\\{job_name_us}", jobName.replaceAll("/", "_")) - .replaceAll("\\{job_folder}", folder) - .replaceAll("\\{job_folder_us}", folder.replaceAll("/", "_")) - .replaceAll("\\{node_name}", envVars.get("NODE_NAME")); + Map valueMap = new HashMap<>(); + valueMap.put("job_base_name", jobBaseName); + valueMap.put("job_name", jobName); + valueMap.put("job_name_us", jobName.replaceAll("/", "_")); + valueMap.put("job_folder", folder); + valueMap.put("job_folder_us", folder.replaceAll("/", "_")); + valueMap.put("node_name", envVars.get("NODE_NAME")); + return new StringSubstitutor(valueMap); } - public static List generatePolicies(String policies, EnvVars envVars) { + protected static List generatePolicies(String policies, EnvVars envVars) { if (StringUtils.isBlank(policies)) { return null; } - return Arrays.stream(policies.split("\n")) + return Arrays.stream(getPolicyTokenSubstitutor(envVars).replace(policies).split("\n")) .filter(StringUtils::isNotBlank) - .map(policy -> replacePolicyTokens(policy.trim(), envVars)) + .map(String::trim) .collect(Collectors.toList()); } diff --git a/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html index 917e2096..3edb2dca 100644 --- a/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html +++ b/src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/help-policies.html @@ -5,22 +5,22 @@ up the policies per job based on the job folder or name. This allows you to restrict access on specific jobs or folders. Each policy can use the following tokens to templatize the policies:
    -
  • {job_base_name} - equal to the JOB_BASE_NAME env var
  • -
  • {job_name} - equal to the JOB_NAME env var
  • -
  • {job_name_us} - same as {job_name} with slashes converted to underscores
  • -
  • {job_folder} - the folder of the job (JOB_NAME - JOB_BASE_NAME without the trailing slash)
  • -
  • {job_folder_us} - same as {job_folder} with slashes converted to underscores
  • -
  • {node_name} - equal to the NODE_NAME env var
  • +
  • ${job_base_name} - equal to the JOB_BASE_NAME env var
  • +
  • ${job_name} - equal to the JOB_NAME env var
  • +
  • ${job_name_us} - same as ${job_name} with slashes converted to underscores
  • +
  • ${job_folder} - the folder of the job (JOB_NAME - JOB_BASE_NAME without the trailing slash)
  • +
  • ${job_folder_us} - same as ${job_folder} with slashes converted to underscores
  • +
  • ${node_name} - equal to the NODE_NAME env var
For example, a policy list such as:
  • pol_jenkins_base
  • -
  • pol_jenkins_job_base_{job_base_name}
  • -
  • pol_jenkins_folder_us_{job_name_folder_us}
  • -
  • pol_jenkins/folder/{job_folder}
  • -
  • pol_jenkins_job_us_{job_name_us}
  • -
  • pol_jenkins/job/{job_name}
  • +
  • pol_jenkins_job_base_${job_base_name}
  • +
  • pol_jenkins_folder_us_${job_name_folder_us}
  • +
  • pol_jenkins/folder/${job_folder}
  • +
  • pol_jenkins_job_us_${job_name_us}
  • +
  • pol_jenkins/job/${job_name}
Would result in six policies being applied to each job run. If the JOB_NAME was diff --git a/src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java b/src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java index 96650c36..cae8c568 100644 --- a/src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java +++ b/src/test/java/com/datapipe/jenkins/vault/VaultAccessorTest.java @@ -13,7 +13,7 @@ public class VaultAccessorTest { private static final String POLICIES_STR = - "\npol1\n\nbase_{job_base_name}\njob/{job_name}\n job_{job_name_us}\nfolder/{job_folder}\nfolder_{job_folder_us}\nnode_{node_name}\n"; + "\npol1\n\nbase_${job_base_name}\njob/${job_name}\n job_${job_name_us}\nfolder/${job_folder}\nfolder_${job_folder_us}\nnode_${node_name}\n"; @Test public void testGeneratePolicies() { From 820ef8230e26ebca6a2aae11c254ffc40c758875 Mon Sep 17 00:00:00 2001 From: saville Date: Mon, 20 Nov 2023 10:40:42 -0700 Subject: [PATCH 6/6] Fix flaky test --- .../AbstractVaultTokenCredentialWithExpirationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java index 716fef31..e77941c9 100644 --- a/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java +++ b/src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java @@ -106,7 +106,7 @@ public void shouldReuseTheExistingTokenIfNotExpired() throws VaultException { when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2"); when(childAuthResponse.getAuthClientToken()).thenReturn("childToken1", "childToken2"); when(auth.lookupSelf()).thenReturn(lookupResponse); - when(lookupResponse.getTTL()).thenReturn(5L); + when(lookupResponse.getTTL()).thenReturn(30L); vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null); vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);