Skip to content

Commit 08c5dea

Browse files
author
saville
committed
Support creating child tokens for token credential binding
1 parent 9324022 commit 08c5dea

7 files changed

Lines changed: 136 additions & 28 deletions

File tree

src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public VaultAccessor init() {
6666
if (credential == null) {
6767
vault = new Vault(config);
6868
} else {
69-
vault = credential.authorizeWithVault(config, policies);
69+
vault = credential.authorizeWithVault(config, policies).getVault();
7070
}
7171

7272
vault.withRetries(maxRetries, retryIntervalMilliseconds);
@@ -161,7 +161,7 @@ private static StringSubstitutor getPolicyTokenSubstitutor(EnvVars envVars) {
161161
return new StringSubstitutor(valueMap);
162162
}
163163

164-
protected static List<String> generatePolicies(String policies, EnvVars envVars) {
164+
public static List<String> generatePolicies(String policies, EnvVars envVars) {
165165
if (StringUtils.isBlank(policies)) {
166166
return null;
167167
}

src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredential.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ protected AbstractVaultTokenCredential(CredentialsScope scope, String id, String
1616
protected abstract String getToken(Vault vault);
1717

1818
@Override
19-
public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
19+
public VaultAuthorizationResult authorizeWithVault(VaultConfig config, List<String> policies) {
2020
Vault vault = new Vault(config);
21-
return new Vault(config.token(getToken(vault)));
21+
String token = getToken(vault);
22+
return new VaultAuthorizationResult(new Vault(config.token(token)), token);
2223
}
2324
}

src/main/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpiration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ private String getCacheKey(List<String> policies) {
105105
}
106106

107107
@Override
108-
public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
108+
public VaultAuthorizationResult authorizeWithVault(VaultConfig config, List<String> policies) {
109109
// Upgraded instances can have these not initialized in the constructor (serialized jobs possibly)
110110
if (tokenCache == null) {
111111
tokenCache = new HashMap<>();
@@ -129,7 +129,7 @@ public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
129129
} else {
130130
config.token(tokenCache.get(cacheKey));
131131
}
132-
return vault;
132+
return new VaultAuthorizationResult(vault, config.getToken());
133133
}
134134

135135
protected Vault getVault(VaultConfig config) {

src/main/java/com/datapipe/jenkins/vault/credentials/VaultCredential.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
@NameWith(VaultCredential.NameProvider.class)
1313
public interface VaultCredential extends StandardCredentials, Serializable {
1414

15-
Vault authorizeWithVault(VaultConfig config, List<String> policies);
15+
VaultAuthorizationResult authorizeWithVault(VaultConfig config, List<String> policies);
1616

1717
class NameProvider extends CredentialsNameProvider<VaultCredential> {
1818

@@ -21,4 +21,22 @@ public String getName(@NonNull VaultCredential credentials) {
2121
return credentials.getDescription();
2222
}
2323
}
24+
25+
final class VaultAuthorizationResult {
26+
private final Vault vault;
27+
private final String token;
28+
29+
public VaultAuthorizationResult(Vault vault, String token) {
30+
this.vault = vault;
31+
this.token = token;
32+
}
33+
34+
public Vault getVault() {
35+
return vault;
36+
}
37+
38+
public String getToken() {
39+
return token;
40+
}
41+
}
2442
}

src/main/java/com/datapipe/jenkins/vault/credentials/VaultTokenCredentialBinding.java

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package com.datapipe.jenkins.vault.credentials;
22

33
import com.bettercloud.vault.Vault;
4-
import com.bettercloud.vault.VaultConfig;
5-
import com.bettercloud.vault.VaultException;
6-
import com.datapipe.jenkins.vault.exception.VaultPluginException;
4+
import com.cloudbees.plugins.credentials.CredentialsProvider;
5+
import com.cloudbees.plugins.credentials.common.IdCredentials;
6+
import com.datapipe.jenkins.vault.VaultAccessor;
7+
import com.datapipe.jenkins.vault.configuration.VaultConfiguration;
78
import edu.umd.cs.findbugs.annotations.NonNull;
89
import edu.umd.cs.findbugs.annotations.Nullable;
910
import hudson.Extension;
1011
import hudson.FilePath;
1112
import hudson.Launcher;
13+
import hudson.model.Descriptor;
1214
import hudson.model.Run;
1315
import hudson.model.TaskListener;
1416
import java.io.IOException;
@@ -17,9 +19,12 @@
1719
import java.util.HashSet;
1820
import java.util.Map;
1921
import java.util.Set;
22+
import javax.annotation.Nonnull;
23+
import jenkins.model.Jenkins;
2024
import org.apache.commons.lang.StringUtils;
2125
import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor;
2226
import org.jenkinsci.plugins.credentialsbinding.MultiBinding;
27+
import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException;
2328
import org.kohsuke.stapler.DataBoundConstructor;
2429
import org.kohsuke.stapler.DataBoundSetter;
2530

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

32-
@NonNull
37+
private final String credentialsId;
3338
private final String addrVariable;
3439
private final String tokenVariable;
3540
private final String vaultAddr;
@@ -47,6 +52,8 @@ public class VaultTokenCredentialBinding extends MultiBinding<AbstractVaultToken
4752
public VaultTokenCredentialBinding(@Nullable String addrVariable,
4853
@Nullable String tokenVariable, String credentialsId, String vaultAddr) {
4954
super(credentialsId);
55+
// The superclass field is private, so we need to store our own version
56+
this.credentialsId = credentialsId;
5057
this.vaultAddr = vaultAddr;
5158
this.addrVariable = StringUtils
5259
.defaultIfBlank(addrVariable, DEFAULT_VAULT_ADDR_VARIABLE_NAME);
@@ -94,32 +101,60 @@ protected Class<AbstractVaultTokenCredential> type() {
94101
return AbstractVaultTokenCredential.class;
95102
}
96103

104+
private @Nonnull AbstractVaultTokenCredential getCredentials(@Nonnull Run<?,?> build,
105+
VaultConfiguration config) throws CredentialNotFoundException {
106+
// Copied and modified to pull the credentials ID from the Vault configuration
107+
IdCredentials cred = CredentialsProvider.findCredentialById(config.getVaultCredentialId(),
108+
IdCredentials.class, build);
109+
if (cred==null)
110+
throw new CredentialNotFoundException("Could not find credentials entry with ID '" +
111+
config.getVaultCredentialId() + "'");
112+
113+
if (type().isInstance(cred)) {
114+
CredentialsProvider.track(build, cred);
115+
return type().cast(cred);
116+
}
117+
118+
Descriptor expected = Jenkins.getActiveInstance().getDescriptor(type());
119+
throw new CredentialNotFoundException("Credentials '"+config.getVaultCredentialId()+"' is of type '"+
120+
cred.getDescriptor().getDisplayName()+"' where '"+
121+
(expected!=null ? expected.getDisplayName() : type().getName())+
122+
"' was expected");
123+
}
124+
97125
@Override
98126
public MultiEnvironment bind(@NonNull Run<?, ?> build, FilePath workspace, Launcher launcher,
99-
@NonNull TaskListener listener) throws IOException, InterruptedException {
100-
AbstractVaultTokenCredential credentials = getCredentials(build);
127+
@NonNull TaskListener listener) throws IOException {
128+
VaultConfiguration config = getVaultConfiguration(build);
129+
AbstractVaultTokenCredential credentials = getCredentials(build, config);
101130
Map<String, String> m = new HashMap<>();
102-
m.put(addrVariable, vaultAddr);
103-
m.put(namespaceVariable, vaultNamespace);
104-
String token = getToken(credentials);
131+
m.put(addrVariable, config.getVaultUrl());
132+
m.put(namespaceVariable, StringUtils.defaultString(config.getVaultNamespace()));
133+
String token = getToken(build, credentials, config);
105134
// don't add null token variable, can cause NPE in places where credential bindings impls
106135
// are not expecting null env var values.
107136
m.put(tokenVariable, StringUtils.defaultString(token));
108137
return new MultiEnvironment(m);
109138
}
110139

111-
private String getToken(AbstractVaultTokenCredential credentials) {
112-
try {
113-
VaultConfig config = new VaultConfig().address(vaultAddr);
114-
if (StringUtils.isNotEmpty(vaultNamespace)) {
115-
config.nameSpace(vaultNamespace);
116-
}
117-
config.build();
118-
119-
return credentials.getToken(new Vault(config));
120-
} catch (VaultException e) {
121-
throw new VaultPluginException("could not log in into vault", e);
140+
private VaultConfiguration getVaultConfiguration(Run<?, ?> build) {
141+
VaultConfiguration initialConfig = new VaultConfiguration();
142+
initialConfig.setVaultCredentialId(credentialsId);
143+
initialConfig.setVaultUrl(vaultAddr);
144+
initialConfig.setVaultNamespace(vaultNamespace);
145+
return VaultAccessor.pullAndMergeConfiguration(build, initialConfig);
146+
}
147+
148+
private String getToken(Run<?, ?> build, AbstractVaultTokenCredential credentials,
149+
VaultConfiguration config) {
150+
if (StringUtils.isBlank(config.getPolicies())) {
151+
// Use simpler method to get token if no policies are set
152+
return credentials.getToken(new Vault(config.getVaultConfig()));
122153
}
154+
return credentials.authorizeWithVault(
155+
config.getVaultConfig(),
156+
VaultAccessor.generatePolicies(config.getPolicies(), build.getCharacteristicEnvVars())
157+
).getToken();
123158
}
124159

125160
@Override

src/test/java/com/datapipe/jenkins/vault/it/VaultConfigurationIT.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.datapipe.jenkins.vault.configuration.VaultConfiguration;
1717
import com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential;
1818
import com.datapipe.jenkins.vault.credentials.VaultCredential;
19+
import com.datapipe.jenkins.vault.credentials.VaultCredential.VaultAuthorizationResult;
1920
import com.datapipe.jenkins.vault.credentials.VaultTokenCredential;
2021
import com.datapipe.jenkins.vault.model.VaultSecret;
2122
import com.datapipe.jenkins.vault.model.VaultSecretValue;
@@ -483,7 +484,8 @@ public static VaultAppRoleCredential createTokenCredential(final String credenti
483484
when(cred.getDescription()).thenReturn("description");
484485
when(cred.getRoleId()).thenReturn("role-id-" + credentialId);
485486
when(cred.getSecretId()).thenReturn(Secret.fromString("secret-id-" + credentialId));
486-
when(cred.authorizeWithVault(any(), eq(null))).thenReturn(vault);
487+
when(cred.authorizeWithVault(any(), eq(null))).thenReturn(
488+
new VaultAuthorizationResult(vault, "token-" + credentialId));
487489
return cred;
488490

489491
}

src/test/java/com/datapipe/jenkins/vault/it/VaultTokenCredentialBindingIT.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
package com.datapipe.jenkins.vault.it;
22

33
import com.bettercloud.vault.api.Auth;
4+
import com.cloudbees.hudson.plugins.folder.Folder;
45
import com.cloudbees.plugins.credentials.CredentialsProvider;
56
import com.cloudbees.plugins.credentials.CredentialsScope;
7+
import com.cloudbees.plugins.credentials.CredentialsStore;
68
import com.cloudbees.plugins.credentials.domains.Domain;
9+
import com.datapipe.jenkins.vault.configuration.FolderVaultConfiguration;
10+
import com.datapipe.jenkins.vault.configuration.VaultConfiguration;
711
import com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential;
812
import com.datapipe.jenkins.vault.credentials.VaultTokenCredential;
913
import hudson.FilePath;
1014
import hudson.model.Result;
1115
import hudson.util.Secret;
16+
import java.io.IOException;
1217
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
1318
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
1419
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
20+
import org.junit.Before;
1521
import org.junit.Rule;
1622
import org.junit.Test;
1723
import org.jvnet.hudson.test.JenkinsRule;
@@ -28,6 +34,18 @@ public class VaultTokenCredentialBindingIT {
2834
@Rule
2935
public JenkinsRule rule = new JenkinsRule();
3036

37+
@Before
38+
public void setUp() {
39+
CredentialsStore store = CredentialsProvider.lookupStores(rule.jenkins).iterator().next();
40+
store.getCredentials(Domain.global()).forEach(c -> {
41+
try {
42+
store.removeCredentials(Domain.global(), c);
43+
} catch (IOException e) {
44+
throw new RuntimeException(e);
45+
}
46+
});
47+
}
48+
3149
@Test
3250
public void shouldInjectCredentialsForAppRole() throws Exception {
3351
final String credentialsId = "creds";
@@ -131,6 +149,40 @@ public void shouldFailIfMissingVaultAddress() throws Exception {
131149
rule.assertLogNotContains(token, b);
132150
}
133151

152+
@Test
153+
public void shouldFallbackToFolderConfig() throws Exception {
154+
final String credentialsId = "creds";
155+
final String token = "fakeToken";
156+
final String jobId = "testJob";
157+
VaultTokenCredential c = new VaultTokenCredential(CredentialsScope.GLOBAL,
158+
credentialsId, "fake description", Secret.fromString(token));
159+
CredentialsProvider.lookupStores(rule.jenkins).iterator().next()
160+
.addCredentials(Domain.global(), c);
161+
162+
// Configure folder
163+
VaultConfiguration folderConfig = new VaultConfiguration();
164+
folderConfig.setVaultNamespace("testNamespace");
165+
folderConfig.setVaultUrl("https://test-vault");
166+
folderConfig.setVaultCredentialId(credentialsId);
167+
Folder folder = new Folder(rule.jenkins.getItemGroup(), "testFolder");
168+
rule.jenkins.add(folder, folder.getName());
169+
folder.addProperty(new FolderVaultConfiguration(folderConfig));
170+
WorkflowJob p = folder.createProject(WorkflowJob.class, jobId);
171+
p.setDefinition(new CpsFlowDefinition(""
172+
+ "node {\n"
173+
+ " withCredentials([[$class: 'VaultTokenCredentialBinding', addrVariable: 'VAULT_ADDR', tokenVariable: 'VAULT_TOKEN', namespaceVariable: 'VAULT_NAMESPACE']]) {\n"
174+
+ " " + getShellString() + " 'echo \"" + getVariable("VAULT_ADDR") + ":"
175+
+ getVariable("VAULT_TOKEN") + ":"
176+
+ getVariable("VAULT_NAMESPACE") + "\" > script'\n"
177+
+ " }\n"
178+
+ "}", true));
179+
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
180+
rule.assertBuildStatus(Result.SUCCESS, rule.waitForCompletion(b));
181+
rule.assertLogNotContains(token, b);
182+
rule.assertLogNotContains(folderConfig.getVaultNamespace(), b);
183+
rule.assertLogNotContains(folderConfig.getVaultUrl(), b);
184+
}
185+
134186
@Test
135187
public void shouldUseSpecifiedEnvironmentVariables() throws Exception {
136188
final String credentialsId = "creds";

0 commit comments

Comments
 (0)