Skip to content

Commit

Permalink
Support creating child tokens for token credential binding
Browse files Browse the repository at this point in the history
  • Loading branch information
saville committed Sep 19, 2024
1 parent 9324022 commit 08c5dea
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 28 deletions.
4 changes: 2 additions & 2 deletions src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -161,7 +161,7 @@ private static StringSubstitutor getPolicyTokenSubstitutor(EnvVars envVars) {
return new StringSubstitutor(valueMap);
}

protected static List<String> generatePolicies(String policies, EnvVars envVars) {
public static List<String> generatePolicies(String policies, EnvVars envVars) {
if (StringUtils.isBlank(policies)) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ protected AbstractVaultTokenCredential(CredentialsScope scope, String id, String
protected abstract String getToken(Vault vault);

@Override
public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
public VaultAuthorizationResult authorizeWithVault(VaultConfig config, List<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private String getCacheKey(List<String> policies) {
}

@Override
public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
public VaultAuthorizationResult authorizeWithVault(VaultConfig config, List<String> policies) {
// Upgraded instances can have these not initialized in the constructor (serialized jobs possibly)
if (tokenCache == null) {
tokenCache = new HashMap<>();
Expand All @@ -129,7 +129,7 @@ public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
} else {
config.token(tokenCache.get(cacheKey));
}
return vault;
return new VaultAuthorizationResult(vault, config.getToken());
}

protected Vault getVault(VaultConfig config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
@NameWith(VaultCredential.NameProvider.class)
public interface VaultCredential extends StandardCredentials, Serializable {

Vault authorizeWithVault(VaultConfig config, List<String> policies);
VaultAuthorizationResult authorizeWithVault(VaultConfig config, List<String> policies);

class NameProvider extends CredentialsNameProvider<VaultCredential> {

Expand All @@ -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;

Check warning on line 39 in src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 39 is not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -29,7 +34,7 @@ public class VaultTokenCredentialBinding extends MultiBinding<AbstractVaultToken
private final static String DEFAULT_VAULT_TOKEN_VARIABLE_NAME = "VAULT_TOKEN";
private final static String DEFAULT_VAULT_NAMESPACE_VARIABLE_NAME = "VAULT_NAMESPACE";

@NonNull
private final String credentialsId;
private final String addrVariable;
private final String tokenVariable;
private final String vaultAddr;
Expand All @@ -47,6 +52,8 @@ public class VaultTokenCredentialBinding extends MultiBinding<AbstractVaultToken
public VaultTokenCredentialBinding(@Nullable String addrVariable,
@Nullable String tokenVariable, String credentialsId, String vaultAddr) {
super(credentialsId);
// The superclass field is private, so we need to store our own version
this.credentialsId = credentialsId;
this.vaultAddr = vaultAddr;
this.addrVariable = StringUtils
.defaultIfBlank(addrVariable, DEFAULT_VAULT_ADDR_VARIABLE_NAME);
Expand Down Expand Up @@ -94,32 +101,60 @@ protected Class<AbstractVaultTokenCredential> 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)) {

Check warning on line 113 in src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 113 is only partially covered, one branch is missing
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())+

Check warning on line 121 in src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 118-121 are not covered by tests
"' 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<String, String> 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())) {

Check warning on line 150 in src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 150 is only partially covered, one branch is missing
// 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();

Check warning on line 157 in src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 154-157 are not covered by tests
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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";
Expand Down

0 comments on commit 08c5dea

Please sign in to comment.