Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>5.9</version>
<version>5.18</version>
<relativePath />
</parent>
<groupId>com.datapipe.jenkins.plugins</groupId>
Expand All @@ -14,8 +14,8 @@
<properties>
<changelist>9999-SNAPSHOT</changelist>
<!-- https://www.jenkins.io/doc/developer/plugin-development/choosing-jenkins-baseline/ -->
<jenkins.baseline>2.479</jenkins.baseline>
<jenkins.version>${jenkins.baseline}.3</jenkins.version>
<jenkins.baseline>2.516</jenkins.baseline>
<jenkins.version>${jenkins.baseline}.1</jenkins.version>
<hpi.compatibleSinceVersion>2.0.0</hpi.compatibleSinceVersion>
<gitHubRepo>jenkinsci/${project.artifactId}</gitHubRepo>
</properties>
Expand Down Expand Up @@ -175,6 +175,13 @@
</exclusion>
</exclusions>
</dependency>
<!-- Re-introduce commons-compress for tests, required by VaultContainer utilities used in ITs -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down Expand Up @@ -221,7 +228,7 @@
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-${jenkins.baseline}.x</artifactId>
<version>4545.v56392b_7ca_7b_a_</version>
<version>5294.va_d2e144c80e1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
Expand Down
131 changes: 123 additions & 8 deletions src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import hudson.Util;
import hudson.model.Run;
import hudson.security.ACL;
import io.github.jopenlibs.vault.SslConfig;
import io.github.jopenlibs.vault.Vault;
import io.github.jopenlibs.vault.VaultConfig;
import io.github.jopenlibs.vault.VaultException;
Expand Down Expand Up @@ -185,10 +186,13 @@ public static Map<String, String> retrieveVaultSecrets(Run<?,?> run, PrintStream
}

