Skip to content
Permalink
Browse files

[JENKINS-24805] First work on masking secrets in freestyle logs.

This still needs tests - I just want to make sure the approach is right.
  • Loading branch information...
abayer committed Oct 31, 2016
1 parent 686a29f commit a47f4b8529fd216c3c797566aa969191834abd7a
@@ -38,12 +38,15 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;

import hudson.util.Secret;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException;
import org.kohsuke.stapler.DataBoundConstructor;
@@ -138,4 +141,19 @@ public Unbinder getUnbinder() {
return (BindingDescriptor<C>) super.getDescriptor();
}

/**
* Utility method for turning a collection of secret strings into a {@link Secret}.
* @param secrets A collection of secret strings
* @return A {@link Secret} generated from that collection.
*/
public static Secret getSecretForStrings(Collection<String> secrets) {
StringBuilder b = new StringBuilder();
for (String secret : secrets) {
if (b.length() > 0) {
b.append('|');
}
b.append(Pattern.quote(secret));
}
return Secret.fromString(b.toString());
}
}
@@ -136,14 +136,7 @@
private String charsetName;

Filter(Collection<String> secrets, String charsetName) {
StringBuilder b = new StringBuilder();
for (String secret : secrets) {
if (b.length() > 0) {
b.append('|');
}
b.append(Pattern.quote(secret));
}
pattern = Secret.fromString(b.toString());
pattern = MultiBinding.getSecretForStrings(secrets);
this.charsetName = charsetName;
}

@@ -26,48 +26,95 @@

import hudson.Extension;
import hudson.Launcher;
import hudson.console.ConsoleLogFilter;
import hudson.console.LineTransformationOutputStream;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Run;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import hudson.util.Secret;
import org.apache.commons.codec.Charsets;
import org.jenkinsci.plugins.credentialsbinding.MultiBinding;
import org.kohsuke.stapler.DataBoundConstructor;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

@SuppressWarnings({"rawtypes", "unchecked"}) // inherited from BuildWrapper
public class SecretBuildWrapper extends BuildWrapper {

private /*almost final*/ List<? extends MultiBinding<?>> bindings;

private static Map<AbstractBuild<?, ?>, Collection<String>> secretsForBuild = new WeakHashMap<AbstractBuild<?, ?>, Collection<String>>();

/**
* Gets the {@link Pattern} for the secret values for a given build, if that build has secrets defined. If not, return
* null.
* @param build A non-null build.
* @return A compiled {@link Pattern} from the build's secret values, if the build has any.
*/
public static @Nullable Pattern getPatternForBuild(@Nonnull AbstractBuild<?, ?> build) {
if (secretsForBuild.containsKey(build)) {
return Pattern.compile(MultiBinding.getSecretForStrings(secretsForBuild.get(build)).getPlainText());
} else {
return null;
}
}

@DataBoundConstructor public SecretBuildWrapper(List<? extends MultiBinding<?>> bindings) {
this.bindings = bindings == null ? Collections.<MultiBinding<?>>emptyList() : bindings;
}

public List<? extends MultiBinding<?>> getBindings() {
return bindings;
}

@Override public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
@Override
public OutputStream decorateLogger(AbstractBuild build, OutputStream logger) throws IOException, InterruptedException, Run.RunnerAbortedException {
if (!bindings.isEmpty()) {
return new Filter(build.getCharset().name()).decorateLogger(build, logger);
} else {
return logger;
}
}

@Override public Environment setUp(final AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
final List<MultiBinding.MultiEnvironment> m = new ArrayList<MultiBinding.MultiEnvironment>();
for (MultiBinding binding : bindings) {
m.add(binding.bind(build, build.getWorkspace(), launcher, listener));
}

secretsForBuild.put(build, new HashSet<String>());

return new Environment() {
@Override public void buildEnvVars(Map<String,String> env) {
for (MultiBinding.MultiEnvironment e : m) {
env.putAll(e.getValues());
secretsForBuild.get(build).addAll(e.getValues().values());
}
}
@Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {
for (MultiBinding.MultiEnvironment e : m) {
e.getUnbinder().unbind(build, build.getWorkspace(), launcher, listener);
}
secretsForBuild.remove(build);
return true;
}
};
@@ -86,6 +133,52 @@ protected Object readResolve() {
return this;
}

/** Similar to {@code MaskPasswordsOutputStream}. */
private static final class Filter extends ConsoleLogFilter implements Serializable {

private static final long serialVersionUID = 1;

private String charsetName;

Filter(String charsetName) {
this.charsetName = charsetName;
}

// To avoid de-serialization issues with newly added field (charsetName)
private Object readResolve() throws ObjectStreamException {
if (this.charsetName == null) {
this.charsetName = Charsets.UTF_8.name();
}
return this;
}

@Override public OutputStream decorateLogger(final AbstractBuild build, final OutputStream logger) throws IOException, InterruptedException {
return new LineTransformationOutputStream() {
Pattern p;

@Override protected void eol(byte[] b, int len) throws IOException {
if (p == null) {
p = getPatternForBuild(build);
}

if (p != null) {
Matcher m = p.matcher(new String(b, 0, len, charsetName));
if (m.find()) {
logger.write(m.replaceAll("****").getBytes(charsetName));
} else {
// Avoid byte → char → byte conversion unless we are actually doing something.
logger.write(b, 0, len);
}
} else {
// Avoid byte → char → byte conversion unless we are actually doing something.
logger.write(b, 0, len);
}
}
};
}

}

@Extension public static class DescriptorImpl extends BuildWrapperDescriptor {

@Override public boolean isApplicable(AbstractProject<?, ?> item) {

0 comments on commit a47f4b8

Please sign in to comment.
You can’t perform that action at this time.