Skip to content

Commit f5d54b3

Browse files
bluesliverxsaville
and
saville
authoredNov 20, 2023
Adds support for using job-specific policies (#223)
* Fixes #214, adds support for separating job policies * Add configuration to credentials to enable using limited policies * Fix handling of TTL in child tokens * Add ability to disable folders or jobs from overriding policies * Use StringSubstitutor for templating policies * Fix flaky test --------- Co-authored-by: saville <[email protected]>
1 parent af8c162 commit f5d54b3

24 files changed

+492
-37
lines changed
 

‎README.md

+18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ When registering the approle backend you can set a couple of different parameter
2121
* many more
2222

2323
This is just a short introduction, please refer to [Hashicorp itself](https://www.vaultproject.io/docs/auth/approle.html) to get detailed information.
24+
25+
### Isolating policies for different jobs
26+
It may be desirable to have jobs or folders with separate Vault policies allocated. This may be done
27+
with the optional `policies` configuration option combined with authentication such as the AppRole
28+
credential. The process is the following:
29+
* The Jenkins job attempts to retrieve a secret from Vault
30+
* The AppRole authentication is used to retrieve a new token (if the old one has not expired yet)
31+
* The Vault plugin then uses the `policies` configuration value with job info to come up with a list of policies
32+
* If this list is not empty, the AppRole token is used to retrieve a new token that only has the specified policies applied
33+
* This token is then used for all Vault plugin operations in the job
34+
35+
The policies list may be templatized with values that can come from each job in order to customize
36+
policies per job or folder. See the `policies` configuration help for more information on available
37+
tokens to use in the configuration. The `Limit Token Policies` option must also be enabled on the
38+
auth credential. Please note that the AppRole (or other authentication method) should have all policies
39+
configured as `token_policies` and not `identity_policies`, as job-specific tokens inherit all
40+
`identity_policies` automatically.
41+
2442
### What about other backends?
2543
Hashicorp explicitly recommends the AppRole Backend for machine-to-machine authentication. Token based auth is mainly supported for backward compatibility.
2644
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!

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

+43-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.io.PrintStream;
2828
import java.io.Serializable;
2929
import java.nio.charset.StandardCharsets;
30+
import java.util.Arrays;
3031
import java.util.Collections;
3132
import java.util.HashMap;
3233
import java.util.List;
@@ -35,13 +36,15 @@
3536
import java.util.stream.Collectors;
3637
import jenkins.model.Jenkins;
3738
import org.apache.commons.lang.StringUtils;
39+
import org.apache.commons.text.StringSubstitutor;
3840

3941
public class VaultAccessor implements Serializable {
4042

4143
private static final long serialVersionUID = 1L;
4244

4345
private VaultConfig config;
4446
private VaultCredential credential;
47+
private List<String> policies;
4548
private int maxRetries = 0;
4649
private int retryIntervalMilliseconds = 1000;
4750

@@ -63,7 +66,7 @@ public VaultAccessor init() {
6366
if (credential == null) {
6467
vault = new Vault(config);
6568
} else {
66-
vault = credential.authorizeWithVault(config);
69+
vault = credential.authorizeWithVault(config, policies);
6770
}
6871

6972
vault.withRetries(maxRetries, retryIntervalMilliseconds);
@@ -89,6 +92,14 @@ public void setCredential(VaultCredential credential) {
8992
this.credential = credential;
9093
}
9194

95+
public List<String> getPolicies() {
96+
return policies;
97+
}
98+
99+
public void setPolicies(List<String> policies) {
100+
this.policies = policies;
101+
}
102+
92103
public int getMaxRetries() {
93104
return maxRetries;
94105
}
@@ -130,6 +141,36 @@ public VaultResponse revoke(String leaseId) {
130141
}
131142
}
132143

144+
private static StringSubstitutor getPolicyTokenSubstitutor(EnvVars envVars) {
145+
String jobName = envVars.get("JOB_NAME");
146+
String jobBaseName = envVars.get("JOB_BASE_NAME");
147+
String folder = "";
148+
if (!jobName.equals(jobBaseName) && jobName.contains("/")) {
149+
String[] jobElements = jobName.split("/");
150+
folder = Arrays.stream(jobElements)
151+
.limit(jobElements.length - 1)
152+
.collect(Collectors.joining("/"));
153+
}
154+
Map<String, String> valueMap = new HashMap<>();
155+
valueMap.put("job_base_name", jobBaseName);
156+
valueMap.put("job_name", jobName);
157+
valueMap.put("job_name_us", jobName.replaceAll("/", "_"));
158+
valueMap.put("job_folder", folder);
159+
valueMap.put("job_folder_us", folder.replaceAll("/", "_"));
160+
valueMap.put("node_name", envVars.get("NODE_NAME"));
161+
return new StringSubstitutor(valueMap);
162+
}
163+
164+
protected static List<String> generatePolicies(String policies, EnvVars envVars) {
165+
if (StringUtils.isBlank(policies)) {
166+
return null;
167+
}
168+
return Arrays.stream(getPolicyTokenSubstitutor(envVars).replace(policies).split("\n"))
169+
.filter(StringUtils::isNotBlank)
170+
.map(String::trim)
171+
.collect(Collectors.toList());
172+
}
173+
133174
public static Map<String, String> retrieveVaultSecrets(Run<?,?> run, PrintStream logger, EnvVars envVars, VaultAccessor vaultAccessor, VaultConfiguration initialConfiguration, List<VaultSecret> vaultSecrets) {
134175
Map<String, String> overrides = new HashMap<>();
135176

@@ -156,6 +197,7 @@ public static Map<String, String> retrieveVaultSecrets(Run<?,?> run, PrintStream
156197
}
157198
vaultAccessor.setConfig(vaultConfig);
158199
vaultAccessor.setCredential(credential);
200+
vaultAccessor.setPolicies(generatePolicies(config.getPolicies(), envVars));
159201
vaultAccessor.setMaxRetries(config.getMaxRetries());
160202
vaultAccessor.setRetryIntervalMilliseconds(config.getRetryIntervalMilliseconds());
161203
vaultAccessor.init();

‎src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java

+28
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public class VaultConfiguration extends AbstractDescribableImpl<VaultConfigurati
5050

5151
private String prefixPath;
5252

53+
private String policies;
54+
55+
private Boolean disableChildPoliciesOverride;
56+
5357
private Integer timeout = DEFAULT_TIMEOUT;
5458

5559
@DataBoundConstructor
@@ -73,6 +77,8 @@ public VaultConfiguration(VaultConfiguration toCopy) {
7377
this.engineVersion = toCopy.engineVersion;
7478
this.vaultNamespace = toCopy.vaultNamespace;
7579
this.prefixPath = toCopy.prefixPath;
80+
this.policies = toCopy.policies;
81+
this.disableChildPoliciesOverride = toCopy.disableChildPoliciesOverride;
7682
this.timeout = toCopy.timeout;
7783
}
7884

@@ -99,6 +105,10 @@ public VaultConfiguration mergeWithParent(VaultConfiguration parent) {
99105
if (StringUtils.isBlank(result.getPrefixPath())) {
100106
result.setPrefixPath(parent.getPrefixPath());
101107
}
108+
if (StringUtils.isBlank(result.getPolicies()) ||
109+
(parent.getDisableChildPoliciesOverride() != null && parent.getDisableChildPoliciesOverride())) {
110+
result.setPolicies(parent.getPolicies());
111+
}
102112
if (result.timeout == null) {
103113
result.setTimeout(parent.getTimeout());
104114
}
@@ -183,6 +193,24 @@ public void setPrefixPath(String prefixPath) {
183193
this.prefixPath = fixEmptyAndTrim(prefixPath);
184194
}
185195

196+
public String getPolicies() {
197+
return policies;
198+
}
199+
200+
@DataBoundSetter
201+
public void setPolicies(String policies) {
202+
this.policies = fixEmptyAndTrim(policies);
203+
}
204+
205+
public Boolean getDisableChildPoliciesOverride() {
206+
return disableChildPoliciesOverride;
207+
}
208+
209+
@DataBoundSetter
210+
public void setDisableChildPoliciesOverride(Boolean disableChildPoliciesOverride) {
211+
this.disableChildPoliciesOverride = disableChildPoliciesOverride;
212+
}
213+
186214
public Integer getTimeout() {
187215
return timeout;
188216
}

‎src/main/java/com/datapipe/jenkins/vault/credentials/AbstractAuthenticatingVaultTokenCredential.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void setNamespace(String namespace) {
4646
}
4747

4848
@Override
49-
protected final String getToken(Vault vault) {
49+
protected Auth getVaultAuth(@NonNull Vault vault) {
5050
// set authentication namespace if configured for this credential.
5151
// importantly, this will not effect the underlying VaultConfig namespace.
5252
Auth auth = vault.auth();
@@ -57,7 +57,12 @@ protected final String getToken(Vault vault) {
5757
auth.withNameSpace(null);
5858
}
5959
}
60-
return getToken(auth);
60+
return auth;
61+
}
62+
63+
@Override
64+
protected final String getToken(Vault vault) {
65+
return getToken(getVaultAuth(vault));
6166
}
6267

6368
/**

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.bettercloud.vault.VaultConfig;
55
import com.cloudbees.plugins.credentials.CredentialsScope;
66
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
7+
import java.util.List;
78

89
public abstract class AbstractVaultTokenCredential
910
extends BaseStandardCredentials implements VaultCredential {
@@ -15,7 +16,7 @@ protected AbstractVaultTokenCredential(CredentialsScope scope, String id, String
1516
protected abstract String getToken(Vault vault);
1617

1718
@Override
18-
public Vault authorizeWithVault(VaultConfig config) {
19+
public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
1920
Vault vault = new Vault(config);
2021
return new Vault(config.token(getToken(vault)));
2122
}

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

+122-20
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,130 @@
33
import com.bettercloud.vault.Vault;
44
import com.bettercloud.vault.VaultConfig;
55
import com.bettercloud.vault.VaultException;
6+
import com.bettercloud.vault.api.Auth;
7+
import com.bettercloud.vault.api.Auth.TokenRequest;
68
import com.cloudbees.plugins.credentials.CredentialsScope;
9+
import com.datapipe.jenkins.vault.exception.VaultPluginException;
10+
import edu.umd.cs.findbugs.annotations.CheckForNull;
11+
import edu.umd.cs.findbugs.annotations.NonNull;
712
import java.util.Calendar;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
816
import java.util.logging.Level;
917
import java.util.logging.Logger;
18+
import org.kohsuke.stapler.DataBoundSetter;
1019

1120
public abstract class AbstractVaultTokenCredentialWithExpiration
1221
extends AbstractVaultTokenCredential {
1322

14-
private final static Logger LOGGER = Logger
23+
protected final static Logger LOGGER = Logger
1524
.getLogger(AbstractVaultTokenCredentialWithExpiration.class.getName());
1625

17-
private Calendar tokenExpiry;
18-
private String currentClientToken;
26+
@CheckForNull
27+
private Boolean usePolicies;
28+
29+
/**
30+
* Get if the configured policies should be used or not.
31+
* @return true if the policies should be used, false or null otherwise
32+
*/
33+
@CheckForNull
34+
public Boolean getUsePolicies() {
35+
return usePolicies;
36+
}
37+
38+
/**
39+
* Set if the configured policies are used or not.
40+
* @param usePolicies true if policies should be used, false otherwise
41+
*/
42+
@DataBoundSetter
43+
public void setUsePolicies(Boolean usePolicies) {
44+
this.usePolicies = usePolicies;
45+
}
46+
47+
private Map<String, Calendar> tokenExpiry;
48+
private Map<String, String> tokenCache;
1949

2050
protected AbstractVaultTokenCredentialWithExpiration(CredentialsScope scope, String id,
2151
String description) {
2252
super(scope, id, description);
53+
tokenExpiry = new HashMap<>();
54+
tokenCache = new HashMap<>();
2355
}
2456

2557
protected abstract String getToken(Vault vault);
2658

59+
/**
60+
* Retrieve the Vault auth client. May be overridden in subclasses.
61+
* @param vault the Vault instance
62+
* @return the Vault auth client
63+
*/
64+
protected Auth getVaultAuth(@NonNull Vault vault) {
65+
return vault.auth();
66+
}
67+
68+
/**
69+
* Retrieves a new child token with specific policies if this credential is configured to use
70+
* policies and a list of requested policies is provided.
71+
* @param vault the vault instance
72+
* @param policies the policies list
73+
* @return the new token or null if it cannot be provisioned
74+
*/
75+
protected String getChildToken(Vault vault, List<String> policies) {
76+
if (usePolicies == null || !usePolicies || policies == null || policies.isEmpty()) {
77+
return null;
78+
}
79+
Auth auth = getVaultAuth(vault);
80+
try {
81+
String ttl = String.format("%ds", getTokenTTL(vault));
82+
TokenRequest tokenRequest = (new TokenRequest())
83+
.polices(policies)
84+
// Set the TTL to the parent token TTL
85+
.ttl(ttl);
86+
LOGGER.log(Level.FINE, "Requesting child token with policies {0} and TTL {1}",
87+
new Object[] {policies, ttl});
88+
return auth.createToken(tokenRequest).getAuthClientToken();
89+
} catch (VaultException e) {
90+
throw new VaultPluginException("Could not retrieve token with policies from Vault", e);
91+
}
92+
}
93+
94+
/**
95+
* Retrieves a key to be used for the token cache based on a list of policies.
96+
* @param policies the list of policies
97+
* @return the key to use for the map, either an empty string or a comma-separated list of policies
98+
*/
99+
private String getCacheKey(List<String> policies) {
100+
if (policies == null || policies.isEmpty()) {
101+
return "";
102+
}
103+
return String.join(",", policies);
104+
}
105+
27106
@Override
28-
public Vault authorizeWithVault(VaultConfig config) {
107+
public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
108+
// Upgraded instances can have these not initialized in the constructor (serialized jobs possibly)
109+
if (tokenCache == null) {
110+
tokenCache = new HashMap<>();
111+
tokenExpiry = new HashMap<>();
112+
}
113+
114+
String cacheKey = getCacheKey(policies);
29115
Vault vault = getVault(config);
30-
if (tokenExpired()) {
31-
currentClientToken = getToken(vault);
32-
config.token(currentClientToken);
33-
setTokenExpiry(vault);
116+
if (tokenExpired(cacheKey)) {
117+
tokenCache.put(cacheKey, getToken(vault));
118+
config.token(tokenCache.get(cacheKey));
119+
120+
// After current token is configured, try to retrieve a new child token with limited policies
121+
String childToken = getChildToken(vault, policies);
122+
if (childToken != null) {
123+
// A new token was generated, put it in the cache and configure vault
124+
tokenCache.put(cacheKey, childToken);
125+
config.token(childToken);
126+
}
127+
setTokenExpiry(vault, cacheKey);
34128
} else {
35-
config.token(currentClientToken);
129+
config.token(tokenCache.get(cacheKey));
36130
}
37131
return vault;
38132
}
@@ -41,33 +135,41 @@ protected Vault getVault(VaultConfig config) {
41135
return new Vault(config);
42136
}
43137

44-
private void setTokenExpiry(Vault vault) {
138+
private long getTokenTTL(Vault vault) throws VaultException {
139+
return getVaultAuth(vault).lookupSelf().getTTL();
140+
}
141+
142+
private void setTokenExpiry(Vault vault, String cacheKey) {
45143
int tokenTTL = 0;
46144
try {
47-
tokenTTL = (int) vault.auth().lookupSelf().getTTL();
145+
tokenTTL = (int) getTokenTTL(vault);
48146
} catch (VaultException e) {
49-
LOGGER.log(Level.WARNING, "Could not determine token expiration. " +
50-
"Check if token is allowed to access auth/token/lookup-self. " +
147+
LOGGER.log(Level.WARNING, "Could not determine token expiration for policies '" +
148+
cacheKey + "'. Check if token is allowed to access auth/token/lookup-self. " +
51149
"Assuming token TTL expired.", e);
52150
}
53-
tokenExpiry = Calendar.getInstance();
54-
tokenExpiry.add(Calendar.SECOND, tokenTTL);
151+
Calendar expiry = Calendar.getInstance();
152+
expiry.add(Calendar.SECOND, tokenTTL);
153+
tokenExpiry.put(cacheKey, expiry);
55154
}
56155

57-
private boolean tokenExpired() {
58-
if (tokenExpiry == null) {
156+
private boolean tokenExpired(String cacheKey) {
157+
Calendar expiry = tokenExpiry.get(cacheKey);
158+
if (expiry == null) {
59159
return true;
60160
}
61161

62162
boolean result = true;
63163
Calendar now = Calendar.getInstance();
64-
long timeDiffInMillis = now.getTimeInMillis() - tokenExpiry.getTimeInMillis();
164+
long timeDiffInMillis = now.getTimeInMillis() - expiry.getTimeInMillis();
165+
LOGGER.log(Level.FINE, "Expiration for " + cacheKey + " is " + expiry + ", diff: " + timeDiffInMillis);
65166
if (timeDiffInMillis < -10000L) {
66167
// token will be valid for at least another 10s
67168
result = false;
68-
LOGGER.log(Level.FINE, "Auth token is still valid");
169+
LOGGER.log(Level.FINE, "Auth token is still valid for policies '" + cacheKey + "'");
69170
} else {
70-
LOGGER.log(Level.FINE, "Auth token has to be re-issued" + timeDiffInMillis);
171+
LOGGER.log(Level.FINE,"Auth token has to be re-issued for policies '" + cacheKey +
172+
"' (" + timeDiffInMillis + "ms difference)");
71173
}
72174

73175
return result;

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
import com.cloudbees.plugins.credentials.common.StandardCredentials;
88
import edu.umd.cs.findbugs.annotations.NonNull;
99
import java.io.Serializable;
10+
import java.util.List;
1011

1112
@NameWith(VaultCredential.NameProvider.class)
1213
public interface VaultCredential extends StandardCredentials, Serializable {
1314

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

1617
class NameProvider extends CredentialsNameProvider<VaultCredential> {
1718

‎src/main/resources/com/datapipe/jenkins/vault/configuration/VaultConfiguration/config.jelly

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
<f:entry field="engineVersion" title="K/V Engine Version">
1818
<f:select/>
1919
</f:entry>
20+
<f:entry title="(Optional) Job Policies" field="policies">
21+
<f:textarea/>
22+
</f:entry>
23+
<f:entry title="(Optional) Disable Policies Override" field="disableChildPoliciesOverride">
24+
<f:checkbox/>
25+
</f:entry>
2026
<f:entry title="Fail if path is not found" field="failIfNotFound">
2127
<f:checkbox default="${descriptor.DEFAULT_FAIL_NOT_FOUND}"/>
2228
</f:entry>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div>
2+
If set, this will disable any child folder or job from overriding the job policies.
3+
This prevents the escalation of privileges by subfolders or jobs.
4+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<div>
2+
The Vault policies to use when requesting a token for a job, separated by newlines. If left empty,
3+
this will use all policies from the configured authentication. This is useful for
4+
AppRole authentication where the AppRole can have many policies attached it and divide
5+
up the policies per job based on the job folder or name. This allows you to restrict access on
6+
specific jobs or folders. Each policy can use the following tokens to templatize the policies:
7+
<ul>
8+
<li>${job_base_name} - equal to the JOB_BASE_NAME env var</li>
9+
<li>${job_name} - equal to the JOB_NAME env var</li>
10+
<li>${job_name_us} - same as ${job_name} with slashes converted to underscores</li>
11+
<li>${job_folder} - the folder of the job (JOB_NAME - JOB_BASE_NAME without the trailing slash)</li>
12+
<li>${job_folder_us} - same as ${job_folder} with slashes converted to underscores</li>
13+
<li>${node_name} - equal to the NODE_NAME env var</li>
14+
</ul>
15+
16+
For example, a policy list such as:
17+
<ul>
18+
<li>pol_jenkins_base</li>
19+
<li>pol_jenkins_job_base_${job_base_name}</li>
20+
<li>pol_jenkins_folder_us_${job_name_folder_us}</li>
21+
<li>pol_jenkins/folder/${job_folder}</li>
22+
<li>pol_jenkins_job_us_${job_name_us}</li>
23+
<li>pol_jenkins/job/${job_name}</li>
24+
</ul>
25+
26+
Would result in six policies being applied to each job run. If the JOB_NAME was
27+
"folder1/folder2/job1" and the JOB_BASE_NAME was "job1", the policies applied would be:
28+
<ul>
29+
<li>pol_jenkins_base</li>
30+
<li>pol_jenkins_job_base_job1</li>
31+
<li>pol_jenkins_folder_us_folder1_folder2</li>
32+
<li>pol_jenkins/folder/folder1/folder2</li>
33+
<li>pol_jenkins_job_us_folder1_folder2_job1</li>
34+
<li>pol_jenkins/job/folder1/folder2/job1</li>
35+
</ul>
36+
37+
Please note that the AppRole should have all policies configured as token_policies and not
38+
identity_policies, as job-specific tokens inherit all identity_policies automatically.
39+
</div>

‎src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAppRoleCredential/credentials.jelly

+3
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@
1313
<f:entry title="${%Namespace}" field="namespace">
1414
<f:textbox/>
1515
</f:entry>
16+
<f:entry field="usePolicies" title="Limit Token Policies">
17+
<f:checkbox/>
18+
</f:entry>
1619
<st:include page="id-and-description" class="${descriptor.clazz}"/>
1720
</j:jelly>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
If checked and policies are defined in the Vault plugin configuration, a child token will be
3+
provisioned after authenticating with Vault with only the configured policies. See the Vault
4+
plugin configuration policies for more information.
5+
</div>

‎src/main/resources/com/datapipe/jenkins/vault/credentials/VaultAwsIamCredential/credentials.jelly

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
<f:entry title="${%Namespace}" field="namespace">
1414
<f:textbox/>
1515
</f:entry>
16+
<f:entry field="usePolicies" title="Limit Token Policies">
17+
<f:checkbox/>
18+
</f:entry>
1619

1720
<st:include page="id-and-description" class="${descriptor.clazz}"/>
1821
</j:jelly>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
If checked and policies are defined in the Vault plugin configuration, a child token will be
3+
provisioned after authenticating with Vault with only the configured policies. See the Vault
4+
plugin configuration policies for more information.
5+
</div>

‎src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGCPCredential/credentials.jelly

+3
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
<f:entry title="${%Namespace}" field="namespace">
1111
<f:textbox/>
1212
</f:entry>
13+
<f:entry field="usePolicies" title="Limit Token Policies">
14+
<f:checkbox/>
15+
</f:entry>
1316
<st:include page="id-and-description" class="${descriptor.clazz}"/>
1417
</j:jelly>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
If checked and policies are defined in the Vault plugin configuration, a child token will be
3+
provisioned after authenticating with Vault with only the configured policies. See the Vault
4+
plugin configuration policies for more information.
5+
</div>

‎src/main/resources/com/datapipe/jenkins/vault/credentials/VaultGithubTokenCredential/credentials.jelly

+3
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
<f:entry title="Mount Path">
1111
<f:textbox field="mountPath" name="mountPath" default="${descriptor.defaultPath}"/>
1212
</f:entry>
13+
<f:entry field="usePolicies" title="Limit Token Policies">
14+
<f:checkbox/>
15+
</f:entry>
1316
<st:include page="id-and-description" class="${descriptor.clazz}"/>
1417
</j:jelly>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
If checked and policies are defined in the Vault plugin configuration, a child token will be
3+
provisioned after authenticating with Vault with only the configured policies. See the Vault
4+
plugin configuration policies for more information.
5+
</div>

‎src/main/resources/com/datapipe/jenkins/vault/credentials/VaultKubernetesCredential/credentials.jelly

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
<f:entry title="${%Namespace}" field="namespace">
1111
<f:textbox/>
1212
</f:entry>
13+
<f:entry field="usePolicies" title="Limit Token Policies">
14+
<f:checkbox/>
15+
</f:entry>
1316

1417
<st:include page="id-and-description" class="${descriptor.clazz}"/>
1518
</j:jelly>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
If checked and policies are defined in the Vault plugin configuration, a child token will be
3+
provisioned after authenticating with Vault with only the configured policies. See the Vault
4+
plugin configuration policies for more information.
5+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.datapipe.jenkins.vault;
2+
3+
import hudson.EnvVars;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import org.junit.Test;
7+
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.hamcrest.Matchers.equalTo;
10+
import static org.mockito.Mockito.mock;
11+
import static org.mockito.Mockito.when;
12+
13+
public class VaultAccessorTest {
14+
15+
private static final String POLICIES_STR =
16+
"\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";
17+
18+
@Test
19+
public void testGeneratePolicies() {
20+
EnvVars envVars = mock(EnvVars.class);
21+
when(envVars.get("JOB_NAME")).thenReturn("job1");
22+
when(envVars.get("JOB_BASE_NAME")).thenReturn("job1");
23+
when(envVars.get("NODE_NAME")).thenReturn("node1");
24+
25+
List<String> policies = VaultAccessor.generatePolicies(POLICIES_STR, envVars);
26+
assertThat(policies, equalTo(Arrays.asList(
27+
"pol1", "base_job1", "job/job1", "job_job1", "folder/", "folder_", "node_node1"
28+
)));
29+
}
30+
31+
@Test
32+
public void testGeneratePoliciesWithFolder() {
33+
EnvVars envVars = mock(EnvVars.class);
34+
when(envVars.get("JOB_NAME")).thenReturn("folder1/folder2/job1");
35+
when(envVars.get("JOB_BASE_NAME")).thenReturn("job1");
36+
when(envVars.get("NODE_NAME")).thenReturn("node1");
37+
38+
List<String> policies = VaultAccessor.generatePolicies(POLICIES_STR, envVars);
39+
assertThat(policies, equalTo(Arrays.asList(
40+
"pol1", "base_job1", "job/folder1/folder2/job1", "job_folder1_folder2_job1",
41+
"folder/folder1/folder2", "folder_folder1_folder2", "node_node1"
42+
)));
43+
}
44+
}

‎src/test/java/com/datapipe/jenkins/vault/credentials/AbstractAuthenticatingVaultTokenCredentialTest.java

+8
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ public void setUp() throws Exception {
3434
when(authResponse.getAuthClientToken()).thenReturn("12345");
3535
}
3636

37+
@Test
38+
public void nonRootNamespaceFromGetVaultAuth() {
39+
ExampleVaultTokenCredential cred = new ExampleVaultTokenCredential();
40+
cred.setNamespace("foo");
41+
Auth authRet = cred.getVaultAuth(vault);
42+
verify(authRet).withNameSpace("foo");
43+
}
44+
3745
@Test
3846
public void nonRootNamespace() {
3947
ExampleVaultTokenCredential cred = new ExampleVaultTokenCredential();

‎src/test/java/com/datapipe/jenkins/vault/credentials/AbstractVaultTokenCredentialWithExpirationTest.java

+75-11
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44
import com.bettercloud.vault.VaultConfig;
55
import com.bettercloud.vault.VaultException;
66
import com.bettercloud.vault.api.Auth;
7+
import com.bettercloud.vault.api.Auth.TokenRequest;
78
import com.bettercloud.vault.response.AuthResponse;
89
import com.bettercloud.vault.response.LookupResponse;
910
import com.cloudbees.plugins.credentials.CredentialsScope;
1011
import com.datapipe.jenkins.vault.exception.VaultPluginException;
12+
import java.util.ArrayList;
13+
import java.util.Arrays;
14+
import java.util.List;
1115
import org.junit.Before;
1216
import org.junit.Test;
1317

18+
import static org.mockito.ArgumentMatchers.any;
1419
import static org.mockito.ArgumentMatchers.anyString;
20+
import static org.mockito.ArgumentMatchers.argThat;
1521
import static org.mockito.Mockito.mock;
1622
import static org.mockito.Mockito.times;
1723
import static org.mockito.Mockito.verify;
@@ -22,66 +28,123 @@ public class AbstractVaultTokenCredentialWithExpirationTest {
2228
private Vault vault;
2329
private VaultConfig vaultConfig;
2430
private Auth auth;
25-
private AuthResponse authResponse;
31+
private AuthResponse authResponse, childAuthResponse;
2632
private LookupResponse lookupResponse;
2733
private ExampleVaultTokenCredentialWithExpiration vaultTokenCredentialWithExpiration;
34+
private List<String> policies;
2835

2936
@Before
3037
public void setUp() throws VaultException {
38+
policies = Arrays.asList("pol1", "pol2");
3139
vault = mock(Vault.class);
3240
vaultConfig = mock(VaultConfig.class);
3341
auth = mock(Auth.class);
3442
authResponse = mock(AuthResponse.class);
43+
childAuthResponse = mock(AuthResponse.class);
44+
when(auth.createToken(any(TokenRequest.class))).thenReturn(childAuthResponse);
3545
lookupResponse = mock(LookupResponse.class);
3646
vaultTokenCredentialWithExpiration = new ExampleVaultTokenCredentialWithExpiration(vault);
3747

3848
when(vault.auth()).thenReturn(auth);
3949
when(auth.loginByCert()).thenReturn(authResponse);
4050
when(authResponse.getAuthClientToken()).thenReturn("fakeToken");
51+
when(childAuthResponse.getAuthClientToken()).thenReturn("childToken");
4152
}
4253

4354
@Test
4455
public void shouldBeAbleToFetchTokenOnInit() throws VaultException {
4556
when(auth.lookupSelf()).thenReturn(lookupResponse);
4657
when(lookupResponse.getTTL()).thenReturn(5L);
4758

48-
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
59+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
4960

5061
verify(vaultConfig).token("fakeToken");
5162
}
5263

5364
@Test
54-
public void shouldReuseTheExistingTokenIfNotExpired() throws VaultException {
65+
public void shouldFetchNewTokenForDifferentPolicies() throws VaultException {
5566
when(auth.lookupSelf()).thenReturn(lookupResponse);
5667
when(lookupResponse.getTTL()).thenReturn(5L);
68+
when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2");
69+
when(childAuthResponse.getAuthClientToken()).thenReturn("childToken1", "childToken2");
70+
71+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
72+
verify(vaultConfig).token("fakeToken1");
73+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies);
74+
verify(vaultConfig).token("childToken1");
75+
}
76+
77+
@Test
78+
public void shouldNotFetchChildTokenIfEmptyPoliciesSpecified() throws VaultException {
79+
when(authResponse.getAuthClientToken()).thenReturn("fakeToken");
80+
when(auth.lookupSelf()).thenReturn(lookupResponse);
81+
when(lookupResponse.getTTL()).thenReturn(0L);
82+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, new ArrayList<>());
83+
84+
verify(vaultConfig, times(1)).token(anyString());
85+
verify(vaultConfig).token("fakeToken");
86+
}
87+
88+
@Test
89+
public void shouldFetchChildTokenIfPoliciesSpecified() throws VaultException {
90+
when(auth.createToken(argThat((TokenRequest tr) ->
91+
tr.getPolices() == policies && tr.getTtl().equals("30s")
92+
))).thenReturn(childAuthResponse);
93+
when(auth.lookupSelf()).thenReturn(lookupResponse);
94+
// First response is for parent, second is for child
95+
when(lookupResponse.getTTL()).thenReturn(30L, 0L);
96+
97+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies);
98+
99+
verify(vaultConfig, times(2)).token(anyString());
100+
verify(vaultConfig).token("fakeToken");
101+
verify(vaultConfig).token("childToken");
102+
}
57103

58-
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
59-
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
104+
@Test
105+
public void shouldReuseTheExistingTokenIfNotExpired() throws VaultException {
106+
when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2");
107+
when(childAuthResponse.getAuthClientToken()).thenReturn("childToken1", "childToken2");
108+
when(auth.lookupSelf()).thenReturn(lookupResponse);
109+
when(lookupResponse.getTTL()).thenReturn(30L);
60110

61-
verify(vaultConfig, times(2)).token("fakeToken");
111+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
112+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
113+
verify(vaultConfig, times(2)).token("fakeToken1");
114+
115+
// Different policies results in a new token
116+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies);
117+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies);
118+
verify(vaultConfig, times(2)).token("childToken1");
62119
}
63120

64121
@Test
65122
public void shouldFetchNewTokenIfExpired() throws VaultException {
66123
when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2");
124+
when(childAuthResponse.getAuthClientToken()).thenReturn("childToken1", "childToken2");
67125
when(auth.lookupSelf()).thenReturn(lookupResponse);
68126
when(lookupResponse.getTTL()).thenReturn(0L);
69127

70-
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
71-
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
72-
128+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
129+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
73130
verify(vaultConfig, times(2)).token(anyString());
74131
verify(vaultConfig).token("fakeToken1");
75132
verify(vaultConfig).token("fakeToken2");
133+
134+
// Different policies results in a new token
135+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies);
136+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, policies);
137+
verify(vaultConfig).token("childToken1");
138+
verify(vaultConfig).token("childToken2");
76139
}
77140

78141
@Test
79142
public void shouldExpireTokenImmediatelyIfExceptionFetchingTTL() throws VaultException {
80143
when(authResponse.getAuthClientToken()).thenReturn("fakeToken1", "fakeToken2");
81144
when(auth.lookupSelf()).thenThrow(new VaultException("Fail for testing"));
82145

83-
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
84-
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig);
146+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
147+
vaultTokenCredentialWithExpiration.authorizeWithVault(vaultConfig, null);
85148

86149
verify(vaultConfig, times(2)).token(anyString());
87150
verify(vaultConfig).token("fakeToken1");
@@ -96,6 +159,7 @@ static class ExampleVaultTokenCredentialWithExpiration extends
96159
protected ExampleVaultTokenCredentialWithExpiration(Vault vault) {
97160
super(CredentialsScope.GLOBAL, "id", "description");
98161
this.vault = vault;
162+
this.setUsePolicies(true);
99163
}
100164

101165
@Override

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

+54-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import com.bettercloud.vault.VaultConfig;
55
import com.bettercloud.vault.response.LogicalResponse;
66
import com.bettercloud.vault.rest.RestResponse;
7+
import com.cloudbees.hudson.plugins.folder.Folder;
78
import com.cloudbees.plugins.credentials.Credentials;
89
import com.cloudbees.plugins.credentials.CredentialsScope;
910
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
1011
import com.cloudbees.plugins.credentials.domains.Domain;
1112
import com.datapipe.jenkins.vault.VaultAccessor;
1213
import com.datapipe.jenkins.vault.VaultBuildWrapper;
14+
import com.datapipe.jenkins.vault.configuration.FolderVaultConfiguration;
1315
import com.datapipe.jenkins.vault.configuration.GlobalVaultConfiguration;
1416
import com.datapipe.jenkins.vault.configuration.VaultConfiguration;
1517
import com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential;
@@ -49,8 +51,11 @@
4951
import static org.hamcrest.Matchers.hasSize;
5052
import static org.hamcrest.Matchers.is;
5153
import static org.hamcrest.Matchers.notNullValue;
54+
import static org.hamcrest.Matchers.nullValue;
55+
import static org.hamcrest.Matchers.equalTo;
5256
import static org.hamcrest.collection.IsMapContaining.hasEntry;
5357
import static org.mockito.ArgumentMatchers.anyInt;
58+
import static org.mockito.ArgumentMatchers.eq;
5459
import static org.mockito.Mockito.any;
5560
import static org.mockito.Mockito.anyString;
5661
import static org.mockito.Mockito.mock;
@@ -152,6 +157,33 @@ public static String getVariable(String v) {
152157
return isWindows() ? "%" + v + "%" : "$" + v;
153158
}
154159

160+
private void assertOverridePolicies(String globalPolicies, Boolean globalDisableOverride, Boolean folderDisableOverride,
161+
String policiesResult) throws Exception {
162+
VaultConfiguration globalConfig = GlobalVaultConfiguration.get().getConfiguration();
163+
globalConfig.setPolicies(globalPolicies);
164+
globalConfig.setDisableChildPoliciesOverride(globalDisableOverride);
165+
166+
Folder folder = jenkins.createProject(Folder.class, "sub1");
167+
VaultConfiguration folderConfig = new VaultConfiguration();
168+
folderConfig.setPolicies("folder-policies");
169+
folderConfig.setDisableChildPoliciesOverride(folderDisableOverride);
170+
folder.addProperty(new FolderVaultConfiguration(folderConfig));
171+
172+
FreeStyleProject project = folder.createProject(FreeStyleProject.class, "test");
173+
FreeStyleBuild build = mock(FreeStyleBuild.class);
174+
when(build.getParent()).thenReturn(project);
175+
VaultConfiguration vaultConfig = new VaultConfiguration();
176+
vaultConfig.setPolicies("job-policies");
177+
178+
assertThat(VaultAccessor.pullAndMergeConfiguration(build, vaultConfig).getPolicies(),
179+
equalTo(policiesResult));
180+
}
181+
182+
private void assertOverridePolicies(Boolean globalDisableOverride, Boolean folderDisableOverride,
183+
String policiesResult) throws Exception {
184+
assertOverridePolicies("global-policies", globalDisableOverride, folderDisableOverride, policiesResult);
185+
}
186+
155187
@Test
156188
public void shouldUseGlobalConfiguration() throws Exception {
157189
List<VaultSecret> secrets = standardSecrets();
@@ -220,6 +252,27 @@ public void shouldUseJobConfiguration() throws Exception {
220252
verify(mockAccessor, times(1)).read("secret/path1", GLOBAL_ENGINE_VERSION_2);
221253
jenkins.assertLogContains("echo ****", build);
222254
jenkins.assertLogNotContains("some-secret", build);
255+
assertThat(VaultAccessor.pullAndMergeConfiguration(build, vaultConfig).getPolicies(), nullValue());
256+
}
257+
258+
@Test
259+
public void shouldUseJobConfigurationWithoutDisableOverrides() throws Exception {
260+
assertOverridePolicies(false, false, "job-policies");
261+
}
262+
263+
@Test
264+
public void shouldUseFolderConfigurationWithDisableOverrides() throws Exception {
265+
assertOverridePolicies(false, true, "folder-policies");
266+
}
267+
268+
@Test
269+
public void shouldUseGlobalConfigurationWithDisableOverrides() throws Exception {
270+
assertOverridePolicies(true, false, "global-policies");
271+
}
272+
273+
@Test
274+
public void shouldUseEmptyGlobalConfigurationWithDisableOverrides() throws Exception {
275+
assertOverridePolicies(null, true, true, null);
223276
}
224277

225278
@Test
@@ -430,7 +483,7 @@ public static VaultAppRoleCredential createTokenCredential(final String credenti
430483
when(cred.getDescription()).thenReturn("description");
431484
when(cred.getRoleId()).thenReturn("role-id-" + credentialId);
432485
when(cred.getSecretId()).thenReturn(Secret.fromString("secret-id-" + credentialId));
433-
when(cred.authorizeWithVault(any())).thenReturn(vault);
486+
when(cred.authorizeWithVault(any(), eq(null))).thenReturn(vault);
434487
return cred;
435488

436489
}

0 commit comments

Comments
 (0)
Please sign in to comment.