diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java
index daf88e9000..4899606ae4 100644
--- a/core/src/main/java/hudson/Functions.java
+++ b/core/src/main/java/hudson/Functions.java
@@ -510,6 +510,15 @@ public class Functions {
return c.getValue();
}
+ private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+");
+ @Restricted(NoExternalUse.class)
+ public static String validateIconSize(String iconSize) throws SecurityException {
+ if (!ICON_SIZE.matcher(iconSize).matches()) {
+ throw new SecurityException("invalid iconSize");
+ }
+ return iconSize;
+ }
+
/**
* Gets the suffix to use for YUI JavaScript.
*/
@@ -1678,6 +1687,9 @@ public class Functions {
public String getPasswordValue(Object o) {
if (o==null) return null;
if (o instanceof Secret) return ((Secret)o).getEncryptedValue();
+ if (getIsUnitTest()) {
+ throw new SecurityException("attempted to render plaintext ‘" + o + "’ in password field; use a getter of type Secret instead");
+ }
return o.toString();
}
diff --git a/core/src/main/java/hudson/cli/CreateJobCommand.java b/core/src/main/java/hudson/cli/CreateJobCommand.java
index db73c30b51..02dd6ae55e 100644
--- a/core/src/main/java/hudson/cli/CreateJobCommand.java
+++ b/core/src/main/java/hudson/cli/CreateJobCommand.java
@@ -72,6 +72,7 @@ public class CreateJobCommand extends CLICommand {
name = name.substring(i + 1);
}
+ Jenkins.checkGoodName(name);
ig.createProjectFromXML(name, stdin);
return 0;
}
diff --git a/core/src/main/java/hudson/markup/MyspacePolicy.java b/core/src/main/java/hudson/markup/MyspacePolicy.java
index 46d0c31e22..507acd12a5 100644
--- a/core/src/main/java/hudson/markup/MyspacePolicy.java
+++ b/core/src/main/java/hudson/markup/MyspacePolicy.java
@@ -48,7 +48,7 @@ public class MyspacePolicy {
"script","noscript",/*"iframe",*/"frameset","frame");
tag("label", "for");
- tag("form", "action",ONSITE_OR_OFFSITE_URL,
+ tag("form", "action",ONSITE_URL,
"method");
tag("button", "value", "type");
tag("input", "maxlength","checked",
@@ -66,7 +66,6 @@ public class MyspacePolicy {
tag("span,div");
tag("img", "src",ONSITE_OR_OFFSITE_URL,
"hspace","vspace");
- tag("iframe", "src");
tag("ul,ol,li,dd,dl,dt,thead,tbody,tfoot");
tag("table", "noresize");
tag("td,th,tr");
diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java
index 95e9668afa..a0f7130cb4 100644
--- a/core/src/main/java/hudson/model/AbstractProject.java
+++ b/core/src/main/java/hudson/model/AbstractProject.java
@@ -2023,7 +2023,14 @@ public abstract class AbstractProject
,R extends A
for (Publisher _t : Descriptor.newInstancesFromHeteroList(req, json, "publisher", Jenkins.getInstance().getExtensionList(BuildTrigger.DescriptorImpl.class))) {
BuildTrigger t = (BuildTrigger) _t;
- for (AbstractProject downstream : t.getChildProjects(this)) {
+ List childProjects;
+ SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
+ try {
+ childProjects = t.getChildProjects(this);
+ } finally {
+ SecurityContextHolder.setContext(orig);
+ }
+ for (AbstractProject downstream : childProjects) {
downstream.checkPermission(BUILD);
}
}
diff --git a/core/src/main/java/hudson/model/Cause.java b/core/src/main/java/hudson/model/Cause.java
index 8283cfa68c..44861804da 100644
--- a/core/src/main/java/hudson/model/Cause.java
+++ b/core/src/main/java/hudson/model/Cause.java
@@ -34,6 +34,7 @@ import jenkins.model.Jenkins;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.CheckForNull;
@@ -412,10 +413,13 @@ public abstract class Cause {
@Override
public String getShortDescription() {
if(note != null) {
- return Messages.Cause_RemoteCause_ShortDescriptionWithNote(addr, note);
- } else {
- return Messages.Cause_RemoteCause_ShortDescription(addr);
+ try {
+ return Messages.Cause_RemoteCause_ShortDescriptionWithNote(addr, Jenkins.getInstance().getMarkupFormatter().translate(note));
+ } catch (IOException x) {
+ // ignore
+ }
}
+ return Messages.Cause_RemoteCause_ShortDescription(addr);
}
@Override
diff --git a/core/src/main/java/hudson/model/PasswordParameterDefinition.java b/core/src/main/java/hudson/model/PasswordParameterDefinition.java
index 6c3388cd5e..ce93144a56 100644
--- a/core/src/main/java/hudson/model/PasswordParameterDefinition.java
+++ b/core/src/main/java/hudson/model/PasswordParameterDefinition.java
@@ -28,6 +28,8 @@ import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.DataBoundConstructor;
import hudson.Extension;
import hudson.util.Secret;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.DoNotUse;
/**
* Parameter whose value is a {@link Secret} and is hidden from the UI.
@@ -76,6 +78,11 @@ public class PasswordParameterDefinition extends SimpleParameterDefinition {
return Secret.toString(defaultValue);
}
+ @Restricted(DoNotUse.class) // used from Jelly
+ public Secret getDefaultValueAsSecret() {
+ return defaultValue;
+ }
+
// kept for backward compatibility
public void setDefaultValue(String defaultValue) {
this.defaultValue = Secret.fromString(defaultValue);
diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java
index 860b9b77b9..676ccf313a 100644
--- a/core/src/main/java/hudson/model/View.java
+++ b/core/src/main/java/hudson/model/View.java
@@ -783,7 +783,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
{
StaplerRequest req = Stapler.getCurrentRequest();
- iconSize = req != null ? Functions.getCookie(req, "iconSize", "32x32") : "32x32";
+ iconSize = req != null ? Functions.validateIconSize(Functions.getCookie(req, "iconSize", "32x32")) : "32x32";
}
@Override protected void compute() throws Exception {
diff --git a/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java b/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
index 1efffba17e..de1a0cb3ce 100644
--- a/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
+++ b/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
@@ -86,6 +86,7 @@ public class AuthenticationProcessingFilter2 extends AuthenticationProcessingFil
// HttpSessionContextIntegrationFilter stores the updated SecurityContext object into this session later
// (either when a redirect is issued, via its HttpResponseWrapper, or when the execution returns to its
// doFilter method.
+ request.getSession().invalidate();
request.getSession();
SecurityListener.fireLoggedIn(authResult.getName());
}
diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
index 59507afd5d..f132fc7c5e 100644
--- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
+++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
@@ -76,6 +76,8 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -171,8 +173,15 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
@Override
protected Details authenticate(String username, String password) throws AuthenticationException {
Details u = loadUserByUsername(username);
- if (!u.isPasswordCorrect(password))
- throw new BadCredentialsException("Failed to login as "+username);
+ if (!u.isPasswordCorrect(password)) {
+ String message;
+ try {
+ message = ResourceBundle.getBundle("org.acegisecurity.messages").getString("AbstractUserDetailsAuthenticationProvider.badCredentials");
+ } catch (MissingResourceException x) {
+ message = "Bad credentials";
+ }
+ throw new BadCredentialsException(message);
+ }
return u;
}
diff --git a/core/src/main/java/hudson/util/RemotingDiagnostics.java b/core/src/main/java/hudson/util/RemotingDiagnostics.java
index 9c7497e6d1..ac71de50e8 100644
--- a/core/src/main/java/hudson/util/RemotingDiagnostics.java
+++ b/core/src/main/java/hudson/util/RemotingDiagnostics.java
@@ -191,7 +191,7 @@ public final class RemotingDiagnostics {
@WebMethod(name="heapdump.hprof")
public void doHeapDump(StaplerRequest req, StaplerResponse rsp) throws IOException, InterruptedException {
- owner.checkPermission(Jenkins.ADMINISTER);
+ owner.checkPermission(Jenkins.RUN_SCRIPTS);
rsp.setContentType("application/octet-stream");
FilePath dump = obtain();
diff --git a/core/src/main/java/hudson/util/RobustReflectionConverter.java b/core/src/main/java/hudson/util/RobustReflectionConverter.java
index 0be434f1de..18909ba74f 100644
--- a/core/src/main/java/hudson/util/RobustReflectionConverter.java
+++ b/core/src/main/java/hudson/util/RobustReflectionConverter.java
@@ -44,6 +44,7 @@ import hudson.model.Saveable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -72,6 +73,8 @@ public class RobustReflectionConverter implements Converter {
protected transient SerializationMethodInvoker serializationMethodInvoker;
private transient ReflectionProvider pureJavaReflectionProvider;
private final @Nonnull XStream2.ClassOwnership classOwnership;
+ /** {@code pkg.Clazz#fieldName} */
+ private final Set criticalFields = Collections.synchronizedSet(new HashSet());
public RobustReflectionConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
this(mapper, reflectionProvider, new XStream2().new PluginClassOwnership());
@@ -84,6 +87,10 @@ public class RobustReflectionConverter implements Converter {
serializationMethodInvoker = new SerializationMethodInvoker();
}
+ void addCriticalField(Class> clazz, String field) {
+ criticalFields.add(clazz.getName() + '#' + field);
+ }
+
public boolean canConvert(Class type) {
return true;
}
@@ -260,8 +267,16 @@ public class RobustReflectionConverter implements Converter {
while (reader.hasMoreChildren()) {
reader.moveDown();
+ boolean critical = false;
try {
String fieldName = mapper.realMember(result.getClass(), reader.getNodeName());
+ for (Class> concrete = result.getClass(); concrete != null; concrete = concrete.getSuperclass()) {
+ // Not quite right since a subclass could shadow a field, but probably suffices:
+ if (criticalFields.contains(concrete.getName() + '#' + fieldName)) {
+ critical = true;
+ break;
+ }
+ }
boolean implicitCollectionHasSameName = mapper.getImplicitCollectionDefForFieldName(result.getClass(), reader.getNodeName()) != null;
Class classDefiningField = determineWhichClassDefinesField(reader);
@@ -293,8 +308,14 @@ public class RobustReflectionConverter implements Converter {
}
}
} catch (XStreamException e) {
+ if (critical) {
+ throw e;
+ }
addErrorInContext(context, e);
} catch (LinkageError e) {
+ if (critical) {
+ throw e;
+ }
addErrorInContext(context, e);
}
diff --git a/core/src/main/java/hudson/util/XStream2.java b/core/src/main/java/hudson/util/XStream2.java
index e6d8775139..bf0a67c853 100644
--- a/core/src/main/java/hudson/util/XStream2.java
+++ b/core/src/main/java/hudson/util/XStream2.java
@@ -65,6 +65,8 @@ import java.nio.charset.Charset;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.CheckForNull;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* {@link XStream} enhanced for additional Java5 support and improved robustness.
@@ -121,6 +123,16 @@ public class XStream2 extends XStream {
return reflectionConverter;
}
+ /**
+ * Specifies that a given field of a given class should not be treated with laxity by {@link RobustCollectionConverter}.
+ * @param clazz a class which we expect to hold a non-{@code transient} field
+ * @param field a field name in that class
+ */
+ @Restricted(NoExternalUse.class) // TODO could be opened up later
+ public void addCriticalField(Class> clazz, String field) {
+ reflectionConverter.addCriticalField(clazz, field);
+ }
+
static String trimVersion(String version) {
// TODO seems like there should be some trick with VersionNumber to do this
return version.replaceFirst(" .+$", "");
diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java
index 8d21184d8d..dc87020a82 100644
--- a/core/src/main/java/jenkins/model/Jenkins.java
+++ b/core/src/main/java/jenkins/model/Jenkins.java
@@ -294,7 +294,6 @@ import java.util.logging.Level;
import static java.util.logging.Level.SEVERE;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
-import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -3473,9 +3472,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
*/
public void doIconSize( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
String qs = req.getQueryString();
- if(qs==null || !ICON_SIZE.matcher(qs).matches())
+ if(qs==null)
throw new ServletException();
- Cookie cookie = new Cookie("iconSize", qs);
+ Cookie cookie = new Cookie("iconSize", Functions.validateIconSize(qs));
cookie.setMaxAge(/* ~4 mo. */9999999); // #762
rsp.addCookie(cookie);
String ref = req.getHeader("Referer");
@@ -4077,8 +4076,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
private static final Logger LOGGER = Logger.getLogger(Jenkins.class.getName());
- private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+");
-
public static final PermissionGroup PERMISSIONS = Permission.HUDSON_PERMISSIONS;
public static final Permission ADMINISTER = Permission.HUDSON_ADMINISTER;
public static final Permission READ = new Permission(PERMISSIONS,"Read",Messages._Hudson_ReadPermission_Description(),Permission.READ,PermissionScope.JENKINS);
@@ -4101,6 +4098,8 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
// for backward compatibility with <1.75, recognize the tag name "view" as well.
XSTREAM.alias("view", ListView.class);
XSTREAM.alias("listView", ListView.class);
+ XSTREAM2.addCriticalField(Jenkins.class, "securityRealm");
+ XSTREAM2.addCriticalField(Jenkins.class, "authorizationStrategy");
// this seems to be necessary to force registration of converter early enough
Mode.class.getEnumConstants();
diff --git a/core/src/main/java/jenkins/security/ApiTokenFilter.java b/core/src/main/java/jenkins/security/ApiTokenFilter.java
index 630ae5ec04..b1078e6240 100644
--- a/core/src/main/java/jenkins/security/ApiTokenFilter.java
+++ b/core/src/main/java/jenkins/security/ApiTokenFilter.java
@@ -2,9 +2,13 @@ package jenkins.security;
import hudson.model.User;
import hudson.security.ACL;
+import hudson.security.UserMayOrMayNotExistException;
import hudson.util.Scrambler;
+import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.springframework.dao.DataAccessException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
@@ -41,6 +45,17 @@ public class ApiTokenFilter implements Filter {
int idx = uidpassword.indexOf(':');
if (idx >= 0) {
String username = uidpassword.substring(0, idx);
+ try {
+ Jenkins.getInstance().getSecurityRealm().loadUserByUsername(username);
+ } catch (UserMayOrMayNotExistException x) {
+ // OK, give them the benefit of the doubt.
+ } catch (UsernameNotFoundException x) {
+ // Not/no longer a user; deny the API token. (But do not leak the information that this happened.)
+ chain.doFilter(request, response);
+ return;
+ } catch (DataAccessException x) {
+ throw new ServletException(x);
+ }
String password = uidpassword.substring(idx+1);
// attempt to authenticate as API token
diff --git a/core/src/main/resources/hudson/model/PasswordParameterDefinition/config.jelly b/core/src/main/resources/hudson/model/PasswordParameterDefinition/config.jelly
index 2ccb38245e..53b9fd8557 100644
--- a/core/src/main/resources/hudson/model/PasswordParameterDefinition/config.jelly
+++ b/core/src/main/resources/hudson/model/PasswordParameterDefinition/config.jelly
@@ -30,7 +30,7 @@ THE SOFTWARE.
-
+
diff --git a/core/src/main/resources/lib/hudson/setIconSize.jelly b/core/src/main/resources/lib/hudson/setIconSize.jelly
index 138c220ae0..bf9e7a0c05 100644
--- a/core/src/main/resources/lib/hudson/setIconSize.jelly
+++ b/core/src/main/resources/lib/hudson/setIconSize.jelly
@@ -27,7 +27,7 @@ THE SOFTWARE.
-->
-
+
diff --git a/core/src/test/java/hudson/markup/MyspacePolicyTest.java b/core/src/test/java/hudson/markup/MyspacePolicyTest.java
index 75ac37b28c..6885afecf8 100644
--- a/core/src/test/java/hudson/markup/MyspacePolicyTest.java
+++ b/core/src/test/java/hudson/markup/MyspacePolicyTest.java
@@ -34,9 +34,7 @@ public class MyspacePolicyTest extends Assert {
assertIntact("");
assertIntact("title
blurb
");
- assertIntact("");
- assertIntact("");
- assertReject("javascript","");
+ assertReject("iframe", "");
assertReject("script","");
assertReject("script","");
@@ -47,7 +45,7 @@ public class MyspacePolicyTest extends Assert {
assertIntact("inline CSS
");
assertIntact("
");
- assertIntact("");
+ assertReject("sun.com", "");
}
private void assertIntact(String input) {
diff --git a/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java b/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java
new file mode 100644
index 0000000000..8b4dcb2906
--- /dev/null
+++ b/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java
@@ -0,0 +1,43 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2013 Jesse Glick.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.model;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+
+public class PasswordParameterDefinitionTest {
+
+ @Rule public JenkinsRule j = new JenkinsRule();
+
+ @Test public void defaultValueKeptSecret() throws Exception {
+ FreeStyleProject p = j.createFreeStyleProject();
+ p.addProperty(new ParametersDefinitionProperty(new PasswordParameterDefinition("p", "s3cr3t", "")));
+ j.configRoundtrip(p);
+ assertEquals("s3cr3t", ((PasswordParameterDefinition) p.getProperty(ParametersDefinitionProperty.class).getParameterDefinition("p")).getDefaultValue());
+ }
+
+}
diff --git a/test/src/test/java/hudson/tasks/BuildTriggerTest.java b/test/src/test/java/hudson/tasks/BuildTriggerTest.java
index f9df59e122..2d4f5e5fcb 100644
--- a/test/src/test/java/hudson/tasks/BuildTriggerTest.java
+++ b/test/src/test/java/hudson/tasks/BuildTriggerTest.java
@@ -23,14 +23,26 @@
*/
package hudson.tasks;
+import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
import hudson.maven.MavenModuleSet;
import hudson.maven.MavenModuleSetBuild;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
+import hudson.model.Item;
import hudson.model.Result;
import hudson.model.Run;
+import hudson.security.AuthorizationMatrixProperty;
+import hudson.security.LegacySecurityRealm;
+import hudson.security.Permission;
+import hudson.security.ProjectMatrixAuthorizationStrategy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import jenkins.model.Jenkins;
import org.jvnet.hudson.test.ExtractResourceSCM;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.MockBuilder;
@@ -131,4 +143,36 @@ public class BuildTriggerTest extends HudsonTestCase {
public void testMavenTriggerEvenWhenUnstable() throws Exception {
doMavenTriggerTest(true);
}
+
+ public void testConfigureDownstreamProjectSecurity() throws Exception {
+ jenkins.setSecurityRealm(new LegacySecurityRealm());
+ ProjectMatrixAuthorizationStrategy auth = new ProjectMatrixAuthorizationStrategy();
+ auth.add(Jenkins.READ, "alice");
+ jenkins.setAuthorizationStrategy(auth);
+ FreeStyleProject upstream = createFreeStyleProject("upstream");
+ Map> perms = new HashMap>();
+ perms.put(Item.READ, Collections.singleton("alice"));
+ perms.put(Item.CONFIGURE, Collections.singleton("alice"));
+ upstream.addProperty(new AuthorizationMatrixProperty(perms));
+ FreeStyleProject downstream = createFreeStyleProject("downstream");
+ /* Original SECURITY-55 test case:
+ downstream.addProperty(new AuthorizationMatrixProperty(Collections.singletonMap(Item.READ, Collections.singleton("alice"))));
+ */
+ WebClient wc = createWebClient();
+ wc.login("alice");
+ HtmlPage page = wc.getPage(upstream, "configure");
+ HtmlForm config = page.getFormByName("config");
+ config.getButtonByCaption("Add post-build action").click(); // lib/hudson/project/config-publishers2.jelly
+ page.getAnchorByText("Build other projects").click();
+ HtmlTextInput childProjects = config.getInputByName("buildTrigger.childProjects");
+ childProjects.setValueAttribute("downstream");
+ try {
+ submit(config);
+ fail();
+ } catch (FailingHttpStatusCodeException x) {
+ assertEquals(403, x.getStatusCode());
+ }
+ assertEquals(Collections.emptyList(), upstream.getDownstreamProjects());
+ }
+
}