diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java index 115a01e63597ffbd3b9236f39732e71b386bf2c1..9d8f3ec1f53cbaac5496b8201b4da473747def2a 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -43,6 +43,7 @@ import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.AtomicFileWriter; import hudson.util.IOUtils; import jenkins.model.Jenkins; +import org.acegisecurity.Authentication; import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; import org.kohsuke.stapler.WebMethod; @@ -210,7 +211,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet * Not all the Items need to support this operation, but if you decide to do so, * you can use this method. */ - protected void renameTo(String newName) throws IOException { + protected void renameTo(final String newName) throws IOException { // always synchronize from bigger objects first final ItemGroup parent = getParent(); synchronized (parent) { @@ -223,13 +224,28 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet if (this.name.equals(newName)) return; - Item existing = parent.getItem(newName); - if (existing != null && existing!=this) - // 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"); + // the test to see if the project already exists or not needs to be done in escalated privilege + // to avoid overwriting + ACL.impersonate(ACL.SYSTEM,new Callable() { + final Authentication user = Jenkins.getAuthentication(); + @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; + } + }); + String oldName = this.name; String oldFullName = getFullName(); @@ -634,7 +650,13 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet } // try to reflect the changes by reloading - new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(this); + Object o = new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(this); + if (o!=this) { + // ensure that we've got the same job type. extending this code to support updating + // to different job type requires destroying & creating a new job type + throw new IOException("Expecting "+this.getClass()+" but got "+o.getClass()+" instead"); + } + Items.whileUpdatingByXml(new Callable() { @Override public Void call() throws IOException { onLoad(getParent(), getRootDir().getName()); diff --git a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java index 8106dbe1b873e4be09712158dd17a35334361d5a..6372b60ed21b917ec65346f035e188f8f5078e87 100644 --- a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java +++ b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java @@ -28,6 +28,8 @@ import hudson.remoting.PingThread; import hudson.remoting.Channel.Mode; import hudson.util.ChunkedOutputStream; import hudson.util.ChunkedInputStream; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -151,10 +153,12 @@ abstract public class FullDuplexHttpChannel { /** * Set to true if the servlet container doesn't support chunked encoding. */ + @Restricted(NoExternalUse.class) public static boolean DIY_CHUNKING = Boolean.getBoolean("hudson.diyChunking"); /** * Controls the time out of waiting for the 2nd HTTP request to arrive. */ - private static long CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(15); + @Restricted(NoExternalUse.class) + public static long CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(15); } diff --git a/core/src/main/java/hudson/security/ACL.java b/core/src/main/java/hudson/security/ACL.java index d13b53ec969f7ede1a2583b3c62d7916929203fd..cc4739bd7b470a40ef2280ce91f77f12199995d6 100644 --- a/core/src/main/java/hudson/security/ACL.java +++ b/core/src/main/java/hudson/security/ACL.java @@ -24,6 +24,7 @@ package hudson.security; import javax.annotation.Nonnull; +import hudson.remoting.Callable; import jenkins.security.NonSerializableSecurityContext; import jenkins.model.Jenkins; import org.acegisecurity.AccessDeniedException; @@ -146,4 +147,19 @@ public abstract class ACL { } } + /** + * Safer variant of {@link #impersonate(Authentication)} that does not require a finally-block. + * @param auth authentication, such as {@link #SYSTEM} + * @param body an action to run with this alternate authentication in effect + * @since TODO + */ + public static V impersonate(Authentication auth, Callable body) throws T { + SecurityContext old = impersonate(auth); + try { + return body.call(); + } finally { + SecurityContextHolder.setContext(old); + } + } + } diff --git a/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java index 6228c9b251010470c8eeee731d492b95ffd200f5..6836467217f28345b9d286caa312199d10d5a5a8 100644 --- a/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java +++ b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java @@ -14,7 +14,6 @@ import javax.mail.internet.InternetAddress; import javax.servlet.ServletContext; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.logging.Level; import java.util.logging.Logger; @@ -108,9 +107,14 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { */ private void updateSecureSessionFlag() { try { - boolean v = fixNull(jenkinsUrl).startsWith("https"); ServletContext context = Jenkins.getInstance().servletContext; - Method m = context.getClass().getMethod("getSessionCookieConfig"); + Method m; + try { + m = context.getClass().getMethod("getSessionCookieConfig"); + } catch (NoSuchMethodException x) { // 3.0+ + LOGGER.log(Level.FINE, "Failed to set secure cookie flag", x); + return; + } Object sessionCookieConfig = m.invoke(context); // not exposing session cookie to JavaScript to mitigate damage caused by XSS @@ -119,9 +123,10 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { setHttpOnly.invoke(sessionCookieConfig,true); Method setSecure = scc.getMethod("setSecure",boolean.class); + boolean v = fixNull(jenkinsUrl).startsWith("https"); setSecure.invoke(sessionCookieConfig,v); } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to set secure cookie flag. Maybe running on Servlet 2.5 and younger?", e); + LOGGER.log(Level.WARNING, "Failed to set secure cookie flag", e); } } diff --git a/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy b/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy index ea4b0e064960ddd6149083bbea3445687e9e00e1..56e8696754fd423a71a9f5d8c590822a8ba5aeb8 100644 --- a/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy +++ b/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy @@ -33,6 +33,10 @@ import hudson.tasks.BuildStepMonitor; import hudson.tasks.BuildTrigger import hudson.tasks.Publisher import hudson.tasks.Recorder; +import com.gargoylesoftware.htmlunit.html.HtmlPage +import hudson.maven.MavenModuleSet; +import hudson.security.*; +import hudson.tasks.BuildTrigger; import hudson.tasks.Shell; import hudson.scm.NullSCM; import hudson.scm.SCM @@ -49,7 +53,7 @@ import hudson.util.StreamTaskListener; import hudson.util.OneShotEvent import jenkins.model.Jenkins; import org.acegisecurity.context.SecurityContext; -import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.context.SecurityContextHolder import org.jvnet.hudson.test.HudsonTestCase import org.jvnet.hudson.test.Bug; import org.jvnet.hudson.test.MemoryAssert @@ -524,4 +528,70 @@ public class AbstractProjectTest extends HudsonTestCase { 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. + */ + public void testConfigDotXmlSubmissionToDifferentType() { + jenkins.crumbIssuer = null + def p = createFreeStyleProject() + + HttpURLConnection con = postConfigDotXml(p, "") + + // this should fail with a type mismatch error + // the error message should report both what was submitted and what was expected + assert con.responseCode == 500 + def msg = con.errorStream.text + println msg + assert msg.contains(FreeStyleProject.class.name) + assert msg.contains(MavenModuleSet.class.name) + + // control. this should work + con = postConfigDotXml(p, "") + assert con.responseCode == 200 + } + + private HttpURLConnection postConfigDotXml(FreeStyleProject p, String xml) { + HttpURLConnection con = new URL(getURL(), "job/${p.name}/config.xml").openConnection() + con.requestMethod = "POST" + con.setRequestProperty("Content-Type", "application/xml") + con.doOutput = true + con.outputStream.withStream { s -> + s.write(xml.bytes) + } + return con + } }