/*
* The MIT License
*
* Copyright 2013 jglick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jenkinsci.plugins.credentialsbinding.impl;
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.OutputStream;
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 org.jenkinsci.plugins.credentialsbinding.MultiBinding;
import org.kohsuke.stapler.DataBoundConstructor;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
@SuppressWarnings({"rawtypes", "unchecked"}) // inherited from BuildWrapper
public class SecretBuildWrapper extends BuildWrapper {
private /*almost final*/ List<? extends MultiBinding<?>> bindings;
private final 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 @CheckForNull Pattern getPatternForBuild(@Nonnull AbstractBuild<?, ?> build) {
if (secretsForBuild.containsKey(build)) {
return Pattern.compile(MultiBinding.getPatternStringForSecrets(secretsForBuild.get(build)));
} 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 OutputStream decorateLogger(AbstractBuild build, OutputStream logger) throws IOException, InterruptedException, Run.RunnerAbortedException {
return new Filter(build.getCharset().name()).decorateLogger(build, logger);
}
@Override public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
final List<MultiBinding.MultiEnvironment> m = new ArrayList<MultiBinding.MultiEnvironment>();
Set<String> secrets = new HashSet<String>();
for (MultiBinding binding : bindings) {
MultiBinding.MultiEnvironment e = binding.bind(build, build.getWorkspace(), launcher, listener);
m.add(e);
secrets.addAll(e.getValues().values());
}
if (!secrets.isEmpty()) {
secretsForBuild.put(build, secrets);
}
return new Environment() {
@Override public void buildEnvVars(Map<String,String> env) {
for (MultiBinding.MultiEnvironment e : m) {
env.putAll(e.getValues());
}
}
@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;
}
};
}
@Override public void makeSensitiveBuildVariables(AbstractBuild build, Set<String> sensitiveVariables) {
for (MultiBinding binding : bindings) {
sensitiveVariables.addAll(binding.variables());
}
}
protected Object readResolve() {
if (bindings == null) {
bindings = Collections.emptyList();
}
return this;
}
/** Similar to {@code MaskPasswordsOutputStream}. */
private static final class Filter extends ConsoleLogFilter {
private final String charsetName;
Filter(String charsetName) {
this.charsetName = charsetName;
}
@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) {
return true;
}
@Override public String getDisplayName() {
return Messages.SecretBuildWrapper_use_secret_text_s_or_file_s_();
}
}
}