提交 021912b9 编写于 作者: J Jesse Glick

Merge branch 'master' into AccessDeniedException2-header-size-JENKINS-39402

...@@ -56,9 +56,14 @@ Upcoming changes</a> ...@@ -56,9 +56,14 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. --> <!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=--> <div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image> <ul class=image>
<li class=>
</ul> </ul>
</div><!--=TRUNK-END=--> </div><!--=TRUNK-END=-->
<h3><a name=v2.44>What's new in 2.44</a> (2017/02/01)</h3>
<ul class=image>
<li class="major bug">
<strong>Important security fixes</strong>
(<a href="https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2017-02-01">security advisory</a>)
</ul>
<h3><a name=v2.43>What's new in 2.43</a> (2017/01/29)</h3> <h3><a name=v2.43>What's new in 2.43</a> (2017/01/29)</h3>
<ul class=image> <ul class=image>
<li class=rfe> <li class=rfe>
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<parent> <parent>
<groupId>org.jenkins-ci.main</groupId> <groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId> <artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version> <version>2.45-SNAPSHOT</version>
</parent> </parent>
<artifactId>cli</artifactId> <artifactId>cli</artifactId>
......
...@@ -29,7 +29,7 @@ THE SOFTWARE. ...@@ -29,7 +29,7 @@ THE SOFTWARE.
<parent> <parent>
<groupId>org.jenkins-ci.main</groupId> <groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId> <artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version> <version>2.45-SNAPSHOT</version>
</parent> </parent>
<artifactId>jenkins-core</artifactId> <artifactId>jenkins-core</artifactId>
...@@ -159,7 +159,7 @@ THE SOFTWARE. ...@@ -159,7 +159,7 @@ THE SOFTWARE.
<dependency> <dependency>
<groupId>org.kohsuke.stapler</groupId> <groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler-adjunct-timeline</artifactId> <artifactId>stapler-adjunct-timeline</artifactId>
<version>1.4</version> <version>1.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.kohsuke.stapler</groupId> <groupId>org.kohsuke.stapler</groupId>
...@@ -591,12 +591,6 @@ THE SOFTWARE. ...@@ -591,12 +591,6 @@ THE SOFTWARE.
<version>1.3.1-jenkins-1</version> <version>1.3.1-jenkins-1</version>
</dependency> </dependency>
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.3m</version>
</dependency>
<!-- offline profiler API to put in the classpath if we need it --> <!-- offline profiler API to put in the classpath if we need it -->
<!--dependency> <!--dependency>
<groupId>com.yourkit.api</groupId> <groupId>com.yourkit.api</groupId>
......
...@@ -51,6 +51,9 @@ import java.util.Collection; ...@@ -51,6 +51,9 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import com.jcraft.jzlib.GZIPInputStream; import com.jcraft.jzlib.GZIPInputStream;
import com.jcraft.jzlib.GZIPOutputStream; import com.jcraft.jzlib.GZIPOutputStream;
import hudson.remoting.ClassFilter;
import jenkins.security.HMACConfidentialKey;
import jenkins.util.SystemProperties;
/** /**
* Data that hangs off from a console output. * Data that hangs off from a console output.
...@@ -94,8 +97,6 @@ import com.jcraft.jzlib.GZIPOutputStream; ...@@ -94,8 +97,6 @@ import com.jcraft.jzlib.GZIPOutputStream;
* {@link ConsoleNote} always sticks to a particular point in the console output. * {@link ConsoleNote} always sticks to a particular point in the console output.
* *
* <p> * <p>
* This design allows descendant processes of Hudson to emit {@link ConsoleNote}s. For example, Ant forked
* by a shell forked by Hudson can put an encoded note in its stdout, and Hudson will correctly understands that.
* The preamble and postamble includes a certain ANSI escape sequence designed in such a way to minimize garbage * The preamble and postamble includes a certain ANSI escape sequence designed in such a way to minimize garbage
* if this output is observed by a human being directly. * if this output is observed by a human being directly.
* *
...@@ -121,6 +122,15 @@ import com.jcraft.jzlib.GZIPOutputStream; ...@@ -121,6 +122,15 @@ import com.jcraft.jzlib.GZIPOutputStream;
* @since 1.349 * @since 1.349
*/ */
public abstract class ConsoleNote<T> implements Serializable, Describable<ConsoleNote<?>>, ExtensionPoint { public abstract class ConsoleNote<T> implements Serializable, Describable<ConsoleNote<?>>, ExtensionPoint {
private static final HMACConfidentialKey MAC = new HMACConfidentialKey(ConsoleNote.class, "MAC");
/**
* Allows historical build records with unsigned console notes to be displayed, at the expense of any security.
* Disables checking of {@link #MAC} so do not set this flag unless you completely trust all users capable of affecting build output,
* which in practice means that all SCM committers as well as all Jenkins users with any non-read-only access are consider administrators.
*/
static /* nonfinal for tests & script console */ boolean INSECURE = SystemProperties.getBoolean(ConsoleNote.class.getName() + ".INSECURE");
/** /**
* When the line of a console output that this annotation is attached is read by someone, * When the line of a console output that this annotation is attached is read by someone,
* a new {@link ConsoleNote} is de-serialized and this method is invoked to annotate that line. * a new {@link ConsoleNote} is de-serialized and this method is invoked to annotate that line.
...@@ -179,6 +189,11 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol ...@@ -179,6 +189,11 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
DataOutputStream dos = new DataOutputStream(new Base64OutputStream(buf2,true,-1,null)); DataOutputStream dos = new DataOutputStream(new Base64OutputStream(buf2,true,-1,null));
try { try {
buf2.write(PREAMBLE); buf2.write(PREAMBLE);
if (Jenkins.getInstanceOrNull() != null) { // else we are in another JVM and cannot sign; result will be ignored unless INSECURE
byte[] mac = MAC.mac(buf.toByteArray());
dos.writeInt(- mac.length); // negative to differentiate from older form
dos.write(mac);
}
dos.writeInt(buf.size()); dos.writeInt(buf.size());
buf.writeTo(dos); buf.writeTo(dos);
} finally { } finally {
...@@ -211,7 +226,17 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol ...@@ -211,7 +226,17 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
return null; // not a valid preamble return null; // not a valid preamble
DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in)); DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
int sz = decoded.readInt(); int macSz = - decoded.readInt();
byte[] mac;
int sz;
if (macSz > 0) { // new format
mac = new byte[macSz];
decoded.readFully(mac);
sz = decoded.readInt();
} else {
mac = null;
sz = - macSz;
}
byte[] buf = new byte[sz]; byte[] buf = new byte[sz];
decoded.readFully(buf); decoded.readFully(buf);
...@@ -220,8 +245,18 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol ...@@ -220,8 +245,18 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
if (!Arrays.equals(postamble,POSTAMBLE)) if (!Arrays.equals(postamble,POSTAMBLE))
return null; // not a valid postamble return null; // not a valid postamble
try (ObjectInputStream ois = new ObjectInputStreamEx( if (mac == null) {
new GZIPInputStream(new ByteArrayInputStream(buf)), Jenkins.getInstance().pluginManager.uberClassLoader)) { if (!INSECURE) {
throw new IOException("Refusing to deserialize unsigned note from an old log.");
}
} else if (!MAC.checkMac(buf, mac)) {
throw new IOException("MAC mismatch");
}
Jenkins jenkins = Jenkins.getInstance();
try (ObjectInputStream ois = new ObjectInputStreamEx(new GZIPInputStream(new ByteArrayInputStream(buf)),
jenkins != null ? jenkins.pluginManager.uberClassLoader : ConsoleNote.class.getClassLoader(),
ClassFilter.DEFAULT)) {
return (ConsoleNote) ois.readObject(); return (ConsoleNote) ois.readObject();
} }
} catch (Error e) { } catch (Error e) {
...@@ -241,8 +276,15 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol ...@@ -241,8 +276,15 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
return; // not a valid preamble return; // not a valid preamble
DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in)); DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
int sz = decoded.readInt(); int macSz = - decoded.readInt();
IOUtils.skip(decoded,sz); if (macSz > 0) { // new format
IOUtils.skip(decoded, macSz);
int sz = decoded.readInt();
IOUtils.skip(decoded, sz);
} else { // old format
int sz = -macSz;
IOUtils.skip(decoded, sz);
}
byte[] postamble = new byte[POSTAMBLE.length]; byte[] postamble = new byte[POSTAMBLE.length];
in.readFully(postamble); in.readFully(postamble);
......
...@@ -32,6 +32,7 @@ import org.jenkinsci.Symbol; ...@@ -32,6 +32,7 @@ import org.jenkinsci.Symbol;
import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
...@@ -64,6 +65,7 @@ public final class HudsonHomeDiskUsageMonitor extends AdministrativeMonitor { ...@@ -64,6 +65,7 @@ public final class HudsonHomeDiskUsageMonitor extends AdministrativeMonitor {
/** /**
* Depending on whether the user said "yes" or "no", send him to the right place. * Depending on whether the user said "yes" or "no", send him to the right place.
*/ */
@RequirePOST
public HttpResponse doAct(@QueryParameter String no) throws IOException { public HttpResponse doAct(@QueryParameter String no) throws IOException {
if(no!=null) { if(no!=null) {
disable(true); disable(true);
......
...@@ -52,6 +52,7 @@ import java.util.concurrent.ConcurrentMap; ...@@ -52,6 +52,7 @@ import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.context.SecurityContextHolder;
......
...@@ -37,6 +37,7 @@ import java.util.logging.Level; ...@@ -37,6 +37,7 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.interceptor.RequirePOST;
/** /**
* Looks out for a broken reverse proxy setup that doesn't rewrite the location header correctly. * Looks out for a broken reverse proxy setup that doesn't rewrite the location header correctly.
...@@ -84,6 +85,7 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor { ...@@ -84,6 +85,7 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor {
/** /**
* Depending on whether the user said "yes" or "no", send him to the right place. * Depending on whether the user said "yes" or "no", send him to the right place.
*/ */
@RequirePOST
public HttpResponse doAct(@QueryParameter String no) throws IOException { public HttpResponse doAct(@QueryParameter String no) throws IOException {
if(no!=null) { // dismiss if(no!=null) { // dismiss
disable(true); disable(true);
......
...@@ -29,6 +29,7 @@ import hudson.Extension; ...@@ -29,6 +29,7 @@ import hudson.Extension;
import org.jenkinsci.Symbol; import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.IOException; import java.io.IOException;
...@@ -56,6 +57,7 @@ public class TooManyJobsButNoView extends AdministrativeMonitor { ...@@ -56,6 +57,7 @@ public class TooManyJobsButNoView extends AdministrativeMonitor {
/** /**
* Depending on whether the user said "yes" or "no", send him to the right place. * Depending on whether the user said "yes" or "no", send him to the right place.
*/ */
@RequirePOST
public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException { public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException {
if(req.hasParameter("no")) { if(req.hasParameter("no")) {
disable(true); disable(true);
......
...@@ -44,7 +44,6 @@ import hudson.util.Secret; ...@@ -44,7 +44,6 @@ import hudson.util.Secret;
import jenkins.model.DirectlyModifiableTopLevelItemGroup; import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import jenkins.security.NotReallyRoleSensitiveCallable; import jenkins.security.NotReallyRoleSensitiveCallable;
import org.acegisecurity.Authentication;
import jenkins.util.xml.XMLUtils; import jenkins.util.xml.XMLUtils;
import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.taskdefs.Copy;
...@@ -75,7 +74,6 @@ import org.kohsuke.stapler.interceptor.RequirePOST; ...@@ -75,7 +74,6 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.xml.transform.Source; import javax.xml.transform.Source;
import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
...@@ -235,27 +233,10 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet ...@@ -235,27 +233,10 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
if (this.name.equals(newName)) if (this.name.equals(newName))
return; return;
// the test to see if the project already exists or not needs to be done in escalated privilege // the lookup is case insensitive, so we should not fail if this item was the “existing” one
// to avoid overwriting // to allow people to rename "Foo" to "foo", for example.
ACL.impersonate(ACL.SYSTEM,new NotReallyRoleSensitiveCallable<Void,IOException>() { // see http://www.nabble.com/error-on-renaming-project-tt18061629.html
final Authentication user = Jenkins.getAuthentication(); Items.verifyItemDoesNotAlreadyExist(parent, newName, this);
@Override
public Void call() throws IOException {
Item existing = parent.getItem(newName);
if (existing != null && existing!=AbstractItem.this) {
if (existing.getACL().hasPermission(user,Item.DISCOVER))
// the look up is case insensitive, so we need "existing!=this"
// to allow people to rename "Foo" to "foo", for example.
// see http://www.nabble.com/error-on-renaming-project-tt18061629.html
throw new IllegalArgumentException("Job " + newName + " already exists");
else {
// can't think of any real way to hide this, but at least the error message could be vague.
throw new IOException("Unable to rename to " + newName);
}
}
return null;
}
});
File oldRoot = this.getRootDir(); File oldRoot = this.getRootDir();
......
...@@ -33,8 +33,12 @@ import java.util.Set; ...@@ -33,8 +33,12 @@ import java.util.Set;
import java.io.IOException; import java.io.IOException;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/** /**
* Checks the health of a subsystem of Jenkins and if there's something * Checks the health of a subsystem of Jenkins and if there's something
...@@ -74,7 +78,7 @@ import org.kohsuke.stapler.StaplerResponse; ...@@ -74,7 +78,7 @@ import org.kohsuke.stapler.StaplerResponse;
* @see Jenkins#administrativeMonitors * @see Jenkins#administrativeMonitors
*/ */
@LegacyInstancesAreScopedToHudson @LegacyInstancesAreScopedToHudson
public abstract class AdministrativeMonitor extends AbstractModelObject implements ExtensionPoint { public abstract class AdministrativeMonitor extends AbstractModelObject implements ExtensionPoint, StaplerProxy {
/** /**
* Human-readable ID of this monitor, which needs to be unique within the system. * Human-readable ID of this monitor, which needs to be unique within the system.
* *
...@@ -142,12 +146,21 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen ...@@ -142,12 +146,21 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
/** /**
* URL binding to disable this monitor. * URL binding to disable this monitor.
*/ */
@RequirePOST
public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOException { public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
disable(true); disable(true);
rsp.sendRedirect2(req.getContextPath()+"/manage"); rsp.sendRedirect2(req.getContextPath()+"/manage");
} }
/**
* Requires ADMINISTER permission for any operation in here.
*/
@Restricted(NoExternalUse.class)
public Object getTarget() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
return this;
}
/** /**
* All registered {@link AdministrativeMonitor} instances. * All registered {@link AdministrativeMonitor} instances.
*/ */
......
...@@ -1110,8 +1110,10 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces ...@@ -1110,8 +1110,10 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
@Exported(inline=true) @Exported(inline=true)
public Map<String/*monitor name*/,Object> getMonitorData() { public Map<String/*monitor name*/,Object> getMonitorData() {
Map<String,Object> r = new HashMap<String, Object>(); Map<String,Object> r = new HashMap<String, Object>();
for (NodeMonitor monitor : NodeMonitor.getAll()) if (hasPermission(CONNECT)) {
r.put(monitor.getClass().getName(),monitor.data(this)); for (NodeMonitor monitor : NodeMonitor.getAll())
r.put(monitor.getClass().getName(), monitor.data(this));
}
return r; return r;
} }
......
...@@ -231,7 +231,7 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont ...@@ -231,7 +231,7 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
Permission DISCOVER = new Permission(PERMISSIONS, "Discover", Messages._AbstractProject_DiscoverPermission_Description(), READ, PermissionScope.ITEM); Permission DISCOVER = new Permission(PERMISSIONS, "Discover", Messages._AbstractProject_DiscoverPermission_Description(), READ, PermissionScope.ITEM);
/** /**
* Ability to view configuration details. * Ability to view configuration details.
* If the user lacks {@link CONFIGURE} then any {@link Secret}s must be masked out, even in encrypted form. * If the user lacks {@link #CONFIGURE} then any {@link Secret}s must be masked out, even in encrypted form.
* @see Secret#ENCRYPTED_VALUE_PATTERN * @see Secret#ENCRYPTED_VALUE_PATTERN
*/ */
Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._AbstractProject_ExtendedReadPermission_Description(), CONFIGURE, SystemProperties.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.ITEM}); Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._AbstractProject_ExtendedReadPermission_Description(), CONFIGURE, SystemProperties.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.ITEM});
......
...@@ -260,10 +260,7 @@ public abstract class ItemGroupMixIn { ...@@ -260,10 +260,7 @@ public abstract class ItemGroupMixIn {
acl.checkPermission(Item.CREATE); acl.checkPermission(Item.CREATE);
Jenkins.getInstance().getProjectNamingStrategy().checkName(name); Jenkins.getInstance().getProjectNamingStrategy().checkName(name);
if (parent.getItem(name) != null) { Items.verifyItemDoesNotAlreadyExist(parent, name, null);
throw new IllegalArgumentException(parent.getDisplayName() + " already contains an item '" + name + "'");
}
// TODO what if we have no DISCOVER permission on the existing job?
// place it as config.xml // place it as config.xml
File configXml = Items.getConfigFile(getRootDirFor(name)).getFile(); File configXml = Items.getConfigFile(getRootDirFor(name)).getFile();
...@@ -316,9 +313,7 @@ public abstract class ItemGroupMixIn { ...@@ -316,9 +313,7 @@ public abstract class ItemGroupMixIn {
acl.getACL().checkCreatePermission(parent, type); acl.getACL().checkCreatePermission(parent, type);
Jenkins.getInstance().getProjectNamingStrategy().checkName(name); Jenkins.getInstance().getProjectNamingStrategy().checkName(name);
if(parent.getItem(name)!=null) Items.verifyItemDoesNotAlreadyExist(parent, name, null);
throw new IllegalArgumentException("Project of the name "+name+" already exists");
// TODO problem with DISCOVER as noted above
TopLevelItem item = type.newInstance(parent, name); TopLevelItem item = type.newInstance(parent, name);
try { try {
......
...@@ -493,9 +493,7 @@ public class Items { ...@@ -493,9 +493,7 @@ public class Items {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
String name = item.getName(); String name = item.getName();
if (destination.getItem(name) != null) { verifyItemDoesNotAlreadyExist(destination, name, null);
throw new IllegalArgumentException(name + " already exists");
}
String oldFullName = item.getFullName(); String oldFullName = item.getFullName();
// TODO AbstractItem.renameTo has a more baroque implementation; factor it out into a utility method perhaps? // TODO AbstractItem.renameTo has a more baroque implementation; factor it out into a utility method perhaps?
File destDir = destination.getRootDirFor(item); File destDir = destination.getRootDirFor(item);
...@@ -623,6 +621,30 @@ public class Items { ...@@ -623,6 +621,30 @@ public class Items {
} }
} }
/**
* Securely check for the existence of an item before trying to create one with the same name.
* @param parent the folder where we are about to create/rename/move an item
* @param newName the proposed new name
* @param variant if not null, an existing item which we accept could be there
* @throws IllegalArgumentException if there is already something there, which you were supposed to know about
* @throws Failure if there is already something there but you should not be told details
*/
static void verifyItemDoesNotAlreadyExist(@Nonnull ItemGroup<?> parent, @Nonnull String newName, @CheckForNull Item variant) throws IllegalArgumentException, Failure {
Item existing;
try (ACLContext ctxt = ACL.as(ACL.SYSTEM)) {
existing = parent.getItem(newName);
}
if (existing != null && existing != variant) {
if (existing.hasPermission(Item.DISCOVER)) {
String prefix = parent.getFullName();
throw new IllegalArgumentException((prefix.isEmpty() ? "" : prefix + "/") + newName + " already exists");
} else {
// Cannot hide its existence, so at least be as vague as possible.
throw new Failure("");
}
}
}
/** /**
* Used to load/save job configuration. * Used to load/save job configuration.
* *
......
...@@ -31,11 +31,16 @@ import hudson.scm.SCM; ...@@ -31,11 +31,16 @@ import hudson.scm.SCM;
import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapper;
import hudson.tasks.Builder; import hudson.tasks.Builder;
import hudson.util.VariableResolver; import hudson.util.VariableResolver;
import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject; import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.Exported;
...@@ -70,6 +75,9 @@ import org.kohsuke.stapler.export.ExportedBean; ...@@ -70,6 +75,9 @@ import org.kohsuke.stapler.export.ExportedBean;
*/ */
@ExportedBean(defaultVisibility=3) @ExportedBean(defaultVisibility=3)
public abstract class ParameterValue implements Serializable { public abstract class ParameterValue implements Serializable {
private static final Logger LOGGER = Logger.getLogger(ParameterValue.class.getName());
protected final String name; protected final String name;
private String description; private String description;
...@@ -91,6 +99,16 @@ public abstract class ParameterValue implements Serializable { ...@@ -91,6 +99,16 @@ public abstract class ParameterValue implements Serializable {
this.description = description; this.description = description;
} }
@Restricted(DoNotUse.class) // for value.jelly
public String getFormattedDescription() {
try {
return Jenkins.getInstance().getMarkupFormatter().translate(description);
} catch (IOException e) {
LOGGER.warning("failed to translate description using configured markup formatter");
return "";
}
}
/** /**
* Name of the parameter. * Name of the parameter.
* *
......
...@@ -1122,5 +1122,20 @@ public class User extends AbstractModelObject implements AccessControlled, Descr ...@@ -1122,5 +1122,20 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* JENKINS-22346. * JENKINS-22346.
*/ */
public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName()+".allowNonExistentUserToLogin"); public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName()+".allowNonExistentUserToLogin");
}
/**
* Jenkins historically created a (usually) ephemeral user record when an user with Overall/Administer permission
* accesses a /user/arbitraryName URL.
* <p>
* Unfortunately this constitutes a CSRF vulnerability, as malicious users can make admins create arbitrary numbers
* of ephemeral user records, so the behavior was changed in Jenkins 2.TODO / 2.32.2.
* <p>
* As some users may be relying on the previous behavior, setting this to true restores the previous behavior. This
* is not recommended.
*
* SECURITY-406.
*/
@Restricted(NoExternalUse.class)
public static boolean ALLOW_USER_CREATION_VIA_URL = SystemProperties.getBoolean(User.class.getName() + ".allowUserCreationViaUrl");
}
此差异已折叠。
...@@ -60,7 +60,6 @@ import org.kohsuke.stapler.HttpResponses; ...@@ -60,7 +60,6 @@ import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerResponse;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import javax.servlet.Filter; import javax.servlet.Filter;
......
...@@ -24,16 +24,19 @@ ...@@ -24,16 +24,19 @@
package hudson.slaves; package hudson.slaves;
import jenkins.model.Jenkins;
import hudson.Functions; import hudson.Functions;
import hudson.model.Computer; import hudson.model.Computer;
import hudson.model.User; import hudson.model.User;
import jenkins.model.Jenkins;
import org.jvnet.localizer.Localizable; import org.jvnet.localizer.Localizable;
import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.Exported;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.ObjectStreamException;
import java.util.Collections;
import java.util.Date; import java.util.Date;
/** /**
...@@ -128,21 +131,49 @@ public abstract class OfflineCause { ...@@ -128,21 +131,49 @@ public abstract class OfflineCause {
/** /**
* Taken offline by user. * Taken offline by user.
*
* @since 1.551 * @since 1.551
*/ */
public static class UserCause extends SimpleOfflineCause { public static class UserCause extends SimpleOfflineCause {
private final User user; @Deprecated
private transient User user;
public UserCause(User user, String message) { // null when unknown
super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy( private /*final*/ @CheckForNull String userId;
user!=null ? user.getId() : Jenkins.ANONYMOUS.getName(),
public UserCause(@CheckForNull User user, @CheckForNull String message) {
this(
user != null ? user.getId() : null,
message != null ? " : " + message : "" message != null ? " : " + message : ""
)); );
this.user = user; }
private UserCause(String userId, String message) {
super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(userId != null ? userId : Jenkins.ANONYMOUS.getName(), message));
this.userId = userId;
} }
public User getUser() { public User getUser() {
return user; return userId == null
? User.getUnknown()
: User.getById(userId, true)
;
}
// Storing the User in a filed was a mistake, switch to userId
@SuppressWarnings("deprecation")
private Object readResolve() throws ObjectStreamException {
if (user != null) {
String id = user.getId();
if (id != null) {
userId = id;
} else {
// The user field is not properly deserialized so id may be missing. Look the user up by fullname
User user = User.get(this.user.getFullName(), true, Collections.emptyMap());
userId = user.getId();
}
this.user = null;
}
return this;
} }
} }
......
...@@ -80,6 +80,7 @@ import java.util.regex.Matcher; ...@@ -80,6 +80,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static hudson.tools.JDKInstaller.Preference.*; import static hudson.tools.JDKInstaller.Preference.*;
import org.kohsuke.stapler.interceptor.RequirePOST;
/** /**
* Install JDKs from java.sun.com. * Install JDKs from java.sun.com.
...@@ -788,7 +789,9 @@ public class JDKInstaller extends ToolInstaller { ...@@ -788,7 +789,9 @@ public class JDKInstaller extends ToolInstaller {
/** /**
* Submits the Oracle account username/password. * Submits the Oracle account username/password.
*/ */
@RequirePOST
public HttpResponse doPostCredential(@QueryParameter String username, @QueryParameter String password) throws IOException, ServletException { public HttpResponse doPostCredential(@QueryParameter String username, @QueryParameter String password) throws IOException, ServletException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
this.username = username; this.username = username;
this.password = Secret.fromString(password); this.password = Secret.fromString(password);
save(); save();
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2016, CloudBees Inc.
*
* 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 hudson.util;
import com.trilead.ssh2.crypto.Base64;
import hudson.Util;
import jenkins.model.Jenkins;
import jenkins.security.CryptoConfidentialKey;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.security.GeneralSecurityException;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Historical algorithms for decrypting {@link Secret}s.
*/
@Restricted(NoExternalUse.class)
public class HistoricalSecrets {
/*package*/ static Secret decrypt(String data, CryptoConfidentialKey key) throws IOException, GeneralSecurityException {
byte[] in = Base64.decode(data.toCharArray());
Secret s = tryDecrypt(key.decrypt(), in);
if (s!=null) return s;
// try our historical key for backward compatibility
Cipher cipher = Secret.getCipher("AES");
cipher.init(Cipher.DECRYPT_MODE, getLegacyKey());
return tryDecrypt(cipher, in);
}
/*package*/ static Secret tryDecrypt(Cipher cipher, byte[] in) {
try {
String plainText = new String(cipher.doFinal(in), UTF_8);
if(plainText.endsWith(MAGIC))
return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
return null;
} catch (GeneralSecurityException e) {
return null; // if the key doesn't match with the bytes, it can result in BadPaddingException
}
}
/**
* Turns {@link Jenkins#getSecretKey()} into an AES key.
*
* @deprecated
* This is no longer the key we use to encrypt new information, but we still need this
* to be able to decrypt what's already persisted.
*/
@Deprecated
/*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException {
String secret = Secret.SECRET;
if(secret==null) return Jenkins.getInstance().getSecretKeyAsAES128();
return Util.toAes128Key(secret);
}
private static final String MAGIC = "::::MAGIC::::";
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
* The MIT License * The MIT License
* *
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2016, CloudBees Inc.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -30,12 +31,12 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader; ...@@ -30,12 +31,12 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.trilead.ssh2.crypto.Base64; import com.trilead.ssh2.crypto.Base64;
import jenkins.util.SystemProperties; import jenkins.util.SystemProperties;
import java.util.Arrays;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import hudson.Util; import hudson.Util;
import jenkins.security.CryptoConfidentialKey; import jenkins.security.CryptoConfidentialKey;
import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.Stapler;
import javax.crypto.SecretKey;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import java.io.Serializable; import java.io.Serializable;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
...@@ -47,6 +48,8 @@ import javax.annotation.Nonnull; ...@@ -47,6 +48,8 @@ import javax.annotation.Nonnull;
import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.accmod.restrictions.NoExternalUse;
import static java.nio.charset.StandardCharsets.UTF_8;
/** /**
* Glorified {@link String} that uses encryption in the persisted form, to avoid accidental exposure of a secret. * Glorified {@link String} that uses encryption in the persisted form, to avoid accidental exposure of a secret.
* *
...@@ -61,14 +64,21 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; ...@@ -61,14 +64,21 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
public final class Secret implements Serializable { public final class Secret implements Serializable {
private static final byte PAYLOAD_V1 = 1;
/** /**
* Unencrypted secret text. * Unencrypted secret text.
*/ */
@Nonnull @Nonnull
private final String value; private final String value;
private byte[] iv;
/*package*/ Secret(String value) {
this.value = value;
}
private Secret(String value) { /*package*/ Secret(String value, byte[] iv) {
this.value = value; this.value = value;
this.iv = iv;
} }
/** /**
...@@ -105,20 +115,6 @@ public final class Secret implements Serializable { ...@@ -105,20 +115,6 @@ public final class Secret implements Serializable {
return value.hashCode(); return value.hashCode();
} }
/**
* Turns {@link Jenkins#getSecretKey()} into an AES key.
*
* @deprecated
* This is no longer the key we use to encrypt new information, but we still need this
* to be able to decrypt what's already persisted.
*/
@Deprecated
/*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException {
String secret = SECRET;
if(secret==null) return Jenkins.getInstance().getSecretKeyAsAES128();
return Util.toAes128Key(secret);
}
/** /**
* Encrypts {@link #value} and returns it in an encoded printable form. * Encrypts {@link #value} and returns it in an encoded printable form.
* *
...@@ -126,23 +122,42 @@ public final class Secret implements Serializable { ...@@ -126,23 +122,42 @@ public final class Secret implements Serializable {
*/ */
public String getEncryptedValue() { public String getEncryptedValue() {
try { try {
Cipher cipher = KEY.encrypt(); synchronized (this) {
// add the magic suffix which works like a check sum. if (iv == null) { //if we were created from plain text or other reason without iv
return new String(Base64.encode(cipher.doFinal((value+MAGIC).getBytes("UTF-8")))); iv = KEY.newIv();
}
}
Cipher cipher = KEY.encrypt(iv);
byte[] encrypted = cipher.doFinal(this.value.getBytes(UTF_8));
byte[] payload = new byte[1 + 8 + iv.length + encrypted.length];
int pos = 0;
// For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
payload[pos++] = PAYLOAD_V1;
payload[pos++] = (byte)(iv.length >> 24);
payload[pos++] = (byte)(iv.length >> 16);
payload[pos++] = (byte)(iv.length >> 8);
payload[pos++] = (byte)(iv.length);
payload[pos++] = (byte)(encrypted.length >> 24);
payload[pos++] = (byte)(encrypted.length >> 16);
payload[pos++] = (byte)(encrypted.length >> 8);
payload[pos++] = (byte)(encrypted.length);
System.arraycopy(iv, 0, payload, pos, iv.length);
pos+=iv.length;
System.arraycopy(encrypted, 0, payload, pos, encrypted.length);
return "{"+new String(Base64.encode(payload))+"}";
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new Error(e); // impossible throw new Error(e); // impossible
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
} }
} }
/** /**
* Pattern matching a possible output of {@link #getEncryptedValue}. * Pattern matching a possible output of {@link #getEncryptedValue}
* Basically, any Base64-encoded value. * Basically, any Base64-encoded value optionally wrapped by {@code {}}.
* You must then call {@link #decrypt} to eliminate false positives. * You must then call {@link #decrypt(String)} to eliminate false positives.
* @see #ENCRYPTED_VALUE_PATTERN
*/ */
@Restricted(NoExternalUse.class) @Restricted(NoExternalUse.class)
public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("[A-Za-z0-9+/]+={0,2}"); public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?");
/** /**
* Reverse operation of {@link #getEncryptedValue()}. Returns null * Reverse operation of {@link #getEncryptedValue()}. Returns null
...@@ -151,32 +166,52 @@ public final class Secret implements Serializable { ...@@ -151,32 +166,52 @@ public final class Secret implements Serializable {
@CheckForNull @CheckForNull
public static Secret decrypt(@CheckForNull String data) { public static Secret decrypt(@CheckForNull String data) {
if(data==null) return null; if(data==null) return null;
try {
byte[] in = Base64.decode(data.toCharArray());
Secret s = tryDecrypt(KEY.decrypt(), in);
if (s!=null) return s;
// try our historical key for backward compatibility if (data.startsWith("{") && data.endsWith("}")) { //likely CBC encrypted/containing metadata but could be plain text
Cipher cipher = getCipher("AES"); byte[] payload;
cipher.init(Cipher.DECRYPT_MODE, getLegacyKey()); try {
return tryDecrypt(cipher, in); payload = Base64.decode(data.substring(1, data.length()-1).toCharArray());
} catch (GeneralSecurityException e) { } catch (IOException e) {
return null; return null;
} catch (UnsupportedEncodingException e) { }
throw new Error(e); // impossible switch (payload[0]) {
} catch (IOException e) { case PAYLOAD_V1:
return null; // For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
} int ivLength = ((payload[1] & 0xff) << 24)
} | ((payload[2] & 0xff) << 16)
| ((payload[3] & 0xff) << 8)
/*package*/ static Secret tryDecrypt(Cipher cipher, byte[] in) throws UnsupportedEncodingException { | (payload[4] & 0xff);
try { int dataLength = ((payload[5] & 0xff) << 24)
String plainText = new String(cipher.doFinal(in), "UTF-8"); | ((payload[6] & 0xff) << 16)
if(plainText.endsWith(MAGIC)) | ((payload[7] & 0xff) << 8)
return new Secret(plainText.substring(0,plainText.length()-MAGIC.length())); | (payload[8] & 0xff);
return null; if (payload.length != 1 + 8 + ivLength + dataLength) {
} catch (GeneralSecurityException e) { // not valid v1
return null; // if the key doesn't match with the bytes, it can result in BadPaddingException return null;
}
byte[] iv = Arrays.copyOfRange(payload, 9, 9 + ivLength);
byte[] code = Arrays.copyOfRange(payload, 9+ivLength, payload.length);
String text;
try {
text = new String(KEY.decrypt(iv).doFinal(code), UTF_8);
} catch (GeneralSecurityException e) {
// it's v1 which cannot be historical, but not decrypting
return null;
}
return new Secret(text, iv);
default:
return null;
}
} else {
try {
return HistoricalSecrets.decrypt(data, KEY);
} catch (GeneralSecurityException e) {
return null;
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
} catch (IOException e) {
return null;
}
} }
} }
...@@ -234,8 +269,6 @@ public final class Secret implements Serializable { ...@@ -234,8 +269,6 @@ public final class Secret implements Serializable {
} }
} }
private static final String MAGIC = "::::MAGIC::::";
/** /**
* Workaround for JENKINS-6459 / http://java.net/jira/browse/GLASSFISH-11862 * Workaround for JENKINS-6459 / http://java.net/jira/browse/GLASSFISH-11862
* @see #getCipher(String) * @see #getCipher(String)
...@@ -252,6 +285,14 @@ public final class Secret implements Serializable { ...@@ -252,6 +285,14 @@ public final class Secret implements Serializable {
*/ */
private static final CryptoConfidentialKey KEY = new CryptoConfidentialKey(Secret.class.getName()); private static final CryptoConfidentialKey KEY = new CryptoConfidentialKey(Secret.class.getName());
/**
* Reset the internal secret key for testing.
*/
@Restricted(NoExternalUse.class)
/*package*/ static void resetKeyForTest() {
KEY.resetForTest();
}
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
static { static {
......
...@@ -3,7 +3,6 @@ package hudson.util; ...@@ -3,7 +3,6 @@ package hudson.util;
import com.trilead.ssh2.crypto.Base64; import com.trilead.ssh2.crypto.Base64;
import hudson.Functions; import hudson.Functions;
import hudson.model.TaskListener; import hudson.model.TaskListener;
import org.apache.commons.io.FileUtils;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
...@@ -35,21 +34,21 @@ public class SecretRewriter { ...@@ -35,21 +34,21 @@ public class SecretRewriter {
*/ */
private int count; private int count;
/**
* If non-null the original file before rewrite gets in here.
*/
private final File backupDirectory;
/** /**
* Canonical paths of the directories we are recursing to protect * Canonical paths of the directories we are recursing to protect
* against symlink induced cycles. * against symlink induced cycles.
*/ */
private Set<String> callstack = new HashSet<String>(); private Set<String> callstack = new HashSet<String>();
public SecretRewriter(File backupDirectory) throws GeneralSecurityException { public SecretRewriter() throws GeneralSecurityException {
cipher = Secret.getCipher("AES"); cipher = Secret.getCipher("AES");
key = Secret.getLegacyKey(); key = HistoricalSecrets.getLegacyKey();
this.backupDirectory = backupDirectory; }
/** @deprecated SECURITY-376: {@code backupDirectory} is ignored */
@Deprecated
public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
this();
} }
private String tryRewrite(String s) throws IOException, InvalidKeyException { private String tryRewrite(String s) throws IOException, InvalidKeyException {
...@@ -65,19 +64,21 @@ public class SecretRewriter { ...@@ -65,19 +64,21 @@ public class SecretRewriter {
return s; // not a valid base64 return s; // not a valid base64
} }
cipher.init(Cipher.DECRYPT_MODE, key); cipher.init(Cipher.DECRYPT_MODE, key);
Secret sec = Secret.tryDecrypt(cipher, in); Secret sec = HistoricalSecrets.tryDecrypt(cipher, in);
if(sec!=null) // matched if(sec!=null) // matched
return sec.getEncryptedValue(); // replace by the new encrypted value return sec.getEncryptedValue(); // replace by the new encrypted value
else // not encrypted with the legacy key. leave it unmodified else // not encrypted with the legacy key. leave it unmodified
return s; return s;
} }
/** /** @deprecated SECURITY-376: {@code backup} is ignored */
* @param backup @Deprecated
* if non-null, the original file will be copied here before rewriting.
* if the rewrite doesn't happen, no copying.
*/
public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException { public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException {
return rewrite(f);
}
public boolean rewrite(File f) throws InvalidKeyException, IOException {
AtomicFileWriter w = new AtomicFileWriter(f, "UTF-8"); AtomicFileWriter w = new AtomicFileWriter(f, "UTF-8");
try { try {
...@@ -113,10 +114,6 @@ public class SecretRewriter { ...@@ -113,10 +114,6 @@ public class SecretRewriter {
} }
if (modified) { if (modified) {
if (backup!=null) {
backup.getParentFile().mkdirs();
FileUtils.copyFile(f,backup);
}
w.commit(); w.commit();
} }
return modified; return modified;
...@@ -161,11 +158,7 @@ public class SecretRewriter { ...@@ -161,11 +158,7 @@ public class SecretRewriter {
if ((count++)%100==0) if ((count++)%100==0)
listener.getLogger().println("Scanning "+child); listener.getLogger().println("Scanning "+child);
try { try {
File backup = null; if (rewrite(child)) {
if (backupDirectory!=null) backup = new File(backupDirectory,relative+'/'+ cn);
if (rewrite(child,backup)) {
if (backup!=null)
listener.getLogger().println("Copied "+child+" to "+backup+" as a backup");
listener.getLogger().println("Rewritten "+child); listener.getLogger().println("Rewritten "+child);
rewritten++; rewritten++;
} }
...@@ -195,7 +188,6 @@ public class SecretRewriter { ...@@ -195,7 +188,6 @@ public class SecretRewriter {
String n = dir.getName(); String n = dir.getName();
return n.equals("workspace") || n.equals("artifacts") return n.equals("workspace") || n.equals("artifacts")
|| n.equals("plugins") // no mutable data here || n.equals("plugins") // no mutable data here
|| n.equals("jenkins.security.RekeySecretAdminMonitor") // we don't want to rewrite backups
|| n.equals(".") || n.equals(".."); || n.equals(".") || n.equals("..");
} }
......
...@@ -6,6 +6,7 @@ import jenkins.model.Jenkins; ...@@ -6,6 +6,7 @@ import jenkins.model.Jenkins;
import org.jenkinsci.Symbol; import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.IOException; import java.io.IOException;
...@@ -34,6 +35,7 @@ public class SecurityIsOffMonitor extends AdministrativeMonitor { ...@@ -34,6 +35,7 @@ public class SecurityIsOffMonitor extends AdministrativeMonitor {
/** /**
* Depending on whether the user said "yes" or "no", send him to the right place. * Depending on whether the user said "yes" or "no", send him to the right place.
*/ */
@RequirePOST
public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException { public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException {
if(req.hasParameter("no")) { if(req.hasParameter("no")) {
disable(true); disable(true);
......
...@@ -118,7 +118,6 @@ import hudson.security.AccessControlled; ...@@ -118,7 +118,6 @@ import hudson.security.AccessControlled;
import hudson.security.AuthorizationStrategy; import hudson.security.AuthorizationStrategy;
import hudson.security.BasicAuthenticationFilter; import hudson.security.BasicAuthenticationFilter;
import hudson.security.FederatedLoginService; import hudson.security.FederatedLoginService;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.security.HudsonFilter; import hudson.security.HudsonFilter;
import hudson.security.LegacyAuthorizationStrategy; import hudson.security.LegacyAuthorizationStrategy;
import hudson.security.LegacySecurityRealm; import hudson.security.LegacySecurityRealm;
...@@ -1694,11 +1693,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve ...@@ -1694,11 +1693,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
*/ */
@Exported(name="jobs") @Exported(name="jobs")
public List<TopLevelItem> getItems() { public List<TopLevelItem> getItems() {
if (authorizationStrategy instanceof AuthorizationStrategy.Unsecured ||
authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) {
return new ArrayList(items.values());
}
List<TopLevelItem> viewableItems = new ArrayList<TopLevelItem>(); List<TopLevelItem> viewableItems = new ArrayList<TopLevelItem>();
for (TopLevelItem item : items.values()) { for (TopLevelItem item : items.values()) {
if (item.hasPermission(Item.READ)) if (item.hasPermission(Item.READ))
...@@ -2272,7 +2266,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve ...@@ -2272,7 +2266,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}) })
.add(new CollectionSearchIndex() {// for views .add(new CollectionSearchIndex() {// for views
protected View get(String key) { return getView(key); } protected View get(String key) { return getView(key); }
protected Collection<View> all() { return views; } protected Collection<View> all() { return viewGroupMixIn.getViews(); }
}); });
return builder; return builder;
} }
...@@ -2871,11 +2865,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve ...@@ -2871,11 +2865,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
/** /**
* Gets the user of the given name. * Gets the user of the given name.
* *
* @return the user of the given name (which may or may not be an id), if that person exists or the invoker {@link #hasPermission} on {@link #ADMINISTER}; else null * @return the user of the given name (which may or may not be an id), if that person exists; else null
* @see User#get(String,boolean), {@link User#getById(String, boolean)} * @see User#get(String,boolean), {@link User#getById(String, boolean)}
*/ */
public @CheckForNull User getUser(String name) { public @CheckForNull User getUser(String name) {
return User.get(name,hasPermission(ADMINISTER)); return User.get(name, User.ALLOW_USER_CREATION_VIA_URL && hasPermission(ADMINISTER));
} }
public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name ) throws IOException { public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name ) throws IOException {
...@@ -4354,6 +4348,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve ...@@ -4354,6 +4348,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
@RequirePOST @RequirePOST
public void doFingerprintCleanup(StaplerResponse rsp) throws IOException { public void doFingerprintCleanup(StaplerResponse rsp) throws IOException {
checkPermission(ADMINISTER);
FingerprintCleanupThread.invoke(); FingerprintCleanupThread.invoke();
rsp.setStatus(HttpServletResponse.SC_OK); rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain"); rsp.setContentType("text/plain");
...@@ -4362,6 +4357,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve ...@@ -4362,6 +4357,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
@RequirePOST @RequirePOST
public void doWorkspaceCleanup(StaplerResponse rsp) throws IOException { public void doWorkspaceCleanup(StaplerResponse rsp) throws IOException {
checkPermission(ADMINISTER);
WorkspaceCleanupThread.invoke(); WorkspaceCleanupThread.invoke();
rsp.setStatus(HttpServletResponse.SC_OK); rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain"); rsp.setContentType("text/plain");
......
package jenkins.security; package jenkins.security;
import hudson.Main;
import hudson.util.Secret; import hudson.util.Secret;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
...@@ -15,6 +21,9 @@ import java.security.GeneralSecurityException; ...@@ -15,6 +21,9 @@ import java.security.GeneralSecurityException;
* @since 1.498 * @since 1.498
*/ */
public class CryptoConfidentialKey extends ConfidentialKey { public class CryptoConfidentialKey extends ConfidentialKey {
@Restricted(NoExternalUse.class) // TODO pending API
public static final int DEFAULT_IV_LENGTH = 16;
private volatile SecretKey secret; private volatile SecretKey secret;
public CryptoConfidentialKey(String id) { public CryptoConfidentialKey(String id) {
super(id); super(id);
...@@ -35,7 +44,7 @@ public class CryptoConfidentialKey extends ConfidentialKey { ...@@ -35,7 +44,7 @@ public class CryptoConfidentialKey extends ConfidentialKey {
store(payload); store(payload);
} }
// Due to the stupid US export restriction JDK only ships 128bit version. // Due to the stupid US export restriction JDK only ships 128bit version.
secret = new SecretKeySpec(payload,0,128/8, ALGORITHM); secret = new SecretKeySpec(payload,0,128/8, KEY_ALGORITHM);
} }
} }
} }
...@@ -47,10 +56,12 @@ public class CryptoConfidentialKey extends ConfidentialKey { ...@@ -47,10 +56,12 @@ public class CryptoConfidentialKey extends ConfidentialKey {
/** /**
* Returns a {@link Cipher} object for encrypting with this key. * Returns a {@link Cipher} object for encrypting with this key.
* @deprecated use {@link #encrypt(byte[])}
*/ */
@Deprecated
public Cipher encrypt() { public Cipher encrypt() {
try { try {
Cipher cipher = Secret.getCipher(ALGORITHM); Cipher cipher = Secret.getCipher(KEY_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, getKey()); cipher.init(Cipher.ENCRYPT_MODE, getKey());
return cipher; return cipher;
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
...@@ -58,12 +69,68 @@ public class CryptoConfidentialKey extends ConfidentialKey { ...@@ -58,12 +69,68 @@ public class CryptoConfidentialKey extends ConfidentialKey {
} }
} }
/**
* Returns a {@link Cipher} object for encrypting with this key using the provided initialization vector.
* @param iv the initialization vector
* @return the cipher
*/
@Restricted(NoExternalUse.class) // TODO pending API
public Cipher encrypt(byte[] iv) {
try {
Cipher cipher = Secret.getCipher(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, getKey(), new IvParameterSpec(iv));
return cipher;
} catch (GeneralSecurityException e) {
throw new AssertionError(e);
}
}
/**
* Returns a {@link Cipher} object for decrypting with this key using the provided initialization vector.
* @param iv the initialization vector
* @return the cipher
*/
@Restricted(NoExternalUse.class) // TODO pending ApI
public Cipher decrypt(byte[] iv) {
try {
Cipher cipher = Secret.getCipher(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getKey(), new IvParameterSpec(iv));
return cipher;
} catch (GeneralSecurityException e) {
throw new AssertionError(e);
}
}
/**
* Generates a new Initialization Vector.
* @param length the length of the salt
* @return some random bytes
* @see #encrypt(byte[])
*/
@Restricted(NoExternalUse.class) // TODO pending API
public byte[] newIv(int length) {
return ConfidentialStore.get().randomBytes(length);
}
/**
* Generates a new Initialization Vector of default length.
* @return some random bytes
* @see #newIv(int)
* @see #encrypt(byte[])
*/
@Restricted(NoExternalUse.class) // TODO pending API
public byte[] newIv() {
return newIv(DEFAULT_IV_LENGTH);
}
/** /**
* Returns a {@link Cipher} object for decrypting with this key. * Returns a {@link Cipher} object for decrypting with this key.
* @deprecated use {@link #decrypt(byte[])}
*/ */
@Deprecated
public Cipher decrypt() { public Cipher decrypt() {
try { try {
Cipher cipher = Secret.getCipher(ALGORITHM); Cipher cipher = Secret.getCipher(KEY_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getKey()); cipher.init(Cipher.DECRYPT_MODE, getKey());
return cipher; return cipher;
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
...@@ -72,5 +139,18 @@ public class CryptoConfidentialKey extends ConfidentialKey { ...@@ -72,5 +139,18 @@ public class CryptoConfidentialKey extends ConfidentialKey {
} }
private static final String ALGORITHM = "AES"; private static final String KEY_ALGORITHM = "AES";
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
/**
* Reset the internal secret key for testing.
*/
@Restricted(NoExternalUse.class)
public void resetForTest() {
if (Main.isUnitTest) {
this.secret = null;
} else {
throw new IllegalStateException("Only for testing");
}
}
} }
package jenkins.security; package jenkins.security;
import hudson.Extension; import hudson.Extension;
import hudson.Util;
import hudson.Functions; import hudson.Functions;
import hudson.init.InitMilestone; import hudson.init.InitMilestone;
import hudson.init.Initializer; import hudson.init.Initializer;
...@@ -13,7 +14,6 @@ import jenkins.model.Jenkins; ...@@ -13,7 +14,6 @@ import jenkins.model.Jenkins;
import jenkins.util.io.FileBoolean; import jenkins.util.io.FileBoolean;
import org.jenkinsci.Symbol; import org.jenkinsci.Symbol;
import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.interceptor.RequirePOST;
...@@ -31,7 +31,7 @@ import java.util.logging.Logger; ...@@ -31,7 +31,7 @@ import java.util.logging.Logger;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
@Extension @Symbol("rekeySecret") @Extension @Symbol("rekeySecret")
public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor implements StaplerProxy { public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor {
/** /**
* Whether we detected a need to run the rewrite program. * Whether we detected a need to run the rewrite program.
...@@ -53,6 +53,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i ...@@ -53,6 +53,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i
*/ */
private final FileBoolean scanOnBoot = state("scanOnBoot"); private final FileBoolean scanOnBoot = state("scanOnBoot");
@SuppressWarnings("OverridableMethodCallInConstructor") // should have been final
public RekeySecretAdminMonitor() throws IOException { public RekeySecretAdminMonitor() throws IOException {
// if JENKINS_HOME existed <1.497, we need to offer rewrite // if JENKINS_HOME existed <1.497, we need to offer rewrite
// this computation needs to be done and the value be captured, // this computation needs to be done and the value be captured,
...@@ -62,14 +63,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i ...@@ -62,14 +63,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i
if (j.isUpgradedFromBefore(new VersionNumber("1.496.*")) if (j.isUpgradedFromBefore(new VersionNumber("1.496.*"))
&& new FileBoolean(new File(j.getRootDir(),"secret.key.not-so-secret")).isOff()) && new FileBoolean(new File(j.getRootDir(),"secret.key.not-so-secret")).isOff())
needed.on(); needed.on();
} Util.deleteRecursive(new File(getBaseDir(), "backups")); // SECURITY-376: no longer used
/**
* Requires ADMINISTER permission for any operation in here.
*/
public Object getTarget() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
return this;
} }
@Override @Override
...@@ -142,7 +136,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i ...@@ -142,7 +136,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i
protected void fix(TaskListener listener) throws Exception { protected void fix(TaskListener listener) throws Exception {
LOGGER.info("Initiating a re-keying of secrets. See "+getLogFile()); LOGGER.info("Initiating a re-keying of secrets. See "+getLogFile());
SecretRewriter rewriter = new SecretRewriter(new File(getBaseDir(),"backups")); SecretRewriter rewriter = new SecretRewriter();
try { try {
PrintStream log = listener.getLogger(); PrintStream log = listener.getLogger();
......
...@@ -9,6 +9,7 @@ import org.jenkinsci.Symbol; ...@@ -9,6 +9,7 @@ import org.jenkinsci.Symbol;
import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
...@@ -50,6 +51,7 @@ public class AdminCallableMonitor extends AdministrativeMonitor { ...@@ -50,6 +51,7 @@ public class AdminCallableMonitor extends AdministrativeMonitor {
/** /**
* Depending on whether the user said "examin" or "dismiss", send him to the right place. * Depending on whether the user said "examin" or "dismiss", send him to the right place.
*/ */
@RequirePOST
public HttpResponse doAct(@QueryParameter String dismiss) throws IOException { public HttpResponse doAct(@QueryParameter String dismiss) throws IOException {
if(dismiss!=null) { if(dismiss!=null) {
disable(true); disable(true);
......
...@@ -5,6 +5,7 @@ import hudson.model.AdministrativeMonitor; ...@@ -5,6 +5,7 @@ import hudson.model.AdministrativeMonitor;
import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
...@@ -33,6 +34,7 @@ public class MasterKillSwitchWarning extends AdministrativeMonitor { ...@@ -33,6 +34,7 @@ public class MasterKillSwitchWarning extends AdministrativeMonitor {
return Messages.MasterKillSwitchWarning_DisplayName(); return Messages.MasterKillSwitchWarning_DisplayName();
} }
@RequirePOST
public HttpResponse doAct(@QueryParameter String dismiss) throws IOException { public HttpResponse doAct(@QueryParameter String dismiss) throws IOException {
if(dismiss!=null) { if(dismiss!=null) {
disable(true); disable(true);
......
...@@ -24,7 +24,7 @@ THE SOFTWARE. ...@@ -24,7 +24,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?> <?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt"> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="${%JENKINS_HOME is almost full}" permission="${app.ADMINISTER}"> <l:layout title="${%JENKINS_HOME is almost full}">
<l:main-panel> <l:main-panel>
<h1> <h1>
<l:icon class="icon-warning icon-xlg"/> <l:icon class="icon-warning icon-xlg"/>
......
...@@ -24,7 +24,7 @@ THE SOFTWARE. ...@@ -24,7 +24,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?> <?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout title="${%Manage Old Data}" permission="${app.ADMINISTER}"> <l:layout title="${%Manage Old Data}">
<st:include page="sidepanel.jelly" it="${app}"/> <st:include page="sidepanel.jelly" it="${app}"/>
<l:main-panel> <l:main-panel>
<h1>${%Manage Old Data}</h1> <h1>${%Manage Old Data}</h1>
......
# This file is under the MIT License by authors # This file is under the MIT License by authors
Build=fghg Build=Eraiki
Build\ Artifacts=gfhfgh Build\ Artifacts=Laguntzaileak eraiki
Took=fghg Took=Hartu
startedAgo=dhgg startedAgo=orain dela zenbat hasia
# This file is under the MIT License by authors # This file is under the MIT License by authors
Back\ to\ Project=drthdf Back\ to\ Project=Proiektura itzuli
Changes=hgdg Changes=Aldaketak
Console\ Output=ghdgfh Console\ Output=Kontsolaren irteera
Edit\ Build\ Information=Kompilazioaren argibidea edidatu Edit\ Build\ Information=Konpilazioaren argibideak edidatu
Status=hgfdhg Status=Egoera
View\ Build\ Information=dhgg View\ Build\ Information=Konpilazioaren egoera ikusi
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter"> <div name="parameter">
<input type="hidden" name="name" value="${it.name}" /> <input type="hidden" name="name" value="${it.name}" />
<f:checkbox name="value" checked="${it.defaultValue}" /> <f:checkbox name="value" checked="${it.defaultValue}" />
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:checkbox name="value" checked="${it.value}" readonly="true" /> <f:checkbox name="value" checked="${it.value}" readonly="true" />
</f:entry> </f:entry>
</j:jelly> </j:jelly>
\ No newline at end of file
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter"> <div name="parameter">
<input type="hidden" name="name" value="${it.name}" /> <input type="hidden" name="name" value="${it.name}" />
<select name="value"> <select name="value">
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter"> <div name="parameter">
<input type="hidden" name="name" value="${it.name}" /> <input type="hidden" name="name" value="${it.name}" />
<input name="file" type="file" jsonAware="true" /> <input name="file" type="file" jsonAware="true" />
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<j:if test="${it.originalFileName != null}"> <j:if test="${it.originalFileName != null}">
<j:invokeStatic var="encodedName" className="hudson.Util" method="rawEncode"> <j:invokeStatic var="encodedName" className="hudson.Util" method="rawEncode">
<j:arg value="${it.name}" /> <j:arg value="${it.name}" />
......
...@@ -40,6 +40,7 @@ THE SOFTWARE. ...@@ -40,6 +40,7 @@ THE SOFTWARE.
<div class="col-xs-24 pane-header">${%Parameters}</div> <div class="col-xs-24 pane-header">${%Parameters}</div>
<div class="row col-xs-24 pane-content"> <div class="row col-xs-24 pane-content">
<table class="pane"> <table class="pane">
<j:set var="escapeEntryTitleAndDescription" value="true"/> <!-- SECURITY-353 defense unless overridden -->
<j:forEach var="parameterValue" items="${it.parameters}"> <j:forEach var="parameterValue" items="${it.parameters}">
<tbody> <tbody>
<st:include it="${parameterValue}" <st:include it="${parameterValue}"
......
...@@ -45,6 +45,7 @@ THE SOFTWARE. ...@@ -45,6 +45,7 @@ THE SOFTWARE.
<f:form method="post" action="build${empty(delay)?'':'?delay='+delay}" name="parameters" <f:form method="post" action="build${empty(delay)?'':'?delay='+delay}" name="parameters"
tableClass="parameters"> tableClass="parameters">
<j:forEach var="parameterDefinition" items="${it.parameterDefinitions}"> <j:forEach var="parameterDefinition" items="${it.parameterDefinitions}">
<j:set var="escapeEntryTitleAndDescription" value="true"/> <!-- SECURITY-353 defense unless overridden -->
<tbody> <tbody>
<st:include it="${parameterDefinition}" <st:include it="${parameterDefinition}"
page="${parameterDefinition.descriptor.valuePage}" /> page="${parameterDefinition.descriptor.valuePage}" />
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter"> <div name="parameter">
<input type="hidden" name="name" value="${it.name}" /> <input type="hidden" name="name" value="${it.name}" />
<f:password name="value" value="${it.DEFAULT_VALUE}"/> <f:password name="value" value="${it.DEFAULT_VALUE}"/>
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:password name="value" value="********" readonly="true" /> <f:password name="value" value="********" readonly="true" />
</f:entry> </f:entry>
</j:jelly> </j:jelly>
\ No newline at end of file
# This file is under the MIT License by authors # This file is under the MIT License by authors
Delete\ this\ build=Kompilazioa ezabatu Delete\ this\ build=Konpilazioa ezabatu
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter"> <div name="parameter">
<input type="hidden" name="name" value="${it.name}" /> <input type="hidden" name="name" value="${it.name}" />
<select name="runId"> <select name="runId">
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<a href="${rootURL}/${it.run.url}" class="model-link inside">${it.run.fullDisplayName}</a> <a href="${rootURL}/${it.run.url}" class="model-link inside">${it.run.fullDisplayName}</a>
</f:entry> </f:entry>
</j:jelly> </j:jelly>
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter"> <div name="parameter">
<input type="hidden" name="name" value="${it.name}" /> <input type="hidden" name="name" value="${it.name}" />
<f:textbox name="value" value="${it.defaultValue}" /> <f:textbox name="value" value="${it.defaultValue}" />
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:textbox name="value" value="${it.value}" readonly="true" /> <f:textbox name="value" value="${it.value}" readonly="true" />
</f:entry> </f:entry>
</j:jelly> </j:jelly>
\ No newline at end of file
...@@ -24,7 +24,8 @@ THE SOFTWARE. ...@@ -24,7 +24,8 @@ THE SOFTWARE.
--> -->
<?jelly escape-by-default='true'?> <?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter"> <div name="parameter">
<input type="hidden" name="name" value="${it.name}"/> <input type="hidden" name="name" value="${it.name}"/>
<f:textarea name="value" value="${it.defaultValue}"/> <f:textarea name="value" value="${it.defaultValue}"/>
......
...@@ -26,7 +26,8 @@ THE SOFTWARE. ...@@ -26,7 +26,8 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project"> xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}"> <j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:textarea name="value" value="${it.value}" readonly="readonly" /> <f:textarea name="value" value="${it.value}" readonly="readonly" />
</f:entry> </f:entry>
</j:jelly> </j:jelly>
\ No newline at end of file
...@@ -4,4 +4,4 @@ Builds=Kompilazioak ...@@ -4,4 +4,4 @@ Builds=Kompilazioak
Configure=Itxuratu Configure=Itxuratu
My\ Views=Nire Begiak My\ Views=Nire Begiak
People=Jendea People=Jendea
Status=Estatus Status=Egoera
# This file is under the MIT License by authors # This file is under the MIT License by authors
People=Gendea People=Jendea
...@@ -23,6 +23,9 @@ allow read,stat <JENKINS_HOME>/userContent($|/.*) ...@@ -23,6 +23,9 @@ allow read,stat <JENKINS_HOME>/userContent($|/.*)
# In the next rule we grant general access under build directories, so first we protect # In the next rule we grant general access under build directories, so first we protect
# the actual build record that Jenkins core reads, which nothing should be touching. # the actual build record that Jenkins core reads, which nothing should be touching.
deny all <BUILDDIR>/build.xml deny all <BUILDDIR>/build.xml
# Similarly for Pipeline build (WorkflowRun) metadata:
deny all <BUILDDIR>/program.dat
deny all <BUILDDIR>/workflow($|/.*)
# Various plugins read/write files under build directories, so allow them all. # Various plugins read/write files under build directories, so allow them all.
# - git 1.x writes changelog.xml from the slave (2.x writes from the master so need not be listed) # - git 1.x writes changelog.xml from the slave (2.x writes from the master so need not be listed)
......
...@@ -32,6 +32,8 @@ THE SOFTWARE. ...@@ -32,6 +32,8 @@ THE SOFTWARE.
<st:attribute name="title"> <st:attribute name="title">
Name of the entry. Think of this like a label for the control. Name of the entry. Think of this like a label for the control.
This content is HTML (unless the boolean variable escapeEntryTitleAndDescription is set). Use h.escape if necessary.
</st:attribute> </st:attribute>
<st:attribute name="field"> <st:attribute name="field">
Used for the databinding. TBD. When this attribute Used for the databinding. TBD. When this attribute
...@@ -46,6 +48,8 @@ THE SOFTWARE. ...@@ -46,6 +48,8 @@ THE SOFTWARE.
This text shouldn't get too long, and in recent Hudson, this feature This text shouldn't get too long, and in recent Hudson, this feature
is somewhat de-emphasized, in favor of the inline foldable help page is somewhat de-emphasized, in favor of the inline foldable help page
specified via @help. specified via @help.
This content is HTML (unless the boolean variable escapeEntryTitleAndDescription is set). Use h.escape if necessary.
</st:attribute> </st:attribute>
<st:attribute name="help"> <st:attribute name="help">
URL to the HTML page. When this attribute is specified, the entry gets URL to the HTML page. When this attribute is specified, the entry gets
...@@ -67,7 +71,7 @@ THE SOFTWARE. ...@@ -67,7 +71,7 @@ THE SOFTWARE.
<tr> <tr>
<td class="setting-leftspace"><st:nbsp/></td> <td class="setting-leftspace"><st:nbsp/></td>
<td class="setting-name"> <td class="setting-name">
<j:out value="${attrs.title}" /> <j:out value="${escapeEntryTitleAndDescription ? h.escape(attrs.title) : attrs.title}" />
</td> </td>
<td class="setting-main"> <td class="setting-main">
<d:invokeBody /> <d:invokeBody />
...@@ -78,7 +82,7 @@ THE SOFTWARE. ...@@ -78,7 +82,7 @@ THE SOFTWARE.
<tr class="validation-error-area"><td colspan="2" /><td /><td /></tr> <tr class="validation-error-area"><td colspan="2" /><td /><td /></tr>
<j:if test="${!empty(attrs.description)}"> <j:if test="${!empty(attrs.description)}">
<f:description> <f:description>
<j:out value="${description}"/> <j:out value="${escapeEntryTitleAndDescription ? h.escape(attrs.description) : attrs.description}"/>
</f:description> </f:description>
</j:if> </j:if>
<j:if test="${attrs.help!=null}"> <j:if test="${attrs.help!=null}">
......
...@@ -23,42 +23,49 @@ class SecretRewriterTest { ...@@ -23,42 +23,49 @@ class SecretRewriterTest {
@Rule public TemporaryFolder tmp = new TemporaryFolder() @Rule public TemporaryFolder tmp = new TemporaryFolder()
def FOO_PATTERN = /<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>/
def MSG_PATTERN = /<msg>\{[A-Za-z0-9+\/]+={0,2}}<\/msg>/
def FOO_PATTERN2 = /(<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>){2}/
def ABC_FOO_PATTERN = /<abc>\s<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>\s<\/abc>/
@Test @Test
void singleFileRewrite() { void singleFileRewrite() {
def o = encryptOld('foobar') // old def o = encryptOld('foobar') // old
def n = encryptNew('foobar') // new def n = encryptNew('foobar') // new
roundtrip "<foo>${o}</foo>", roundtrip "<foo>${o}</foo>",
"<foo>${n}</foo>" {assert it ==~ FOO_PATTERN}
roundtrip "<foo>${o}</foo><foo>${o}</foo>", roundtrip "<foo>${o}</foo><foo>${o}</foo>",
"<foo>${n}</foo><foo>${n}</foo>" {assert it ==~ FOO_PATTERN2}
roundtrip "<foo>${n}</foo>", roundtrip "<foo>${n}</foo>",
"<foo>${n}</foo>" {assert it == "<foo>${n}</foo>"}
roundtrip " <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo> ", roundtrip " <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo> ",
" <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo> " {assert it == "<foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo>"}
// to be rewritten, it needs to be between a tag // to be rewritten, it needs to be between a tag
roundtrip "<foo>$o", "<foo>$o" roundtrip "<foo>$o", {assert it == "<foo>$o"}
roundtrip "$o</foo>", "$o</foo>" roundtrip "$o</foo>", {assert it == "$o</foo>"}
// //
roundtrip "<abc>\n<foo>$o</foo>\n</abc>", "<abc>\n<foo>$n</foo>\n</abc>" roundtrip "<abc>\n<foo>$o</foo>\n</abc>", {assert it ==~ ABC_FOO_PATTERN}
} }
void roundtrip(String before, String after) { void roundtrip(String before, Closure check) {
def sr = new SecretRewriter(null); def sr = new SecretRewriter(null);
def f = File.createTempFile("test", "xml", tmp.root) def f = File.createTempFile("test", "xml", tmp.root)
f.text = before f.text = before
sr.rewrite(f,null) sr.rewrite(f,null)
assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim() check(f.text.replaceAll(System.getProperty("line.separator"), "\n").trim())
//assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim()
} }
String encryptOld(str) { String encryptOld(str) {
def cipher = Secret.getCipher("AES"); def cipher = Secret.getCipher("AES");
cipher.init(Cipher.ENCRYPT_MODE, Secret.legacyKey); cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.legacyKey);
return new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8")))) return new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8"))))
} }
String encryptNew(str) { String encryptNew(str) {
...@@ -70,8 +77,7 @@ class SecretRewriterTest { ...@@ -70,8 +77,7 @@ class SecretRewriterTest {
*/ */
@Test @Test
void recursionDetection() { void recursionDetection() {
def backup = tmp.newFolder("backup") def sw = new SecretRewriter();
def sw = new SecretRewriter(backup);
def st = StreamTaskListener.fromStdout() def st = StreamTaskListener.fromStdout()
def o = encryptOld("Hello world") def o = encryptOld("Hello world")
...@@ -100,12 +106,11 @@ class SecretRewriterTest { ...@@ -100,12 +106,11 @@ class SecretRewriterTest {
assert 6==sw.rewriteRecursive(t, st) assert 6==sw.rewriteRecursive(t, st)
dirs.each { p-> dirs.each { p->
assert new File(t,"$p/foo.xml").text.trim()==answer assert new File(t,"$p/foo.xml").text.trim() ==~ MSG_PATTERN
assert new File(backup,"$p/foo.xml").text.trim()==payload
} }
// t2 is only reachable by following a symlink. this should be covered, too // t2 is only reachable by following a symlink. this should be covered, too
assert new File(t2,"foo.xml").text.trim()==answer.trim(); assert new File(t2,"foo.xml").text.trim() ==~ MSG_PATTERN
} }
} }
...@@ -31,7 +31,8 @@ import org.junit.Rule ...@@ -31,7 +31,8 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.Random; import java.util.Random;
import javax.crypto.Cipher; import javax.crypto.Cipher
import java.util.regex.Pattern;
/** /**
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
...@@ -43,6 +44,8 @@ public class SecretTest { ...@@ -43,6 +44,8 @@ public class SecretTest {
@Rule @Rule
public MockSecretRule mockSecretRule = new MockSecretRule() public MockSecretRule mockSecretRule = new MockSecretRule()
static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?");
@Test @Test
void testEncrypt() { void testEncrypt() {
def secret = Secret.fromString("abc"); def secret = Secret.fromString("abc");
...@@ -54,6 +57,11 @@ public class SecretTest { ...@@ -54,6 +57,11 @@ public class SecretTest {
// can we round trip? // can we round trip?
assert secret==Secret.fromString(secret.encryptedValue); assert secret==Secret.fromString(secret.encryptedValue);
//Two consecutive encryption requests of the same object should result in the same encrypted value - SECURITY-304
assert secret.encryptedValue == secret.encryptedValue
//Two consecutive encryption requests of different objects with the same value should not result in the same encrypted value - SECURITY-304
assert secret.encryptedValue != Secret.fromString(secret.plainText).encryptedValue
} }
@Test @Test
...@@ -62,9 +70,16 @@ public class SecretTest { ...@@ -62,9 +70,16 @@ public class SecretTest {
String plaintext = RandomStringUtils.random(new Random().nextInt(i)); String plaintext = RandomStringUtils.random(new Random().nextInt(i));
String ciphertext = Secret.fromString(plaintext).getEncryptedValue(); String ciphertext = Secret.fromString(plaintext).getEncryptedValue();
//println "${plaintext} → ${ciphertext}" //println "${plaintext} → ${ciphertext}"
assert Secret.ENCRYPTED_VALUE_PATTERN.matcher(ciphertext).matches(); assert ENCRYPTED_VALUE_PATTERN.matcher(ciphertext).matches();
} }
assert !Secret.ENCRYPTED_VALUE_PATTERN.matcher("hello world").matches(); //Not "plain" text
assert !ENCRYPTED_VALUE_PATTERN.matcher("hello world").matches();
//Not "plain" text
assert !ENCRYPTED_VALUE_PATTERN.matcher("helloworld!").matches();
//legacy key
assert ENCRYPTED_VALUE_PATTERN.matcher("abcdefghijklmnopqr0123456789").matches();
//legacy key
assert ENCRYPTED_VALUE_PATTERN.matcher("abcdefghijklmnopqr012345678==").matches();
} }
@Test @Test
...@@ -77,7 +92,7 @@ public class SecretTest { ...@@ -77,7 +92,7 @@ public class SecretTest {
def s = Secret.fromString("Mr.Jenkins"); def s = Secret.fromString("Mr.Jenkins");
def xml = Jenkins.XSTREAM.toXML(s); def xml = Jenkins.XSTREAM.toXML(s);
assert !xml.contains(s.plainText) assert !xml.contains(s.plainText)
assert xml.contains(s.encryptedValue) assert xml ==~ /<hudson\.util\.Secret>\{[A-Za-z0-9+\/]+={0,2}}<\/hudson\.util\.Secret>/
def o = Jenkins.XSTREAM.fromXML(xml); def o = Jenkins.XSTREAM.fromXML(xml);
assert o==s : xml; assert o==s : xml;
...@@ -104,11 +119,11 @@ public class SecretTest { ...@@ -104,11 +119,11 @@ public class SecretTest {
*/ */
@Test @Test
void migrationFromLegacyKeyToConfidentialStore() { void migrationFromLegacyKeyToConfidentialStore() {
def legacy = Secret.legacyKey def legacy = HistoricalSecrets.legacyKey
["Hello world","","\u0000unprintable"].each { str -> ["Hello world","","\u0000unprintable"].each { str ->
def cipher = Secret.getCipher("AES"); def cipher = Secret.getCipher("AES");
cipher.init(Cipher.ENCRYPT_MODE, legacy); cipher.init(Cipher.ENCRYPT_MODE, legacy);
def old = new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8")))) def old = new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8"))))
def s = Secret.fromString(old) def s = Secret.fromString(old)
assert s.plainText==str : "secret by the old key should decrypt" assert s.plainText==str : "secret by the old key should decrypt"
assert s.encryptedValue!=old : "but when encrypting, ConfidentialKey should be in use" assert s.encryptedValue!=old : "but when encrypting, ConfidentialKey should be in use"
......
...@@ -33,7 +33,7 @@ THE SOFTWARE. ...@@ -33,7 +33,7 @@ THE SOFTWARE.
<groupId>org.jenkins-ci.main</groupId> <groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId> <artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version> <version>2.45-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Jenkins main module</name> <name>Jenkins main module</name>
...@@ -181,7 +181,7 @@ THE SOFTWARE. ...@@ -181,7 +181,7 @@ THE SOFTWARE.
<dependency> <dependency>
<groupId>org.jenkins-ci.main</groupId> <groupId>org.jenkins-ci.main</groupId>
<artifactId>remoting</artifactId> <artifactId>remoting</artifactId>
<version>3.4</version> <version>3.4.1</version>
</dependency> </dependency>
<dependency> <dependency>
......
...@@ -28,7 +28,7 @@ THE SOFTWARE. ...@@ -28,7 +28,7 @@ THE SOFTWARE.
<parent> <parent>
<groupId>org.jenkins-ci.main</groupId> <groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId> <artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version> <version>2.45-SNAPSHOT</version>
</parent> </parent>
<artifactId>test</artifactId> <artifactId>test</artifactId>
......
...@@ -519,39 +519,6 @@ public class AbstractProjectTest extends HudsonTestCase { ...@@ -519,39 +519,6 @@ public class AbstractProjectTest extends HudsonTestCase {
done.signal() done.signal()
} }
public void testRenameToPrivileged() {
def secret = jenkins.createProject(FreeStyleProject.class,"secret");
def regular = jenkins.createProject(FreeStyleProject.class,"regular")
jenkins.securityRealm = createDummySecurityRealm();
def auth = new ProjectMatrixAuthorizationStrategy();
jenkins.authorizationStrategy = auth;
auth.add(Jenkins.ADMINISTER, "alice");
auth.add(Jenkins.READ, "bob");
// bob the regular user can only see regular jobs
regular.addProperty(new AuthorizationMatrixProperty([(Job.READ) : ["bob"] as Set]));
def wc = createWebClient()
wc.login("bob")
wc.executeOnServer {
assert jenkins.getItem("secret")==null;
try {
regular.renameTo("secret")
fail("rename as an overwrite should have failed");
} catch (Exception e) {
// expected rename to fail in some non-descriptive generic way
e.printStackTrace()
}
}
// those two jobs should still be there
assert jenkins.getItem("regular")!=null;
assert jenkins.getItem("secret")!=null;
}
/** /**
* Trying to POST to config.xml by a different job type should fail. * Trying to POST to config.xml by a different job type should fail.
*/ */
......
/*
* The MIT License
*
* Copyright 2016 CloudBees, Inc.
*
* 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 hudson.console;
import hudson.MarkupText;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringWriter;
import java.util.logging.Level;
import org.apache.commons.io.Charsets;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.For;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.LoggerRule;
import org.kohsuke.stapler.framework.io.ByteBuffer;
@For({AnnotatedLargeText.class, ConsoleNote.class, ConsoleAnnotationOutputStream.class, PlainTextConsoleOutputStream.class})
public class AnnotatedLargeTextTest {
@ClassRule
public static JenkinsRule r = new JenkinsRule();
@Rule
public LoggerRule logging = new LoggerRule().record(ConsoleAnnotationOutputStream.class, Level.FINE).capture(100);
@Test
public void smokes() throws Exception {
ByteBuffer buf = new ByteBuffer();
PrintStream ps = new PrintStream(buf, true);
ps.print("Some text.\n");
ps.print("Go back to " + TestNote.encodeTo("/root", "your home") + ".\n");
ps.print("More text.\n");
AnnotatedLargeText<Void> text = new AnnotatedLargeText<>(buf, Charsets.UTF_8, true, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
text.writeLogTo(0, baos);
assertEquals("Some text.\nGo back to your home.\nMore text.\n", baos.toString());
StringWriter w = new StringWriter();
text.writeHtmlTo(0, w);
assertEquals("Some text.\nGo back to <a href='/root'>your home</a>.\nMore text.\n", w.toString());
}
@Issue("SECURITY-382")
@Test
public void oldDeserialization() throws Exception {
ByteBuffer buf = new ByteBuffer();
buf.write(("hello" + ConsoleNote.PREAMBLE_STR + "AAAAwR+LCAAAAAAAAP9dzLEOwVAUxvHThtiNprYxsGiMQhiwNSIhMR/tSZXr3Lr3oJPwPt7FM5hM3gFh8i3/5Bt+1yeUrYH6ap9Yza1Ys9WKWuMiR05wqWhEgpmyEy306Jxvwb19ccGNoBJjLplmgWq0xgOGCjkNZ2IyTrsRlFayVTs4gVMYqP3pw28/JnznuABF/rYWyIyeJfLQe1vxZiDQ7NnYZLn0UZGRRjA9MiV+0OyFv3+utadQyH8B+aJxVM4AAAA=" + ConsoleNote.POSTAMBLE_STR + "there\n").getBytes());
AnnotatedLargeText<Void> text = new AnnotatedLargeText<>(buf, Charsets.UTF_8, true, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
text.writeLogTo(0, baos);
assertEquals("hellothere\n", baos.toString());
StringWriter w = new StringWriter();
text.writeHtmlTo(0, w);
assertEquals("hellothere\n", w.toString());
assertThat(logging.getMessages(), hasItem("Failed to resurrect annotation")); // TODO assert that this is IOException: Refusing to deserialize unsigned note from an old log.
ConsoleNote.INSECURE = true;
try {
w = new StringWriter();
text.writeHtmlTo(0, w);
assertThat(w.toString(), containsString("<script>"));
} finally {
ConsoleNote.INSECURE = false;
}
}
@Issue("SECURITY-382")
@Test
public void badMac() throws Exception {
ByteBuffer buf = new ByteBuffer();
buf.write(("Go back to " + ConsoleNote.PREAMBLE_STR + "////4ByIhqPpAc43AbrEtyDUDc1/UEOXsoY6LeoHSeSlb1d7AAAAlR+LCAAAAAAAAP9b85aBtbiIQS+jNKU4P08vOT+vOD8nVc8xLy+/JLEkNcUnsSg9NSS1oiQktbhEBUT45ZekCpys9xWo8J3KxMDkycCWk5qXXpLhw8BcWpRTwiDkk5VYlqifk5iXrh9cUpSZl25dUcQghWaBM4QGGcYAAYxMDAwVBUAGZwkDq35Rfn4JABmN28qcAAAA" + ConsoleNote.POSTAMBLE_STR + "your home.\n").getBytes());
AnnotatedLargeText<Void> text = new AnnotatedLargeText<>(buf, Charsets.UTF_8, true, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
text.writeLogTo(0, baos);
assertEquals("Go back to your home.\n", baos.toString());
StringWriter w = new StringWriter();
text.writeHtmlTo(0, w);
assertEquals("Go back to your home.\n", w.toString());
assertThat(logging.getMessages(), hasItem("Failed to resurrect annotation")); // TODO assert that this is IOException: MAC mismatch
}
/** Simplified version of {@link HyperlinkNote}. */
static class TestNote extends ConsoleNote<Void> {
private final String url;
private final int length;
TestNote(String url, int length) {
this.url = url;
this.length = length;
}
@Override
public ConsoleAnnotator<?> annotate(Void context, MarkupText text, int charPos) {
text.addMarkup(charPos, charPos + length, "<a href='" + url + "'" + ">", "</a>");
return null;
}
static String encodeTo(String url, String text) throws IOException {
return new TestNote(url, text.length()).encode() + text;
}
}
}
...@@ -27,6 +27,7 @@ import java.net.URL; ...@@ -27,6 +27,7 @@ import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
...@@ -117,8 +118,7 @@ public class ConsoleAnnotatorTest { ...@@ -117,8 +118,7 @@ public class ConsoleAnnotatorTest {
// make sure raw console output doesn't include the garbage // make sure raw console output doesn't include the garbage
TextPage raw = (TextPage)r.createWebClient().goTo(b.getUrl()+"consoleText","text/plain"); TextPage raw = (TextPage)r.createWebClient().goTo(b.getUrl()+"consoleText","text/plain");
System.out.println(raw.getContent()); assertThat(raw.getContent(), containsString("\nabc\ndef\n"));
assertTrue(raw.getContent().contains("\nabc\ndef\n"));
} }
......
package hudson.diagnosis; package hudson.diagnosis;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import hudson.model.User;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import hudson.security.HudsonPrivateSecurityRealm;
import hudson.security.Permission;
import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContextHolder;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlPage;
...@@ -13,6 +25,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm; ...@@ -13,6 +25,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.ElementNotFoundException; import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
/** /**
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
...@@ -45,6 +58,48 @@ public class HudsonHomeDiskUsageMonitorTest { ...@@ -45,6 +58,48 @@ public class HudsonHomeDiskUsageMonitorTest {
} }
} }
@Issue("SECURITY-371")
@Test
public void noAccessForNonAdmin() throws Exception {
JenkinsRule.WebClient wc = j.createWebClient();
// TODO: Use MockAuthorizationStrategy in later versions
JenkinsRule.DummySecurityRealm realm = j.createDummySecurityRealm();
realm.addGroups("administrator", "admins");
realm.addGroups("bob", "users");
j.jenkins.setSecurityRealm(realm);
GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();
auth.add(Jenkins.ADMINISTER, "admins");
auth.add(Jenkins.READ, "users");
j.jenkins.setAuthorizationStrategy(auth);
WebRequest request = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull/act"), HttpMethod.POST);
NameValuePair param = new NameValuePair("no", "true");
request.setRequestParameters(Collections.singletonList(param));
HudsonHomeDiskUsageMonitor mon = HudsonHomeDiskUsageMonitor.get();
try {
wc.login("bob");
wc.getPage(request);
} catch (FailingHttpStatusCodeException e) {
assertEquals(403, e.getStatusCode());
}
assertTrue(mon.isEnabled());
try {
WebRequest getIndex = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull"), HttpMethod.GET);
wc.getPage(getIndex);
} catch (FailingHttpStatusCodeException e) {
assertEquals(403, e.getStatusCode());
}
wc.login("administrator");
wc.getPage(request);
assertFalse(mon.isEnabled());
}
/** /**
* Gets the warning form. * Gets the warning form.
*/ */
......
...@@ -27,6 +27,8 @@ import static org.hamcrest.MatcherAssert.assertThat; ...@@ -27,6 +27,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import hudson.security.ACL; import hudson.security.ACL;
...@@ -49,6 +51,7 @@ import org.junit.After; ...@@ -49,6 +51,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerResponse;
...@@ -141,6 +144,26 @@ public class ComputerConfigDotXmlTest { ...@@ -141,6 +144,26 @@ public class ComputerConfigDotXmlTest {
assertThat(updatedSlave.getNumExecutors(), equalTo(42)); assertThat(updatedSlave.getNumExecutors(), equalTo(42));
} }
@Test
@Issue("SECURITY-343")
public void emptyNodeMonitorDataWithoutConnect() throws Exception {
rule.jenkins.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy());
assertTrue(computer.getMonitorData().isEmpty());
}
@Test
@Issue("SECURITY-343")
public void populatedNodeMonitorDataWithConnect() throws Exception {
GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();
rule.jenkins.setAuthorizationStrategy(auth);
auth.add(Computer.CONNECT, "user");
assertFalse(computer.getMonitorData().isEmpty());
}
private OutputStream captureOutput() throws IOException { private OutputStream captureOutput() throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream();
......
...@@ -26,16 +26,19 @@ package hudson.model; ...@@ -26,16 +26,19 @@ package hudson.model;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.xml.XmlPage;
import java.io.File; import java.io.File;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import hudson.slaves.DumbSlave; import hudson.slaves.DumbSlave;
import hudson.slaves.OfflineCause;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
...@@ -43,6 +46,7 @@ import org.junit.Test; ...@@ -43,6 +46,7 @@ import org.junit.Test;
import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient; import org.jvnet.hudson.test.JenkinsRule.WebClient;
import org.jvnet.hudson.test.recipes.LocalData;
public class ComputerTest { public class ComputerTest {
...@@ -87,4 +91,27 @@ public class ComputerTest { ...@@ -87,4 +91,27 @@ public class ComputerTest {
containsString("Agent called ‘nodeA’ already exists")); containsString("Agent called ‘nodeA’ already exists"));
} }
} }
@Test
public void doNotShowUserDetailsInOfflineCause() throws Exception {
DumbSlave slave = j.createOnlineSlave();
final Computer computer = slave.toComputer();
computer.setTemporarilyOffline(true, new OfflineCause.UserCause(User.get("username"), "msg"));
verifyOfflineCause(computer);
}
@Test @LocalData
public void removeUserDetailsFromOfflineCause() throws Exception {
Computer computer = j.jenkins.getComputer("deserialized");
verifyOfflineCause(computer);
}
private void verifyOfflineCause(Computer computer) throws Exception {
XmlPage page = j.createWebClient().goToXml("computer/" + computer.getName() + "/config.xml");
String content = page.getWebResponse().getContentAsString("UTF-8");
assertThat(content, containsString("temporaryOfflineCause"));
assertThat(content, containsString("<userId>username</userId>"));
assertThat(content, not(containsString("ApiTokenProperty")));
assertThat(content, not(containsString("apiToken")));
}
} }
...@@ -24,8 +24,26 @@ ...@@ -24,8 +24,26 @@
package hudson.model; package hudson.model;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import hudson.AbortException;
import hudson.cli.CLICommand;
import hudson.cli.CLICommandInvoker;
import hudson.cli.CopyJobCommand;
import hudson.cli.CreateJobCommand;
import hudson.security.ACL;
import hudson.security.csrf.CrumbIssuer;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.apache.commons.httpclient.HttpStatus;
import org.junit.Test; import org.junit.Test;
...@@ -35,6 +53,7 @@ import org.junit.Rule; ...@@ -35,6 +53,7 @@ import org.junit.Rule;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;
import org.jvnet.hudson.test.MockFolder; import org.jvnet.hudson.test.MockFolder;
public class ItemsTest { public class ItemsTest {
...@@ -100,5 +119,171 @@ public class ItemsTest { ...@@ -100,5 +119,171 @@ public class ItemsTest {
assertFalse(new File(tmp, "foo/test/1").exists()); assertFalse(new File(tmp, "foo/test/1").exists());
assertTrue(new File(tmp, "bar/test/1").exists()); assertTrue(new File(tmp, "bar/test/1").exists());
} }
// TODO would be more efficient to run these all as a single test case, but after a few Jetty seems to stop serving new content and new requests just hang.
private void overwriteTargetSetUp() throws Exception {
// A fully visible item:
r.createFreeStyleProject("visible").setDescription("visible");
// An item known to exist but not visible:
r.createFreeStyleProject("known").setDescription("known");
// An item not even known to exist:
r.createFreeStyleProject("secret").setDescription("secret");
// A folder from which to launch move attacks:
r.createFolder("d");
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
grant(Jenkins.READ).everywhere().to("attacker").
grant(Item.READ, Item.CONFIGURE, Item.CREATE, Item.DELETE).onPaths("(?!known|secret).*").to("attacker").
grant(Item.DISCOVER).onPaths("known").to("attacker"));
}
/** Control cases: if there is no such item yet, nothing is stopping you. */
@Test public void overwriteNonexistentTarget() throws Exception {
overwriteTargetSetUp();
for (OverwriteTactic tactic : OverwriteTactic.values()) {
tactic.run(r, "nonexistent");
System.out.println(tactic + " worked as expected on a nonexistent target");
r.jenkins.getItem("nonexistent").delete();
}
}
private void cannotOverwrite(String target) throws Exception {
overwriteTargetSetUp();
for (OverwriteTactic tactic : OverwriteTactic.values()) {
try {
tactic.run(r, target);
fail(tactic + " was not supposed to work against " + target);
} catch (Exception x) {
System.out.println("good, " + tactic + " failed on " + target + ": " + x);
assertEquals(tactic + " still overwrote " + target, target, r.jenkins.getItemByFullName(target, FreeStyleProject.class).getDescription());
}
}
}
/** More control cases: for non-security-sensitive scenarios, we prevent you from overwriting existing items. */
@Test public void overwriteVisibleTarget() throws Exception {
cannotOverwrite("visible");
}
/** You may not overwrite an item you know is there even if you cannot see it. */
@Test public void overwriteKnownTarget() throws Exception {
cannotOverwrite("known");
}
/** You are somehow prevented from overwriting an item even if you did not previously know it was there. */
@Issue("SECURITY-321")
@Test public void overwriteHiddenTarget() throws Exception {
cannotOverwrite("secret");
}
/** All known means of creating an item under a new name. */
private enum OverwriteTactic {
/** Use the REST command to create an empty project (normally used only from the UI in the New Item dialog). */
REST_EMPTY {
@Override void run(JenkinsRule r, String target) throws Exception {
JenkinsRule.WebClient wc = wc(r);
wc.getOptions().setRedirectEnabled(false);
wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // redirect perversely counts as a failure
WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=hudson.model.FreeStyleProject"), HttpMethod.POST)).getWebResponse();
if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
throw new FailingHttpStatusCodeException(webResponse);
}
}
},
/** Use the REST command to copy an existing project (normally used from the UI in the New Item dialog). */
REST_COPY {
@Override void run(JenkinsRule r, String target) throws Exception {
r.createFreeStyleProject("dupe");
JenkinsRule.WebClient wc = wc(r);
wc.getOptions().setRedirectEnabled(false);
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=copy&from=dupe"), HttpMethod.POST)).getWebResponse();
r.jenkins.getItem("dupe").delete();
if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
throw new FailingHttpStatusCodeException(webResponse);
}
}
},
/** Overwrite target using REST command to create a project from XML submission. */
REST_CREATE {
@Override void run(JenkinsRule r, String target) throws Exception {
JenkinsRule.WebClient wc = wc(r);
WebRequest req = new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target), HttpMethod.POST);
req.setAdditionalHeader("Content-Type", "application/xml");
req.setRequestBody("<project/>");
wc.getPage(req);
}
},
/** Overwrite target using REST command to rename an existing project (normally used from the UI in the Configure screen). */
REST_RENAME {
@Override void run(JenkinsRule r, String target) throws Exception {
r.createFreeStyleProject("dupe");
JenkinsRule.WebClient wc = wc(r);
wc.getOptions().setRedirectEnabled(false);
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "job/dupe/doRename?newName=" + target), HttpMethod.POST)).getWebResponse();
if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
r.jenkins.getItem("dupe").delete();
throw new FailingHttpStatusCodeException(webResponse);
}
assertNull(r.jenkins.getItem("dupe"));
}
},
/** Overwrite target using the CLI {@code create-job} command. */
CLI_CREATE {
@Override void run(JenkinsRule r, String target) throws Exception {
CLICommand cmd = new CreateJobCommand();
CLICommandInvoker invoker = new CLICommandInvoker(r, cmd);
cmd.setTransportAuth(User.get("attacker").impersonate());
int status = invoker.withStdin(new ByteArrayInputStream("<project/>".getBytes("US-ASCII"))).invokeWithArgs(target).returnCode();
if (status != 0) {
throw new AbortException("CLI command failed with status " + status);
}
}
},
/** Overwrite target using the CLI {@code copy-job} command. */
CLI_COPY {
@Override void run(JenkinsRule r, String target) throws Exception {
r.createFreeStyleProject("dupe");
CLICommand cmd = new CopyJobCommand();
CLICommandInvoker invoker = new CLICommandInvoker(r, cmd);
cmd.setTransportAuth(User.get("attacker").impersonate());
int status = invoker.invokeWithArgs("dupe", target).returnCode();
r.jenkins.getItem("dupe").delete();
if (status != 0) {
throw new AbortException("CLI command failed with status " + status);
}
}
},
/** Overwrite target using a move function normally called from {@code cloudbees-folder} via a {@code move} action. */
MOVE {
@Override void run(JenkinsRule r, String target) throws Exception {
try {
SecurityContext orig = ACL.impersonate(User.get("attacker").impersonate());
try {
Items.move(r.jenkins.getItemByFullName("d", MockFolder.class).createProject(FreeStyleProject.class, target), r.jenkins);
} finally {
SecurityContextHolder.setContext(orig);
}
assertNull(r.jenkins.getItemByFullName("d/" + target));
} catch (Exception x) {
r.jenkins.getItemByFullName("d/" + target).delete();
throw x;
}
}
};
abstract void run(JenkinsRule r, String target) throws Exception;
private static final JenkinsRule.WebClient wc(JenkinsRule r) throws Exception {
return r.createWebClient().login("attacker");
}
// TODO replace with standard version once it is fixed to detect an existing query string
private static URL createCrumbedUrl(JenkinsRule r, JenkinsRule.WebClient wc, String relativePath) throws IOException {
CrumbIssuer issuer = r.jenkins.getCrumbIssuer();
String crumbName = issuer.getDescriptor().getCrumbRequestField();
String crumb = issuer.getCrumb(null);
return new URL(wc.getContextPath() + relativePath + (relativePath.contains("?") ? "&" : "?") + crumbName + "=" + crumb);
}
}
} }
...@@ -35,6 +35,7 @@ import hudson.model.Queue.WaitingItem; ...@@ -35,6 +35,7 @@ import hudson.model.Queue.WaitingItem;
import hudson.model.labels.*; import hudson.model.labels.*;
import hudson.model.queue.CauseOfBlockage; import hudson.model.queue.CauseOfBlockage;
import hudson.security.ACL; import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.GlobalMatrixAuthorizationStrategy; import hudson.security.GlobalMatrixAuthorizationStrategy;
import hudson.security.HudsonPrivateSecurityRealm; import hudson.security.HudsonPrivateSecurityRealm;
import hudson.security.Permission; import hudson.security.Permission;
...@@ -53,6 +54,8 @@ import java.util.concurrent.Callable; ...@@ -53,6 +54,8 @@ import java.util.concurrent.Callable;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import jenkins.security.QueueItemAuthenticatorConfiguration; import jenkins.security.QueueItemAuthenticatorConfiguration;
import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.context.SecurityContextHolder;
import static org.hamcrest.core.StringEndsWith.endsWith;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.junit.Before; import org.junit.Before;
...@@ -123,6 +126,32 @@ public class NodeTest { ...@@ -123,6 +126,32 @@ public class NodeTest {
assertNull(computer.getOfflineCause()); assertNull(computer.getOfflineCause());
} }
@Test
public void testOfflineCauseAsAnonymous() throws Exception {
Node node = j.createOnlineSlave();
final Computer computer = node.toComputer();
OfflineCause.UserCause cause;
try (ACLContext ctxt = ACL.as(Jenkins.ANONYMOUS)) {
computer.doToggleOffline("original message");
}
cause = (UserCause) computer.getOfflineCause();
assertThat(cause.toString(), endsWith("Disconnected by anonymous : original message"));
assertEquals(User.getUnknown(), cause.getUser());
final User root = User.get("root@localhost");
try (ACLContext ctxt = ACL.as(root.impersonate())) {
computer.doChangeOfflineCause("new message");
}
cause = (UserCause) computer.getOfflineCause();
assertThat(cause.toString(), endsWith("Disconnected by root@localhost : new message"));
assertEquals(root, cause.getUser());
computer.doToggleOffline(null);
assertNull(computer.getOfflineCause());
}
@Test @Test
public void testGetLabelCloud() throws Exception { public void testGetLabelCloud() throws Exception {
Node node = j.createOnlineSlave(); Node node = j.createOnlineSlave();
......
package hudson.model; package hudson.model;
import static org.junit.Assert.*;
import com.gargoylesoftware.htmlunit.html.DomNodeUtil; import com.gargoylesoftware.htmlunit.html.DomNodeUtil;
import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlFormUtil; import com.gargoylesoftware.htmlunit.html.HtmlFormUtil;
import org.junit.Rule; import com.gargoylesoftware.htmlunit.html.HtmlOption;
import org.junit.Test;
import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlTextInput; import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput; import hudson.markup.MarkupFormatter;
import com.gargoylesoftware.htmlunit.html.HtmlOption; import java.io.IOException;
import java.io.Writer;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpStatus;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.jvnet.hudson.test.CaptureEnvironmentBuilder; import org.jvnet.hudson.test.CaptureEnvironmentBuilder;
import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient; import org.jvnet.hudson.test.JenkinsRule.WebClient;
import java.util.Set;
/** /**
* @author huybrechts * @author huybrechts
*/ */
...@@ -28,6 +33,9 @@ public class ParametersTest { ...@@ -28,6 +33,9 @@ public class ParametersTest {
@Rule @Rule
public JenkinsRule j = new JenkinsRule(); public JenkinsRule j = new JenkinsRule();
@Rule
public ErrorCollector collector = new ErrorCollector();
@Test @Test
public void parameterTypes() throws Exception { public void parameterTypes() throws Exception {
FreeStyleProject otherProject = j.createFreeStyleProject(); FreeStyleProject otherProject = j.createFreeStyleProject();
...@@ -216,4 +224,52 @@ public class ParametersTest { ...@@ -216,4 +224,52 @@ public class ParametersTest {
final HtmlForm form = page.getFormByName("parameters"); final HtmlForm form = page.getFormByName("parameters");
HtmlFormUtil.submit(form, HtmlFormUtil.getButtonByCaption(form, "Build")); HtmlFormUtil.submit(form, HtmlFormUtil.getButtonByCaption(form, "Build"));
} }
@Issue("SECURITY-353")
@Test
public void xss() throws Exception {
j.jenkins.setMarkupFormatter(new MyMarkupFormatter());
FreeStyleProject p = j.createFreeStyleProject("p");
StringParameterDefinition param = new StringParameterDefinition("<param name>", "<param default>", "<param description>");
assertEquals("<b>[</b>param description<b>]</b>", param.getFormattedDescription());
p.addProperty(new ParametersDefinitionProperty(param));
WebClient wc = j.createWebClient();
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
HtmlPage page = wc.getPage(p, "build?delay=0sec");
collector.checkThat(page.getWebResponse().getStatusCode(), is(HttpStatus.SC_METHOD_NOT_ALLOWED)); // 405 to dissuade scripts from thinking this triggered the build
String text = page.getWebResponse().getContentAsString();
collector.checkThat("build page should escape param name", text, containsString("&lt;param name&gt;"));
collector.checkThat("build page should not leave param name unescaped", text, not(containsString("<param name>")));
collector.checkThat("build page should escape param default", text, containsString("&lt;param default&gt;"));
collector.checkThat("build page should not leave param default unescaped", text, not(containsString("<param default>")));
collector.checkThat("build page should mark up param description", text, containsString("<b>[</b>param description<b>]</b>"));
collector.checkThat("build page should not leave param description unescaped", text, not(containsString("<param description>")));
HtmlForm form = page.getFormByName("parameters");
HtmlTextInput value = form.getInputByValue("<param default>");
value.setText("<param value>");
j.submit(form);
j.waitUntilNoActivity();
FreeStyleBuild b = p.getBuildByNumber(1);
page = j.createWebClient().getPage(b, "parameters/");
text = page.getWebResponse().getContentAsString();
collector.checkThat("parameters page should escape param name", text, containsString("&lt;param name&gt;"));
collector.checkThat("parameters page should not leave param name unescaped", text, not(containsString("<param name>")));
collector.checkThat("parameters page should escape param value", text, containsString("&lt;param value&gt;"));
collector.checkThat("parameters page should not leave param value unescaped", text, not(containsString("<param value>")));
collector.checkThat("parameters page should mark up param description", text, containsString("<b>[</b>param description<b>]</b>"));
collector.checkThat("parameters page should not leave param description unescaped", text, not(containsString("<param description>")));
}
static class MyMarkupFormatter extends MarkupFormatter {
@Override
public void translate(String markup, Writer output) throws IOException {
Matcher m = Pattern.compile("[<>]").matcher(markup);
StringBuffer buf = new StringBuffer();
while (m.find()) {
m.appendReplacement(buf, m.group().equals("<") ? "<b>[</b>" : "<b>]</b>");
}
m.appendTail(buf);
output.write(buf.toString());
}
}
} }
...@@ -32,14 +32,19 @@ import static org.junit.Assert.fail; ...@@ -32,14 +32,19 @@ import static org.junit.Assert.fail;
import hudson.model.FreeStyleProject; import hudson.model.FreeStyleProject;
import hudson.model.ListView; import hudson.model.ListView;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import hudson.model.User; import hudson.model.User;
import hudson.model.View;
import hudson.security.ACL; import hudson.security.ACL;
import hudson.security.ACLContext; import hudson.security.ACLContext;
import hudson.security.AuthorizationStrategy;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import net.sf.json.JSONArray; import net.sf.json.JSONArray;
import net.sf.json.JSONObject; import net.sf.json.JSONObject;
...@@ -383,6 +388,37 @@ public class SearchTest { ...@@ -383,6 +388,37 @@ public class SearchTest {
assertTrue(suggest(j.jenkins.getSearchIndex(),"foo").contains(p)); assertTrue(suggest(j.jenkins.getSearchIndex(),"foo").contains(p));
} }
@Issue("SECURITY-385")
@Test
public void testInaccessibleViews() throws IOException {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
GlobalMatrixAuthorizationStrategy strategy = new GlobalMatrixAuthorizationStrategy();
strategy.add(Jenkins.READ, "alice");
j.jenkins.setAuthorizationStrategy(strategy);
j.jenkins.addView(new ListView("foo", j.jenkins));
// SYSTEM can see all the views
assertEquals("two views exist", 2, Jenkins.getInstance().getViews().size());
List<SearchItem> results = new ArrayList<>();
j.jenkins.getSearchIndex().suggest("foo", results);
assertEquals("nonempty results list", 1, results.size());
// Alice can't
assertFalse("no permission", j.jenkins.getView("foo").getACL().hasPermission(User.get("alice").impersonate(), View.READ));
ACL.impersonate(User.get("alice").impersonate(), new Runnable() {
@Override
public void run() {
assertEquals("no visible views", 0, Jenkins.getInstance().getViews().size());
List<SearchItem> results = new ArrayList<>();
j.jenkins.getSearchIndex().suggest("foo", results);
assertEquals("empty results list", Collections.emptyList(), results);
}
});
}
@Test @Test
public void testSearchWithinFolders() throws Exception { public void testSearchWithinFolders() throws Exception {
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2016, CloudBees Inc.
*
* 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 hudson.util;
import hudson.model.FreeStyleProject;
import hudson.model.ParameterDefinition;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.PasswordParameterDefinition;
import org.hamcrest.core.Is;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;
import java.io.IOException;
import java.util.regex.Pattern;
import static org.hamcrest.core.Is.isA;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.*;
/**
* Tests {@link Secret}.
*/
public class SecretCompatTest {
@Rule
public JenkinsRule j = new JenkinsRule() {
@Override
public void before() throws Throwable {
Secret.resetKeyForTest(); //As early as possible
super.before();
}
};
@After
public void after() {
Secret.resetKeyForTest();
}
@Test
@Issue("SECURITY-304")
public void encryptedValueStaysTheSameAfterRoundtrip() throws Exception {
FreeStyleProject project = j.createFreeStyleProject();
project.addProperty(new ParametersDefinitionProperty(new PasswordParameterDefinition("p", "s3cr37", "Keep this a secret")));
project.getAllActions(); // initialize Actionable.actions; otherwise first made nonnull while rendering sidepanel after redirect after round #1 has been saved, so only round #2 has <actions/>
project = j.configRoundtrip(project);
String round1 = project.getConfigFile().asString();
project = j.configRoundtrip(project);
String round2 = project.getConfigFile().asString();
assertEquals(round1, round2);
//But reconfiguring will make it a new value
project = j.jenkins.getItemByFullName(project.getFullName(), FreeStyleProject.class);
project.removeProperty(ParametersDefinitionProperty.class);
project.addProperty(new ParametersDefinitionProperty(new PasswordParameterDefinition("p", "s3cr37", "Keep this a secret")));
project = j.configRoundtrip(project);
String round3 = project.getConfigFile().asString();
assertNotEquals(round2, round3);
//Saving again will produce the same
project = j.configRoundtrip(project);
String round4 = project.getConfigFile().asString();
assertEquals(round3, round4);
}
@Test
@Issue("SECURITY-304")
@LocalData
public void canReadPreSec304Secrets() throws Exception {
FreeStyleProject project = j.jenkins.getItemByFullName("OldSecret", FreeStyleProject.class);
String oldxml = project.getConfigFile().asString();
//It should be unchanged on disk
assertThat(oldxml, containsString("<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>"));
ParametersDefinitionProperty property = project.getProperty(ParametersDefinitionProperty.class);
ParameterDefinition definition = property.getParameterDefinitions().get(0);
assertTrue(definition instanceof PasswordParameterDefinition);
Secret secret = ((PasswordParameterDefinition) definition).getDefaultValueAsSecret();
assertEquals("theSecret", secret.getPlainText());
//OK it was read correctly from disk, now the first roundtrip should update the encrypted value
project = j.configRoundtrip(project);
String newXml = project.getConfigFile().asString();
assertNotEquals(oldxml, newXml); //This could have changed because Jenkins has moved on, so not really a good check
assertThat(newXml, not(containsString("<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>")));
Pattern p = Pattern.compile("<defaultValue>\\{[A-Za-z0-9+/]+={0,2}}</defaultValue>");
assertTrue(p.matcher(newXml).find());
//But the next roundtrip should result in the same data
project = j.configRoundtrip(project);
String round2 = project.getConfigFile().asString();
assertEquals(newXml, round2);
}
}
package hudson.util;
import hudson.model.Items;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.when;
public class XStream2Security383Test {
@Rule
public JenkinsRule j = new JenkinsRule();
@Rule
public TemporaryFolder f = new TemporaryFolder();
@Mock
private StaplerRequest req;
@Mock
private StaplerResponse rsp;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
@Issue("SECURITY-383")
public void testXmlLoad() throws Exception {
File exploitFile = f.newFile();
try {
// be extra sure there's no file already
if (exploitFile.exists() && !exploitFile.delete()) {
throw new IllegalStateException("file exists and cannot be deleted");
}
File tempJobDir = new File(j.jenkins.getRootDir(), "security383");
String exploitXml = IOUtils.toString(
XStream2Security383Test.class.getResourceAsStream(
"/hudson/util/XStream2Security383Test/config.xml"), "UTF-8");
exploitXml = exploitXml.replace("@TOKEN@", exploitFile.getAbsolutePath());
FileUtils.write(new File(tempJobDir, "config.xml"), exploitXml);
try {
Items.load(j.jenkins, tempJobDir);
} catch (Exception e) {
// ignore
}
assertFalse("no file should be created here", exploitFile.exists());
} finally {
exploitFile.delete();
}
}
@Test
@Issue("SECURITY-383")
public void testPostJobXml() throws Exception {
File exploitFile = f.newFile();
try {
// be extra sure there's no file already
if (exploitFile.exists() && !exploitFile.delete()) {
throw new IllegalStateException("file exists and cannot be deleted");
}
File tempJobDir = new File(j.jenkins.getRootDir(), "security383");
String exploitXml = IOUtils.toString(
XStream2Security383Test.class.getResourceAsStream(
"/hudson/util/XStream2Security383Test/config.xml"), "UTF-8");
exploitXml = exploitXml.replace("@TOKEN@", exploitFile.getAbsolutePath());
when(req.getMethod()).thenReturn("POST");
when(req.getInputStream()).thenReturn(new Stream(IOUtils.toInputStream(exploitXml)));
when(req.getContentType()).thenReturn("application/xml");
when(req.getParameter("name")).thenReturn("foo");
try {
j.jenkins.doCreateItem(req, rsp);
} catch (Exception e) {
// don't care
}
assertFalse("no file should be created here", exploitFile.exists());
} finally {
exploitFile.delete();
}
}
private static class Stream extends ServletInputStream {
private final InputStream inner;
public Stream(final InputStream inner) {
this.inner = inner;
}
@Override
public int read() throws IOException {
return inner.read();
}
@Override
public boolean isFinished() {
throw new UnsupportedOperationException();
}
@Override
public boolean isReady() {
throw new UnsupportedOperationException();
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
}
...@@ -32,6 +32,7 @@ import static org.hamcrest.Matchers.not; ...@@ -32,6 +32,7 @@ import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
...@@ -93,6 +94,28 @@ public class JenkinsTest { ...@@ -93,6 +94,28 @@ public class JenkinsTest {
@Rule public JenkinsRule j = new JenkinsRule(); @Rule public JenkinsRule j = new JenkinsRule();
@Issue("SECURITY-406")
@Test
public void testUserCreationFromUrlForAdmins() throws Exception {
WebClient wc = j.createWebClient();
assertNull("User not supposed to exist", User.getById("nonexistent", false));
wc.assertFails("user/nonexistent", 404);
assertNull("User not supposed to exist", User.getById("nonexistent", false));
try {
User.ALLOW_USER_CREATION_VIA_URL = true;
// expected to work
wc.goTo("user/nonexistent2");
assertNotNull("User supposed to exist", User.getById("nonexistent2", false));
} finally {
User.ALLOW_USER_CREATION_VIA_URL = false;
}
}
@Test @Test
public void testIsDisplayNameUniqueTrue() throws Exception { public void testIsDisplayNameUniqueTrue() throws Exception {
final String curJobName = "curJobName"; final String curJobName = "curJobName";
......
...@@ -11,6 +11,7 @@ import hudson.Util; ...@@ -11,6 +11,7 @@ import hudson.Util;
import hudson.util.Secret; import hudson.util.Secret;
import hudson.util.SecretHelper; import hudson.util.SecretHelper;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.hamcrest.CoreMatchers;
import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.recipes.Recipe.Runner; import org.jvnet.hudson.test.recipes.Recipe.Runner;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
...@@ -20,6 +21,9 @@ import javax.inject.Inject; ...@@ -20,6 +21,9 @@ import javax.inject.Inject;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.regex.Pattern;
import static org.junit.Assert.assertThat;
/** /**
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
...@@ -28,6 +32,8 @@ public class RekeySecretAdminMonitorTest extends HudsonTestCase { ...@@ -28,6 +32,8 @@ public class RekeySecretAdminMonitorTest extends HudsonTestCase {
@Inject @Inject
RekeySecretAdminMonitor monitor; RekeySecretAdminMonitor monitor;
final String plain_regex_match = ".*\\{[A-Za-z0-9+/]+={0,2}}.*";
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
SecretHelper.set(TEST_KEY); SecretHelper.set(TEST_KEY);
...@@ -76,8 +82,8 @@ public class RekeySecretAdminMonitorTest extends HudsonTestCase { ...@@ -76,8 +82,8 @@ public class RekeySecretAdminMonitorTest extends HudsonTestCase {
private void verifyRewrite(File dir) throws Exception { private void verifyRewrite(File dir) throws Exception {
File xml = new File(dir, "foo.xml"); File xml = new File(dir, "foo.xml");
assertEquals("<foo>" + encryptNew(TEST_KEY) + "</foo>".trim(), Pattern pattern = Pattern.compile("<foo>"+plain_regex_match+"</foo>");
FileUtils.readFileToString(xml).trim()); assertTrue(pattern.matcher(FileUtils.readFileToString(xml).trim()).matches());
} }
// TODO sometimes fails: "Invalid request submission: {json=[Ljava.lang.String;@2c46358e, .crumb=[Ljava.lang.String;@35661457}" // TODO sometimes fails: "Invalid request submission: {json=[Ljava.lang.String;@2c46358e, .crumb=[Ljava.lang.String;@35661457}"
......
package jenkins.security;
import com.gargoylesoftware.htmlunit.Page;
import hudson.model.UnprotectedRootAction;
import hudson.security.ACL;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.util.HttpResponses;
import jenkins.model.Jenkins;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.HttpResponse;
public class Security380Test {
@Rule
public JenkinsRule j = new JenkinsRule();
@Issue("SECURITY-380")
@Test
public void testGetItemsWithoutAnonRead() throws Exception {
FullControlOnceLoggedInAuthorizationStrategy strategy = new FullControlOnceLoggedInAuthorizationStrategy();
strategy.setAllowAnonymousRead(false);
Jenkins.getInstance().setAuthorizationStrategy(strategy);
Jenkins.getInstance().setSecurityRealm(j.createDummySecurityRealm());
j.createFreeStyleProject();
ACL.impersonate(Jenkins.ANONYMOUS, new Runnable() {
@Override
public void run() {
Assert.assertEquals("no items", 0, Jenkins.getInstance().getItems().size());
}
});
}
@Issue("SECURITY-380")
@Test
public void testGetItems() throws Exception {
FullControlOnceLoggedInAuthorizationStrategy strategy = new FullControlOnceLoggedInAuthorizationStrategy();
strategy.setAllowAnonymousRead(true);
Jenkins.getInstance().setAuthorizationStrategy(strategy);
Jenkins.getInstance().setSecurityRealm(j.createDummySecurityRealm());
j.createFreeStyleProject();
ACL.impersonate(Jenkins.ANONYMOUS, new Runnable() {
@Override
public void run() {
Assert.assertEquals("one item", 1, Jenkins.getInstance().getItems().size());
}
});
}
@Issue("SECURITY-380")
@Test
public void testWithUnprotectedRootAction() throws Exception {
FullControlOnceLoggedInAuthorizationStrategy strategy = new FullControlOnceLoggedInAuthorizationStrategy();
strategy.setAllowAnonymousRead(false);
Jenkins.getInstance().setAuthorizationStrategy(strategy);
Jenkins.getInstance().setSecurityRealm(j.createDummySecurityRealm());
j.createFreeStyleProject();
JenkinsRule.WebClient wc = j.createWebClient();
Page page = wc.goTo("listJobs", "text/plain");
Assert.assertEquals("expect 0 items", "0", page.getWebResponse().getContentAsString().trim());
}
@TestExtension
public static class JobListingUnprotectedRootAction implements UnprotectedRootAction {
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getUrlName() {
return "listJobs";
}
public HttpResponse doIndex() throws Exception {
return HttpResponses.plainText(Integer.toString(Jenkins.getInstance().getItems().size()));
}
}
}
/*
* The MIT License
*
* Copyright 2016 CloudBees, Inc.
*
* 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 jenkins.security.s2m;
import java.io.File;
import javax.inject.Inject;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
public class AdminFilePathFilterTest {
@Rule
public JenkinsRule r = new JenkinsRule();
@Inject
AdminWhitelistRule rule;
@Before
public void setUp() {
r.jenkins.getInjector().injectMembers(this);
rule.setMasterKillSwitch(false);
}
@Issue({"JENKINS-27055", "SECURITY-358"})
@Test
public void matchBuildDir() throws Exception {
File buildDir = r.buildAndAssertSuccess(r.createFreeStyleProject()).getRootDir();
assertTrue(rule.checkFileAccess("write", new File(buildDir, "whatever")));
assertFalse(rule.checkFileAccess("write", new File(buildDir, "build.xml")));
// WorkflowRun:
assertFalse(rule.checkFileAccess("write", new File(buildDir, "program.dat")));
assertFalse(rule.checkFileAccess("write", new File(buildDir, "workflow/23.xml")));
}
}
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package jenkins.security; package jenkins.security.s2m;
import hudson.FilePath; import hudson.FilePath;
import hudson.model.Slave; import hudson.model.Slave;
...@@ -31,8 +31,6 @@ import java.io.File; ...@@ -31,8 +31,6 @@ import java.io.File;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import jenkins.security.s2m.AdminWhitelistRule;
import jenkins.security.s2m.DefaultFilePathFilter;
import org.jenkinsci.remoting.RoleChecker; import org.jenkinsci.remoting.RoleChecker;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -41,7 +39,6 @@ import org.junit.Rule; ...@@ -41,7 +39,6 @@ import org.junit.Rule;
import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule;
import javax.inject.Inject; import javax.inject.Inject;
import org.jvnet.hudson.test.Issue;
public class DefaultFilePathFilterTest { public class DefaultFilePathFilterTest {
...@@ -112,11 +109,4 @@ public class DefaultFilePathFilterTest { ...@@ -112,11 +109,4 @@ public class DefaultFilePathFilterTest {
} }
} }
@Issue("JENKINS-27055")
@Test public void matchBuildDir() throws Exception {
File f = new File(r.buildAndAssertSuccess(r.createFreeStyleProject()).getRootDir(), "whatever");
rule.setMasterKillSwitch(false);
assertTrue(rule.checkFileAccess("write", f));
}
} }
...@@ -43,10 +43,13 @@ import java.io.PrintStream; ...@@ -43,10 +43,13 @@ import java.io.PrintStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Pattern;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import org.acegisecurity.Authentication; import org.acegisecurity.Authentication;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.Issue;
...@@ -74,10 +77,18 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password ...@@ -74,10 +77,18 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
@Extension @Extension
public static final class DescriptorImpl extends Descriptor<PasswordTest> {} public static final class DescriptorImpl extends Descriptor<PasswordTest> {}
@Issue("SECURITY-266") @Issue({"SECURITY-266", "SECURITY-304"})
public void testExposedCiphertext() throws Exception { public void testExposedCiphertext() throws Exception {
boolean saveEnabled = Item.EXTENDED_READ.getEnabled(); boolean saveEnabled = Item.EXTENDED_READ.getEnabled();
try { try {
//final String plain_regex_match = ".*\\{[A-Za-z0-9+/]+={0,2}}.*";
final String xml_regex_match = "\\{[A-Za-z0-9+/]+={0,2}}";
final Pattern xml_regex_pattern = Pattern.compile(xml_regex_match);
final String staticTest = "\n\nvalue=\"{AQAAABAAAAAgXhXgopokysZkduhl+v1gm0UhUBBbjKDVpKz7bGk3mIO53cNTRdlu7LC4jZYEc+vF}\"\n";
//Just a quick verification on what could be on the page and that the regexp is correctly set up
assertThat(xml_regex_pattern.matcher(staticTest).find(), is(true));
jenkins.setSecurityRealm(createDummySecurityRealm()); jenkins.setSecurityRealm(createDummySecurityRealm());
// TODO 1.645+ use MockAuthorizationStrategy // TODO 1.645+ use MockAuthorizationStrategy
GlobalMatrixAuthorizationStrategy pmas = new GlobalMatrixAuthorizationStrategy(); GlobalMatrixAuthorizationStrategy pmas = new GlobalMatrixAuthorizationStrategy();
...@@ -89,7 +100,7 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password ...@@ -89,7 +100,7 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
pmas.add(Item.CREATE, "dev"); // so we can show CopyJobCommand would barf; more realistic would be to grant it only in a subfolder pmas.add(Item.CREATE, "dev"); // so we can show CopyJobCommand would barf; more realistic would be to grant it only in a subfolder
jenkins.setAuthorizationStrategy(pmas); jenkins.setAuthorizationStrategy(pmas);
Secret s = Secret.fromString("s3cr3t"); Secret s = Secret.fromString("s3cr3t");
String sEnc = s.getEncryptedValue(); //String sEnc = s.getEncryptedValue();
FreeStyleProject p = createFreeStyleProject("p"); FreeStyleProject p = createFreeStyleProject("p");
p.setDisplayName("Unicode here ←"); p.setDisplayName("Unicode here ←");
p.setDescription("This+looks+like+Base64+but+is+not+a+secret"); p.setDescription("This+looks+like+Base64+but+is+not+a+secret");
...@@ -98,14 +109,15 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password ...@@ -98,14 +109,15 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
// Control case: an administrator can read and write configuration freely. // Control case: an administrator can read and write configuration freely.
wc.login("admin"); wc.login("admin");
HtmlPage configure = wc.getPage(p, "configure"); HtmlPage configure = wc.getPage(p, "configure");
assertThat(configure.getWebResponse().getContentAsString(), containsString(sEnc)); assertThat(xml_regex_pattern.matcher(configure.getWebResponse().getContentAsString()).find(), is(true));
submit(configure.getFormByName("config")); submit(configure.getFormByName("config"));
VulnerableProperty vp = p.getProperty(VulnerableProperty.class); VulnerableProperty vp = p.getProperty(VulnerableProperty.class);
assertNotNull(vp); assertNotNull(vp);
assertEquals(s, vp.secret); assertEquals(s, vp.secret);
Page configXml = wc.goTo(p.getUrl() + "config.xml", "application/xml"); Page configXml = wc.goTo(p.getUrl() + "config.xml", "application/xml");
String xmlAdmin = configXml.getWebResponse().getContentAsString(); String xmlAdmin = configXml.getWebResponse().getContentAsString();
assertThat(xmlAdmin, containsString("<secret>" + sEnc + "</secret>"));
assertThat(Pattern.compile("<secret>" + xml_regex_match + "</secret>").matcher(xmlAdmin).find(), is(true));
assertThat(xmlAdmin, containsString("<displayName>" + p.getDisplayName() + "</displayName>")); assertThat(xmlAdmin, containsString("<displayName>" + p.getDisplayName() + "</displayName>"));
assertThat(xmlAdmin, containsString("<description>" + p.getDescription() + "</description>")); assertThat(xmlAdmin, containsString("<description>" + p.getDescription() + "</description>"));
// CLICommandInvoker does not work here, as it sets up its own SecurityRealm + AuthorizationStrategy. // CLICommandInvoker does not work here, as it sets up its own SecurityRealm + AuthorizationStrategy.
...@@ -127,11 +139,11 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password ...@@ -127,11 +139,11 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
// Test case: another user with EXTENDED_READ but not CONFIGURE should not get access even to encrypted secrets. // Test case: another user with EXTENDED_READ but not CONFIGURE should not get access even to encrypted secrets.
wc.login("dev"); wc.login("dev");
configure = wc.getPage(p, "configure"); configure = wc.getPage(p, "configure");
assertThat(configure.getWebResponse().getContentAsString(), not(containsString(sEnc))); assertThat(xml_regex_pattern.matcher(configure.getWebResponse().getContentAsString()).find(), is(false));
configXml = wc.goTo(p.getUrl() + "config.xml", "application/xml"); configXml = wc.goTo(p.getUrl() + "config.xml", "application/xml");
String xmlDev = configXml.getWebResponse().getContentAsString(); String xmlDev = configXml.getWebResponse().getContentAsString();
assertThat(xmlDev, not(containsString(sEnc))); assertThat(xml_regex_pattern.matcher(xmlDev).find(), is(false));
assertEquals(xmlAdmin.replace(sEnc, "********"), xmlDev); assertEquals(xmlAdmin.replaceAll(xml_regex_match, "********"), xmlDev);
getJobCommand = new GetJobCommand(); getJobCommand = new GetJobCommand();
Authentication devAuth = User.get("dev").impersonate(); Authentication devAuth = User.get("dev").impersonate();
getJobCommand.setTransportAuth(devAuth); getJobCommand.setTransportAuth(devAuth);
......
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
<disabledAdministrativeMonitors/>
<version>1.0</version>
<numExecutors>2</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
<securityRealm class="hudson.security.SecurityRealm$None"/>
<disableRememberMe>false</disableRememberMe>
<projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
<workspaceDir>${JENKINS_HOME}/workspace/${ITEM_FULLNAME}</workspaceDir>
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
<jdks/>
<viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
<myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
<clouds/>
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
<views>
<hudson.model.AllView>
<owner class="hudson" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
<primaryView>All</primaryView>
<slaveAgentPort>0</slaveAgentPort>
<label></label>
<nodeProperties/>
<globalNodeProperties/>
<noUsageStatistics>true</noUsageStatistics>
</hudson>
<?xml version='1.0' encoding='UTF-8'?>
<slave>
<temporaryOfflineCause class="hudson.slaves.OfflineCause$UserCause">
<timestamp>1479196265920</timestamp>
<description>
<holder>
<owner>hudson.slaves.Messages</owner>
</holder>
<key>SlaveComputer.DisconnectedBy</key>
<args>
<string>username</string>
<string> : msg</string>
</args>
</description>
<user>
<fullName>username</fullName>
<properties>
<jenkins.security.ApiTokenProperty>
<apiToken>wPfVKd4HGJzRoEpazbTu35nXXfI34cguPjm+5JPO7pZDFLFgpFLviQsS3NdJndax</apiToken>
</jenkins.security.ApiTokenProperty>
<hudson.tasks.Mailer_-UserProperty/>
<hudson.model.MyViewsProperty>
<views>
<hudson.model.AllView>
<owner class="hudson.model.MyViewsProperty" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
</hudson.model.MyViewsProperty>
<hudson.model.PaneStatusProperties>
<collapsed/>
</hudson.model.PaneStatusProperties>
<hudson.search.UserSearchProperty>
<insensitiveSearch>false</insensitiveSearch>
</hudson.search.UserSearchProperty>
</properties>
</user>
</temporaryOfflineCause>
<name>deserialized</name>
<description>dummy</description>
<remoteFS>...</remoteFS>
<numExecutors>1</numExecutors>
<mode>NORMAL</mode>
<retentionStrategy class="hudson.slaves.RetentionStrategy$2">
<DESCRIPTOR>
<outer-class reference="../.."/>
</DESCRIPTOR>
</retentionStrategy>
<launcher/>
<label></label>
<nodeProperties/>
<userId>SYSTEM</userId>
</slave>
<?xml version='1.0' encoding='UTF-8'?>
<user>
<fullName>username</fullName>
<id>username</id>
<description></description>
<properties>
<jenkins.security.ApiTokenProperty>
<apiToken>qykj8q6EqvMg9LPu+lCqLiXBZvEVdCTWoYJwmicXgH+yh1ZUm85iHe29grd+g3QG</apiToken>
</jenkins.security.ApiTokenProperty>
<com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty plugin="credentials@1.18">
<domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash"/>
</com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty>
<hudson.tasks.Mailer_-UserProperty>
<emailAddress></emailAddress>
</hudson.tasks.Mailer_-UserProperty>
<hudson.model.MyViewsProperty>
<primaryViewName></primaryViewName>
<views>
<hudson.model.AllView>
<owner class="hudson.model.MyViewsProperty" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
</hudson.model.MyViewsProperty>
<hudson.model.PaneStatusProperties>
<collapsed/>
</hudson.model.PaneStatusProperties>
<org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
<authorizedKeys></authorizedKeys>
</org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
<hudson.search.UserSearchProperty>
<insensitiveSearch>false</insensitiveSearch>
</hudson.search.UserSearchProperty>
</properties>
</user>
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
<disabledAdministrativeMonitors/>
<version>1.625.4-SNAPSHOT (private-12/16/2016 18:04 GMT-rsandell)</version>
<numExecutors>2</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
<securityRealm class="hudson.security.SecurityRealm$None"/>
<disableRememberMe>false</disableRememberMe>
<projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
<workspaceDir>${ITEM_ROOTDIR}/workspace</workspaceDir>
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
<jdks/>
<viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
<myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
<clouds/>
<quietPeriod>5</quietPeriod>
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
<views>
<hudson.model.AllView>
<owner class="hudson" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
<primaryView>All</primaryView>
<slaveAgentPort>0</slaveAgentPort>
<label></label>
<nodeProperties/>
<globalNodeProperties/>
</hudson>
\ No newline at end of file
<?xml version='1.0' encoding='UTF-8'?>
<project>
<actions/>
<description></description>
<keepDependencies>false</keepDependencies>
<properties>
<hudson.model.ParametersDefinitionProperty>
<parameterDefinitions>
<hudson.model.PasswordParameterDefinition>
<name>alice</name>
<description>theSecret</description>
<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>
</hudson.model.PasswordParameterDefinition>
</parameterDefinitions>
</hudson.model.ParametersDefinitionProperty>
</properties>
<scm class="hudson.scm.NullSCM"/>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers/>
<concurrentBuild>false</concurrentBuild>
<builders/>
<publishers/>
<buildWrappers/>
</project>
\ No newline at end of file
4311ab5e95e3da7b9b1360b52cac6f0f666db7b48f9f701296cc07c8f00612b451f1874e584d49560810619e8a6ff6b19f8f58ae1305c515fc62a7b60ea3a69e6058cad16b2c8df317952b749fdaaecab013431da55bb4ea4b8eee754fa043261b51a99a2b537fd57f867cdcb1e209f3bba735a8672dbfc3f10b0e2209a81683
\ No newline at end of file
<set>
<hudson.util.RunList>
<base class="java.util.ServiceLoader">
<providers/>
<lookupIterator>
<configs class="java.util.Collections$3">
<i class="java.util.AbstractList$Itr">
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class="java.util.Arrays$ArrayList">
<a class="string-array">
<string>foo</string>
</a>
</outer-class>
</i>
<val_-c class="java.util.Arrays$ArrayList" reference="../i/outer-class"/>
</configs>
<pending class="javax.imageio.spi.FilterIterator">
<iter class="java.util.AbstractList$Itr">
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class="java.util.Arrays$ArrayList">
<a>
<java.lang.ProcessBuilder>
<command>
<string>touch</string>
<string>@TOKEN@</string>
</command>
<redirectErrorStream>false</redirectErrorStream>
</java.lang.ProcessBuilder>
</a>
</outer-class>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="java.util.HashMap$Node">
<hash>101575</hash>
<key class="string">foo</key>
<value class="string">foo</value>
</next>
</pending>
<outer-class reference="../.."/>
</lookupIterator>
</base>
</hudson.util.RunList>
</set>
\ No newline at end of file
...@@ -28,7 +28,7 @@ THE SOFTWARE. ...@@ -28,7 +28,7 @@ THE SOFTWARE.
<parent> <parent>
<groupId>org.jenkins-ci.main</groupId> <groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId> <artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version> <version>2.45-SNAPSHOT</version>
</parent> </parent>
<artifactId>jenkins-war</artifactId> <artifactId>jenkins-war</artifactId>
......
...@@ -2355,6 +2355,7 @@ function createSearchBox(searchURL) { ...@@ -2355,6 +2355,7 @@ function createSearchBox(searchURL) {
var ac = new YAHOO.widget.AutoComplete("search-box","search-box-completion",ds); var ac = new YAHOO.widget.AutoComplete("search-box","search-box-completion",ds);
ac.typeAhead = false; ac.typeAhead = false;
ac.autoHighlight = false; ac.autoHighlight = false;
ac.formatResult = ac.formatEscapedResult;
var box = $("search-box"); var box = $("search-box");
var sizer = $("search-box-sizer"); var sizer = $("search-box-sizer");
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册