VaultConfig vaultConfig = config.getVaultConfig();
VaultCredential credential = config.getVaultCredential();
if (credential == null) {
credential = retrieveVaultCredentials(run, config);
boolean verbose = Boolean.TRUE.equals(config.getVerboseLogging());
if (verbose) {
logger.printf("Vault: url=%s engineVersion=%s skipSslVerification=%s prefixPath=%s namespace=%s%n",
config.getVaultUrl(), config.getEngineVersion(), config.getSkipSslVerification(),
StringUtils.defaultString(config.getPrefixPath(), ""), StringUtils.defaultString(config.getVaultNamespace(), ""));
}
VaultCredential jobLevelCredential = config.getVaultCredential();

String prefixPath = StringUtils.isBlank(config.getPrefixPath())
? ""
Expand All @@ -197,20 +201,106 @@ public static Map<String, String> retrieveVaultSecrets(Run<?,?> run, PrintStream
if (vaultAccessor == null) {
vaultAccessor = new VaultAccessor();
}
vaultAccessor.setConfig(vaultConfig);
vaultAccessor.setCredential(credential);
// Initialize a shared accessor using job-level settings when applicable; tests may inject a mock accessor
vaultAccessor.setPolicies(generatePolicies(config.getPolicies(), envVars));
vaultAccessor.setMaxRetries(config.getMaxRetries());
vaultAccessor.setRetryIntervalMilliseconds(config.getRetryIntervalMilliseconds());
vaultAccessor.init();
boolean shouldInitSharedAccessor = (jobLevelCredential != null) || StringUtils.isNotBlank(config.getVaultCredentialId());
if (shouldInitSharedAccessor) {
// Resolve job-level credential by id if object is not already present
VaultCredential resolvedJobCred = jobLevelCredential != null
? jobLevelCredential
: retrieveVaultCredentials(run, config);
vaultAccessor.setConfig(vaultConfig);
vaultAccessor.setCredential(resolvedJobCred);
// Allow injected mock to intercept init()
vaultAccessor.init();
}

for (VaultSecret vaultSecret : vaultSecrets) {
// Determine which credential and namespace to use for this secret
VaultAccessor accessorToUse = vaultAccessor;
String overrideCredId = null;
String overrideNamespace = null;
try {
// Reflective-safe access to optional field; direct call is fine as we depend on same module
overrideCredId = vaultSecret.getVaultCredentialId();
overrideNamespace = vaultSecret.getVaultNamespace();
} catch (NoSuchMethodError e) {
// older configurations won't have this method/field
overrideCredId = null;
overrideNamespace = null;
}
// If there is no per-secret credential, no job-level credential object, and no job-level credential id, fail fast
if (StringUtils.isBlank(overrideCredId) && jobLevelCredential == null && StringUtils.isBlank(config.getVaultCredentialId())) {
String secretPathInfo = prefixPath + envVars.expand(vaultSecret.getPath());
throw new VaultPluginException(
String.format("No credential configured for secret '%s'. Set a job-level credential or a per-secret credential override.",
secretPathInfo));
}
// Build per-secret config if namespace is overridden, else reuse existing
VaultConfig perSecretConfig;
if (StringUtils.isNotBlank(overrideNamespace)) {
try {
perSecretConfig = new VaultConfig();
perSecretConfig.address(config.getVaultUrl());
perSecretConfig.engineVersion(config.getEngineVersion());
if (config.getSkipSslVerification()) {
perSecretConfig.sslConfig(new SslConfig().verify(false).build());
}
perSecretConfig.nameSpace(overrideNamespace);
if (StringUtils.isNotEmpty(config.getPrefixPath())) {
perSecretConfig.prefixPath(config.getPrefixPath());
}
} catch (VaultException e) {
throw new VaultPluginException("Could not set up per-secret VaultConfig.", e);
}
} else {
perSecretConfig = vaultConfig;
}

// Resolve credential to use for this secret
VaultCredential credToUse = null;
if (StringUtils.isNotBlank(overrideCredId)) {
credToUse = retrieveVaultCredentialById(run, overrideCredId);
} else {
// Use already-initialized shared accessor when no per-secret override
credToUse = jobLevelCredential;
}

if (verbose && (StringUtils.isNotBlank(overrideCredId) || StringUtils.isNotBlank(overrideNamespace))) {
logger.printf("Using per-secret overrides: credentialId=%s namespace=%s%n",
StringUtils.defaultString(overrideCredId, "(job-level)"),
StringUtils.defaultString(overrideNamespace, "(job-level)"));
}

if (StringUtils.isNotBlank(overrideCredId) || StringUtils.isNotBlank(overrideNamespace)) {
// Create and init a per-secret accessor only when overrides are provided
VaultAccessor perSecretAccessor = new VaultAccessor();
perSecretAccessor.setConfig(perSecretConfig);
perSecretAccessor.setCredential(credToUse);
perSecretAccessor.setPolicies(vaultAccessor.getPolicies());
perSecretAccessor.setMaxRetries(vaultAccessor.getMaxRetries());
perSecretAccessor.setRetryIntervalMilliseconds(vaultAccessor.getRetryIntervalMilliseconds());
try {
perSecretAccessor.init();
} catch (VaultPluginException ex) {
// Provide more context on failures during login/authorization
throw new VaultPluginException(
String.format("Failed to connect/login to Vault for secret (credentialId=%s, namespace=%s)",
StringUtils.defaultString(overrideCredId, StringUtils.defaultString(config.getVaultCredentialId(), "(custom-object)")),
StringUtils.defaultString(overrideNamespace, StringUtils.defaultString(config.getVaultNamespace(), "(default)"))), ex);
}
accessorToUse = perSecretAccessor;
}
String path = prefixPath + envVars.expand(vaultSecret.getPath());
logger.printf("Retrieving secret: %s%n", path);
if (verbose) {
logger.printf("Retrieving secret: %s%n", path);
}
Integer engineVersion = Optional.ofNullable(vaultSecret.getEngineVersion())
.orElse(config.getEngineVersion());
try {
LogicalResponse response = vaultAccessor.read(path, engineVersion);
LogicalResponse response = accessorToUse.read(path, engineVersion);
if (responseHasErrors(config, logger, path, response)) {
continue;
}
Expand Down Expand Up @@ -240,6 +330,31 @@ public static Map<String, String> retrieveVaultSecrets(Run<?,?> run, PrintStream
return overrides;
}

/**
* Resolve a VaultCredential by ID scoped to the job's parent item.
*/
public static VaultCredential retrieveVaultCredentialById(Run build, String id) {
if (Jenkins.getInstanceOrNull() != null) {
if (StringUtils.isBlank(id)) {
throw new VaultPluginException(
"The credential id was blank - please specify the credentials to use.");
}
List<VaultCredential> credentials = CredentialsProvider
.lookupCredentials(VaultCredential.class, build.getParent(), ACL.SYSTEM,
Collections.emptyList());
VaultCredential credential = CredentialsMatchers
.firstOrNull(credentials, new IdMatcher(id));

if (credential == null) {
throw new CredentialsUnavailableException(id);
}

return credential;
}

return null;
}

public static VaultCredential retrieveVaultCredentials(Run build, VaultConfiguration config) {
if (Jenkins.getInstanceOrNull() != null) {
String id = config.getVaultCredentialId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class VaultConfiguration extends AbstractDescribableImpl<VaultConfigurati

private Integer timeout = DEFAULT_TIMEOUT;

private Boolean verboseLogging;

@DataBoundConstructor
public VaultConfiguration() {
// no args constructor
Expand All @@ -80,6 +82,7 @@ public VaultConfiguration(VaultConfiguration toCopy) {
this.policies = toCopy.policies;
this.disableChildPoliciesOverride = toCopy.disableChildPoliciesOverride;
this.timeout = toCopy.timeout;
this.verboseLogging = toCopy.verboseLogging;
}

public VaultConfiguration mergeWithParent(VaultConfiguration parent) {
Expand Down Expand Up @@ -118,6 +121,9 @@ public VaultConfiguration mergeWithParent(VaultConfiguration parent) {
if (result.skipSslVerification == null) {
result.setSkipSslVerification(parent.skipSslVerification);
}
if (result.verboseLogging == null) {
result.setVerboseLogging(parent.getVerboseLogging());
}
return result;
}

Expand Down Expand Up @@ -220,6 +226,15 @@ public void setTimeout(Integer timeout) {
this.timeout = timeout;
}

public Boolean getVerboseLogging() {
return verboseLogging;
}

@DataBoundSetter
public void setVerboseLogging(Boolean verboseLogging) {
this.verboseLogging = verboseLogging;
}

/**
* Number of retries when reading a secret from vault
*
Expand Down Expand Up @@ -303,6 +318,9 @@ public VaultConfiguration fixDefaults() {
if (getFailIfNotFound() == null) {
setFailIfNotFound(DescriptorImpl.DEFAULT_FAIL_NOT_FOUND);
}
if (getVerboseLogging() == null) {
setVerboseLogging(false);
}
return this;
}

Expand Down
34 changes: 34 additions & 0 deletions src/main/java/com/datapipe/jenkins/vault/model/VaultSecret.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
*/
package com.datapipe.jenkins.vault.model;

import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
import com.datapipe.jenkins.vault.credentials.VaultCredential;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
Expand All @@ -32,6 +36,7 @@
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.verb.POST;

import static com.datapipe.jenkins.vault.configuration.VaultConfiguration.engineVersions;
import static hudson.Util.fixEmptyAndTrim;
Expand All @@ -46,6 +51,8 @@
private String path;
private Integer engineVersion;
private List<VaultSecretValue> secretValues;
private String vaultCredentialId;
private String vaultNamespace;

@DataBoundConstructor
public VaultSecret(String path, List<VaultSecretValue> secretValues) {
Expand All @@ -70,6 +77,24 @@
return this.secretValues;
}

@DataBoundSetter
public void setVaultCredentialId(String vaultCredentialId) {
this.vaultCredentialId = fixEmptyAndTrim(vaultCredentialId);
}

public String getVaultCredentialId() {
return vaultCredentialId;
}

@DataBoundSetter
public void setVaultNamespace(String vaultNamespace) {
this.vaultNamespace = fixEmptyAndTrim(vaultNamespace);
}

public String getVaultNamespace() {
return vaultNamespace;
}

@Extension
public static final class DescriptorImpl extends Descriptor<VaultSecret> {

Expand All @@ -83,6 +108,15 @@
return engineVersions(context);
}

@SuppressWarnings("unused") // used by stapler
@POST
public ListBoxModel doFillVaultCredentialIdItems(@AncestorInPath Item item,

Check warning

Code scanning / Jenkins Security Scan

Stapler: Missing permission check Warning

Potential missing permission check in DescriptorImpl#doFillVaultCredentialIdItems

Check warning

Code scanning / Jenkins Security Scan

Jenkins: Missing permission check on a form fill web method with credentials lookup Warning

doFillVaultCredentialIdItems should perform a permission check before calling #includeAs
doFillVaultCredentialIdItems should perform a permission check before calling #includeEmptyValue
@org.kohsuke.stapler.QueryParameter String uri) {
List<DomainRequirement> domainRequirements = URIRequirementBuilder.fromUri(uri).build();
return new StandardListBoxModel().includeEmptyValue().includeAs(hudson.security.ACL.SYSTEM, item,
VaultCredential.class, domainRequirements);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@
<f:entry title="Timeout" field="timeout">
<f:number default="60" clazz="positive-number"/>
</f:entry>
<f:entry title="Verbose logging" field="verboseLogging">
<f:checkbox/>
</f:entry>
</f:advanced>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
<f:entry field="engineVersion" title="K/V Engine Version">
<f:select/>
</f:entry>
<f:entry field="vaultCredentialId" title="Credential (override job-level)">
<f:select/>
</f:entry>
<f:entry field="vaultNamespace" title="Namespace (override job-level)">
<f:textbox/>
</f:entry>
</f:advanced>

</j:jelly>
Loading