/* * The MIT License * * Copyright (c) 2010-2011, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy * * 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 com.michelin.cio.hudson.plugins.maskpasswords; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import hudson.Extension; import hudson.Launcher; import hudson.maven.MavenModuleSetBuild; import hudson.maven.MavenModuleSet; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import hudson.util.Secret; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.jvnet.localizer.Localizable; import org.jvnet.localizer.ResourceBundleHolder; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; /** * Build wrapper that alters the console so that passwords don't get displayed. * * @author Romain Seguy (http://openromain.blogspot.com) */ public final class MaskPasswordsBuildWrapper extends BuildWrapper { private final List varPasswordPairs; @DataBoundConstructor public MaskPasswordsBuildWrapper(List varPasswordPairs) { this.varPasswordPairs = varPasswordPairs; } /** * This method is invoked before {@link #makeBuildVariables()} and {@link #setUp()}. */ @Override public OutputStream decorateLogger(AbstractBuild build, OutputStream logger) { List allPasswords = new ArrayList(); // all passwords to be masked MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); // global passwords List globalVarPasswordPairs = config.getGlobalVarPasswordPairs(); for (VarPasswordPair globalVarPasswordPair : globalVarPasswordPairs) { allPasswords.add(globalVarPasswordPair.getPassword()); } // job's passwords if (varPasswordPairs != null) { for (VarPasswordPair varPasswordPair : varPasswordPairs) { String password = varPasswordPair.getPassword(); if (StringUtils.isNotBlank(password)) { allPasswords.add(password); } } } // find build parameters which are passwords (PasswordParameterValue) ParametersAction params = build.getAction(ParametersAction.class); if (params != null) { for (ParameterValue param : params) { if (config.isMasked(param.getClass().getName())) { String password = param.createVariableResolver(build).resolve(param.getName()); if (StringUtils.isNotBlank(password)) { allPasswords.add(password); } } } } return new MaskPasswordsOutputStream(logger, allPasswords); } /** * Contributes the passwords defined by the user as variables that can be reused from build steps (and other places). */ @Override public void makeBuildVariables(AbstractBuild build, Map variables) { if (build instanceof MavenModuleSetBuild) { // This is for Maven 2/3 projects MavenModuleSet mavenBuildParent = ((MavenModuleSetBuild) build).getParent(); StringBuilder mavenPropsBuilder = new StringBuilder(mavenBuildParent.getMavenOpts()); // global var/password pairs MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); List globalVarPasswordPairs = config.getGlobalVarPasswordPairs(); // we can't use variables.putAll() since passwords are ciphered when in varPasswordPairs for (VarPasswordPair globalVarPasswordPair : globalVarPasswordPairs) { mavenPropsBuilder.append(" -D").append(globalVarPasswordPair.getVar()); mavenPropsBuilder.append("=").append(globalVarPasswordPair.getPassword()); } // job's var/password pairs if (varPasswordPairs != null) { // cf. comment above for (VarPasswordPair varPasswordPair : varPasswordPairs) { if (StringUtils.isNotBlank(varPasswordPair.getVar())) { mavenPropsBuilder.append(" -D").append(varPasswordPair.getVar()); mavenPropsBuilder.append("=").append(varPasswordPair.getPassword()); } } } mavenBuildParent.setMavenOpts(mavenPropsBuilder.toString()); } else { // this part is for free style projects // global var/password pairs MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); List globalVarPasswordPairs = config.getGlobalVarPasswordPairs(); // we can't use variables.putAll() since passwords are ciphered when in varPasswordPairs for (VarPasswordPair globalVarPasswordPair : globalVarPasswordPairs) { variables.put(globalVarPasswordPair.getVar(), globalVarPasswordPair.getPassword()); } // job's var/password pairs if (varPasswordPairs != null) { // cf. comment above for (VarPasswordPair varPasswordPair : varPasswordPairs) { if (StringUtils.isNotBlank(varPasswordPair.getVar())) { variables.put(varPasswordPair.getVar(), varPasswordPair.getPassword()); } } } } } @Override public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { return new Environment() { // nothing to tearDown() }; } public List getVarPasswordPairs() { return varPasswordPairs; } /** * Represents name/password entries defined by users in their jobs. *

* Equality and hashcode are based on {@code var} only, not {@code password}. *

*/ public static class VarPasswordPair implements Cloneable { private final String var; private final Secret password; @DataBoundConstructor public VarPasswordPair(String var, String password) { this.var = var; this.password = Secret.fromString(password); } @Override public Object clone() { return new VarPasswordPair(getVar(), getPassword()); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final VarPasswordPair other = (VarPasswordPair) obj; if ((this.var == null) ? (other.var != null) : !this.var.equals(other.var)) { return false; } return true; } public String getVar() { return var; } public String getPassword() { return Secret.toString(password); } public Secret getPasswordAsSecret() { return password; } @Override public int hashCode() { int hash = 3; hash = 67 * hash + (this.var != null ? this.var.hashCode() : 0); return hash; } } @Extension public static final class DescriptorImpl extends BuildWrapperDescriptor { public DescriptorImpl() { super(MaskPasswordsBuildWrapper.class); } /** * @since 2.5 */ @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { try { getConfig().clear(); LOGGER.fine("Processing the maskedParamDefs and selectedMaskedParamDefs JSON objects"); JSONObject submittedForm = req.getSubmittedForm(); // parameter definitions to be automatically masked JSONArray paramDefinitions = submittedForm.getJSONArray("maskedParamDefs"); JSONArray selectedParamDefinitions = submittedForm.getJSONArray("selectedMaskedParamDefs"); for (int i = 0; i < selectedParamDefinitions.size(); i++) { if (selectedParamDefinitions.getBoolean(i)) { getConfig().addMaskedPasswordParameterDefinition(paramDefinitions.getString(i)); } } // global var/password pairs if (submittedForm.has("globalVarPasswordPairs")) { Object o = submittedForm.get("globalVarPasswordPairs"); if (o instanceof JSONArray) { JSONArray jsonArray = submittedForm.getJSONArray("globalVarPasswordPairs"); for (int i = 0; i < jsonArray.size(); i++) { getConfig().addGlobalVarPasswordPair( new VarPasswordPair(jsonArray.getJSONObject(i).getString("var"), jsonArray.getJSONObject(i) .getString("password"))); } } else if (o instanceof JSONObject) { JSONObject jsonObject = submittedForm.getJSONObject("globalVarPasswordPairs"); getConfig().addGlobalVarPasswordPair( new VarPasswordPair(jsonObject.getString("var"), jsonObject.getString("password"))); } } MaskPasswordsConfig.save(getConfig()); return true; } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to save Mask Passwords plugin configuration", e); return false; } } /** * @since 2.5 */ public MaskPasswordsConfig getConfig() { return MaskPasswordsConfig.getInstance(); } @Override public String getDisplayName() { return new Localizable(ResourceBundleHolder.get(MaskPasswordsBuildWrapper.class), "DisplayName").toString(); } @Override public boolean isApplicable(AbstractProject item) { return true; } } /** * We need this converter to handle marshalling/unmarshalling of the build wrapper data: Relying on the default mechanism * doesn't make it (because {@link Secret} doesn't have the {@code DataBoundConstructor} annotation). */ public static final class ConverterImpl implements Converter { private final static String VAR_PASSWORD_PAIRS_NODE = "varPasswordPairs"; private final static String VAR_PASSWORD_PAIR_NODE = "varPasswordPair"; private final static String VAR_ATT = "var"; private final static String PASSWORD_ATT = "password"; public boolean canConvert(Class clazz) { return clazz.equals(MaskPasswordsBuildWrapper.class); } public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext mc) { MaskPasswordsBuildWrapper maskPasswordsBuildWrapper = (MaskPasswordsBuildWrapper) o; // varPasswordPairs if (maskPasswordsBuildWrapper.getVarPasswordPairs() != null) { writer.startNode(VAR_PASSWORD_PAIRS_NODE); for (VarPasswordPair varPasswordPair : maskPasswordsBuildWrapper.getVarPasswordPairs()) { // blank passwords are skipped if (StringUtils.isBlank(varPasswordPair.getPassword())) { continue; } writer.startNode(VAR_PASSWORD_PAIR_NODE); writer.addAttribute(VAR_ATT, varPasswordPair.getVar()); writer.addAttribute(PASSWORD_ATT, varPasswordPair.getPasswordAsSecret().getEncryptedValue()); writer.endNode(); } writer.endNode(); } } public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext uc) { List varPasswordPairs = new ArrayList(); while (reader.hasMoreChildren()) { reader.moveDown(); if (reader.getNodeName().equals(VAR_PASSWORD_PAIRS_NODE)) { while (reader.hasMoreChildren()) { reader.moveDown(); if (reader.getNodeName().equals(VAR_PASSWORD_PAIR_NODE)) { varPasswordPairs.add(new VarPasswordPair(reader.getAttribute(VAR_ATT), reader .getAttribute(PASSWORD_ATT))); } else { LOGGER.log(Level.WARNING, "Encountered incorrect node name: Expected \"" + VAR_PASSWORD_PAIR_NODE + "\", got \"{0}\"", reader.getNodeName()); } reader.moveUp(); } reader.moveUp(); } else { LOGGER.log(Level.WARNING, "Encountered incorrect node name: \"{0}\"", reader.getNodeName()); } } return new MaskPasswordsBuildWrapper(varPasswordPairs); } } private static final Logger LOGGER = Logger.getLogger(MaskPasswordsBuildWrapper.class.getName()); }