From 08c5dea52b905c2812d38a6cdadce3d8a22161ca Mon Sep 17 00:00:00 2001 From: saville Date: Wed, 18 Sep 2024 19:45:19 -0600 Subject: [PATCH] Support creating child tokens for token credential binding --- .../datapipe/jenkins/vault/VaultAccessor.java | 4 +- .../AbstractVaultTokenCredential.java | 5 +- ...actVaultTokenCredentialWithExpiration.java | 4 +- .../vault/credentials/VaultCredential.java | 20 ++++- .../VaultTokenCredentialBinding.java | 75 ++++++++++++++----- .../vault/it/VaultConfigurationIT.java | 4 +- .../it/VaultTokenCredentialBindingIT.java | 52 +++++++++++++ 7 files changed, 136 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java index c09b3f89..4570143d 100644 --- a/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java +++ b/src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java @@ -66,7 +66,7 @@ public VaultAccessor init() { if (credential == null) { vault = new Vault(config); } else { - vault = credential.authorizeWithVault(config, policies); + vault = credential.authorizeWithVault(config, policies).getVault(); } vault.withRetries(maxRetries, retryIntervalMilliseconds); @@ -161,7 +161,7 @@ private static StringSubstitutor getPolicyTokenSubstitutor(EnvVars envVars) { return new StringSubstitutor(valueMap); } - protected static List generatePolicies(String policies, EnvVars envVars) { + public static List generatePolicies(String policies, EnvVars envVars) { if (StringUtils.isBlank(policies)) { return null; } diff --git a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredential.java b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredential.java index 738c4040..b6a6d64e 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredential.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredential.java @@ -16,8 +16,9 @@ protected AbstractVaultTokenCredential(CredentialsScope scope, String id, String protected abstract String getToken(Vault vault); @Override - public Vault authorizeWithVault(VaultConfig config, List policies) { + public VaultAuthorizationResult authorizeWithVault(VaultConfig config, List policies) { Vault vault = new Vault(config); - return new Vault(config.token(getToken(vault))); + String token = getToken(vault); + return new VaultAuthorizationResult(new Vault(config.token(token)), token); } } 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 cb330c87..372ddc47 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java @@ -105,7 +105,7 @@ private String getCacheKey(List policies) { } @Override - public Vault authorizeWithVault(VaultConfig config, List policies) { + public VaultAuthorizationResult authorizeWithVault(VaultConfig config, List policies) { // Upgraded instances can have these not initialized in the constructor (serialized jobs possibly) if (tokenCache == null) { tokenCache = new HashMap<>(); @@ -129,7 +129,7 @@ public Vault authorizeWithVault(VaultConfig config, List policies) { } else { config.token(tokenCache.get(cacheKey)); } - return vault; + return new VaultAuthorizationResult(vault, config.getToken()); } protected Vault getVault(VaultConfig config) { 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 bffc212e..fb37f282 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java @@ -12,7 +12,7 @@ @NameWith(VaultCredential.NameProvider.class) public interface VaultCredential extends StandardCredentials, Serializable { - Vault authorizeWithVault(VaultConfig config, List policies); + VaultAuthorizationResult authorizeWithVault(VaultConfig config, List policies); class NameProvider extends CredentialsNameProvider { @@ -21,4 +21,22 @@ public String getName(@NonNull VaultCredential credentials) { return credentials.getDescription(); } } + + final class VaultAuthorizationResult { + private final Vault vault; + private final String token; + + public VaultAuthorizationResult(Vault vault, String token) { + this.vault = vault; + this.token = token; + } + + public Vault getVault() { + return vault; + } + + public String getToken() { + return token; + } + } } diff --git a/src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java b/src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java index d8355d23..ddb47b96 100644 --- a/src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java +++ b/src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java @@ -1,14 +1,16 @@ package com.datapipe.jenkins.vault.credentials; import com.bettercloud.vault.Vault; -import com.bettercloud.vault.VaultConfig; -import com.bettercloud.vault.VaultException; -import com.datapipe.jenkins.vault.exception.VaultPluginException; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.datapipe.jenkins.vault.VaultAccessor; +import com.datapipe.jenkins.vault.configuration.VaultConfiguration; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; +import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; import java.io.IOException; @@ -17,9 +19,12 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor; import org.jenkinsci.plugins.credentialsbinding.MultiBinding; +import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -29,7 +34,7 @@ public class VaultTokenCredentialBinding extends MultiBinding type() { return AbstractVaultTokenCredential.class; } + private @Nonnull AbstractVaultTokenCredential getCredentials(@Nonnull Run build, + VaultConfiguration config) throws CredentialNotFoundException { + // Copied and modified to pull the credentials ID from the Vault configuration + IdCredentials cred = CredentialsProvider.findCredentialById(config.getVaultCredentialId(), + IdCredentials.class, build); + if (cred==null) + throw new CredentialNotFoundException("Could not find credentials entry with ID '" + + config.getVaultCredentialId() + "'"); + + if (type().isInstance(cred)) { + CredentialsProvider.track(build, cred); + return type().cast(cred); + } + + Descriptor expected = Jenkins.getActiveInstance().getDescriptor(type()); + throw new CredentialNotFoundException("Credentials '"+config.getVaultCredentialId()+"' is of type '"+ + cred.getDescriptor().getDisplayName()+"' where '"+ + (expected!=null ? expected.getDisplayName() : type().getName())+ + "' was expected"); + } + @Override public MultiEnvironment bind(@NonNull Run build, FilePath workspace, Launcher launcher, - @NonNull TaskListener listener) throws IOException, InterruptedException { - AbstractVaultTokenCredential credentials = getCredentials(build); + @NonNull TaskListener listener) throws IOException { + VaultConfiguration config = getVaultConfiguration(build); + AbstractVaultTokenCredential credentials = getCredentials(build, config); Map m = new HashMap<>(); - m.put(addrVariable, vaultAddr); - m.put(namespaceVariable, vaultNamespace); - String token = getToken(credentials); + m.put(addrVariable, config.getVaultUrl()); + m.put(namespaceVariable, StringUtils.defaultString(config.getVaultNamespace())); + String token = getToken(build, credentials, config); // don't add null token variable, can cause NPE in places where credential bindings impls // are not expecting null env var values. m.put(tokenVariable, StringUtils.defaultString(token)); return new MultiEnvironment(m); } - private String getToken(AbstractVaultTokenCredential credentials) { - try { - VaultConfig config = new VaultConfig().address(vaultAddr); - if (StringUtils.isNotEmpty(vaultNamespace)) { - config.nameSpace(vaultNamespace); - } - config.build(); - - return credentials.getToken(new Vault(config)); - } catch (VaultException e) { - throw new VaultPluginException("could not log in into vault", e); + private VaultConfiguration getVaultConfiguration(Run build) { + VaultConfiguration initialConfig = new VaultConfiguration(); + initialConfig.setVaultCredentialId(credentialsId); + initialConfig.setVaultUrl(vaultAddr); + initialConfig.setVaultNamespace(vaultNamespace); + return VaultAccessor.pullAndMergeConfiguration(build, initialConfig); + } + + private String getToken(Run build, AbstractVaultTokenCredential credentials, + VaultConfiguration config) { + if (StringUtils.isBlank(config.getPolicies())) { + // Use simpler method to get token if no policies are set + return credentials.getToken(new Vault(config.getVaultConfig())); } + return credentials.authorizeWithVault( + config.getVaultConfig(), + VaultAccessor.generatePolicies(config.getPolicies(), build.getCharacteristicEnvVars()) + ).getToken(); } @Override 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 3fb6c56b..c91a7b04 100644 --- a/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java +++ b/src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java @@ -16,6 +16,7 @@ import com.datapipe.jenkins.vault.configuration.VaultConfiguration; import com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential; import com.datapipe.jenkins.vault.credentials.VaultCredential; +import com.datapipe.jenkins.vault.credentials.VaultCredential.VaultAuthorizationResult; import com.datapipe.jenkins.vault.credentials.VaultTokenCredential; import com.datapipe.jenkins.vault.model.VaultSecret; import com.datapipe.jenkins.vault.model.VaultSecretValue; @@ -483,7 +484,8 @@ 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(), eq(null))).thenReturn(vault); + when(cred.authorizeWithVault(any(), eq(null))).thenReturn( + new VaultAuthorizationResult(vault, "token-" + credentialId)); return cred; } diff --git a/src/test/java/com/datapipe/jenkins/vault/it/VaultTokenCredentialBindingIT.java b/src/test/java/com/datapipe/jenkins/vault/it/VaultTokenCredentialBindingIT.java index 5e631126..992ab57b 100644 --- a/src/test/java/com/datapipe/jenkins/vault/it/VaultTokenCredentialBindingIT.java +++ b/src/test/java/com/datapipe/jenkins/vault/it/VaultTokenCredentialBindingIT.java @@ -1,17 +1,23 @@ package com.datapipe.jenkins.vault.it; import com.bettercloud.vault.api.Auth; +import com.cloudbees.hudson.plugins.folder.Folder; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.domains.Domain; +import com.datapipe.jenkins.vault.configuration.FolderVaultConfiguration; +import com.datapipe.jenkins.vault.configuration.VaultConfiguration; import com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential; import com.datapipe.jenkins.vault.credentials.VaultTokenCredential; import hudson.FilePath; import hudson.model.Result; import hudson.util.Secret; +import java.io.IOException; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -28,6 +34,18 @@ public class VaultTokenCredentialBindingIT { @Rule public JenkinsRule rule = new JenkinsRule(); + @Before + public void setUp() { + CredentialsStore store = CredentialsProvider.lookupStores(rule.jenkins).iterator().next(); + store.getCredentials(Domain.global()).forEach(c -> { + try { + store.removeCredentials(Domain.global(), c); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + @Test public void shouldInjectCredentialsForAppRole() throws Exception { final String credentialsId = "creds"; @@ -131,6 +149,40 @@ public void shouldFailIfMissingVaultAddress() throws Exception { rule.assertLogNotContains(token, b); } + @Test + public void shouldFallbackToFolderConfig() throws Exception { + final String credentialsId = "creds"; + final String token = "fakeToken"; + final String jobId = "testJob"; + VaultTokenCredential c = new VaultTokenCredential(CredentialsScope.GLOBAL, + credentialsId, "fake description", Secret.fromString(token)); + CredentialsProvider.lookupStores(rule.jenkins).iterator().next() + .addCredentials(Domain.global(), c); + + // Configure folder + VaultConfiguration folderConfig = new VaultConfiguration(); + folderConfig.setVaultNamespace("testNamespace"); + folderConfig.setVaultUrl("https://test-vault"); + folderConfig.setVaultCredentialId(credentialsId); + Folder folder = new Folder(rule.jenkins.getItemGroup(), "testFolder"); + rule.jenkins.add(folder, folder.getName()); + folder.addProperty(new FolderVaultConfiguration(folderConfig)); + WorkflowJob p = folder.createProject(WorkflowJob.class, jobId); + p.setDefinition(new CpsFlowDefinition("" + + "node {\n" + + " withCredentials([[$class: 'VaultTokenCredentialBinding', addrVariable: 'VAULT_ADDR', tokenVariable: 'VAULT_TOKEN', namespaceVariable: 'VAULT_NAMESPACE']]) {\n" + + " " + getShellString() + " 'echo \"" + getVariable("VAULT_ADDR") + ":" + + getVariable("VAULT_TOKEN") + ":" + + getVariable("VAULT_NAMESPACE") + "\" > script'\n" + + " }\n" + + "}", true)); + WorkflowRun b = p.scheduleBuild2(0).waitForStart(); + rule.assertBuildStatus(Result.SUCCESS, rule.waitForCompletion(b)); + rule.assertLogNotContains(token, b); + rule.assertLogNotContains(folderConfig.getVaultNamespace(), b); + rule.assertLogNotContains(folderConfig.getVaultUrl(), b); + } + @Test public void shouldUseSpecifiedEnvironmentVariables() throws Exception { final String credentialsId = "creds";