/* * The MIT License * * Copyright 2015 Jesse Glick. * * 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.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.console.ConsoleLogFilter; import hudson.console.LineTransformationOutputStream; import hudson.model.AbstractBuild; import hudson.model.Run; import hudson.model.TaskListener; import hudson.util.Secret; import java.io.IOException; import java.io.ObjectStreamException; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.codec.Charsets; import org.jenkinsci.plugins.credentialsbinding.MultiBinding; import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; import org.jenkinsci.plugins.workflow.steps.BodyInvoker; import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.kohsuke.stapler.DataBoundConstructor; import javax.annotation.Nonnull; /** * Workflow step to bind credentials. */ @SuppressWarnings("rawtypes") // TODO DescribableHelper does not yet seem to handle List<? extends MultiBinding<?>> or even List<MultiBinding<?>> public final class BindingStep extends Step { private final List<MultiBinding> bindings; @DataBoundConstructor public BindingStep(List<MultiBinding> bindings) { this.bindings = bindings; } public List<MultiBinding> getBindings() { return bindings; } @Override public StepExecution start(StepContext context) throws Exception { return new Execution(this, context); } static final class Execution extends AbstractStepExecutionImpl { private static final long serialVersionUID = 1; private transient BindingStep step; Execution(@Nonnull BindingStep step, StepContext context) { super(context); this.step = step; } @Override public boolean start() throws Exception { Run<?,?> run = getContext().get(Run.class); TaskListener listener = getContext().get(TaskListener.class); FilePath workspace = getContext().get(FilePath.class); Launcher launcher = getContext().get(Launcher.class); Map<String,String> overrides = new HashMap<String,String>(); List<MultiBinding.Unbinder> unbinders = new ArrayList<MultiBinding.Unbinder>(); for (MultiBinding<?> binding : step.bindings) { if (binding.getDescriptor().requiresWorkspace() && (workspace == null || launcher == null)) { throw new MissingContextVariableException(FilePath.class); } MultiBinding.MultiEnvironment environment = binding.bind(run, workspace, launcher, listener); unbinders.add(environment.getUnbinder()); overrides.putAll(environment.getValues()); } getContext().newBodyInvoker(). withContext(EnvironmentExpander.merge(getContext().get(EnvironmentExpander.class), new Overrider(overrides))). withContext(BodyInvoker.mergeConsoleLogFilters(getContext().get(ConsoleLogFilter.class), new Filter(overrides.values(), run.getCharset().name()))). withCallback(new Callback(unbinders)). start(); return false; } @Override public void stop(Throwable cause) throws Exception { // should be no need to do anything special (but verify in JENKINS-26148) } } private static final class Overrider extends EnvironmentExpander { private static final long serialVersionUID = 1; private final Map<String,Secret> overrides = new HashMap<String,Secret>(); Overrider(Map<String,String> overrides) { for (Map.Entry<String,String> override : overrides.entrySet()) { this.overrides.put(override.getKey(), Secret.fromString(override.getValue())); } } @Override public void expand(EnvVars env) throws IOException, InterruptedException { for (Map.Entry<String,Secret> override : overrides.entrySet()) { env.override(override.getKey(), override.getValue().getPlainText()); } } } /** Similar to {@code MaskPasswordsOutputStream}. */ private static final class Filter extends ConsoleLogFilter implements Serializable { private static final long serialVersionUID = 1; private final Secret pattern; private String charsetName; Filter(Collection<String> secrets, String charsetName) { pattern = Secret.fromString(MultiBinding.getPatternStringForSecrets(secrets)); 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(AbstractBuild _ignore, final OutputStream logger) throws IOException, InterruptedException { final Pattern p = Pattern.compile(pattern.getPlainText()); return new LineTransformationOutputStream() { @Override protected void eol(byte[] b, int len) throws IOException { 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); } } }; } } private static final class Callback extends BodyExecutionCallback.TailCall { private static final long serialVersionUID = 1; private final List<MultiBinding.Unbinder> unbinders; Callback(List<MultiBinding.Unbinder> unbinders) { this.unbinders = unbinders; } @Override protected void finished(StepContext context) throws Exception { Exception xx = null; for (MultiBinding.Unbinder unbinder : unbinders) { try { unbinder.unbind(context.get(Run.class), context.get(FilePath.class), context.get(Launcher.class), context.get(TaskListener.class)); } catch (Exception x) { if (xx == null) { xx = x; } else { xx.addSuppressed(x); } } } if (xx != null) { throw xx; } } } @Extension public static final class DescriptorImpl extends StepDescriptor { @Override public String getFunctionName() { return "withCredentials"; } @Override public String getDisplayName() { return "Bind credentials to variables"; } @Override public boolean takesImplicitBlockArgument() { return true; } @Override public Set<? extends Class<?>> getRequiredContext() { return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(TaskListener.class, Run.class))); } } }