/* * 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.util; import hudson.cli.CLICommandInvoker; import hudson.diagnosis.OldDataMonitor; import hudson.model.AbstractDescribableImpl; import hudson.model.Items; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; import hudson.model.Descriptor; import hudson.model.FreeStyleProject; import hudson.model.Job; import hudson.model.Saveable; import hudson.security.ACL; import java.io.ByteArrayInputStream; import java.util.Collections; import java.util.Map; import jenkins.model.Jenkins; import static org.junit.Assert.*; import net.sf.json.JSONObject; 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.JenkinsRule.WebClient; import org.jvnet.hudson.test.TestExtension; import org.jvnet.hudson.test.recipes.LocalData; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.HttpMethod; import com.gargoylesoftware.htmlunit.WebRequest; public class RobustReflectionConverterTest { @Rule public JenkinsRule r = new JenkinsRule(); @Issue("JENKINS-21024") @LocalData @Test public void randomExceptionsReported() throws Exception { FreeStyleProject p = r.jenkins.getItemByFullName("j", FreeStyleProject.class); assertNotNull(p); assertEquals(Collections.emptyMap(), p.getTriggers()); OldDataMonitor odm = (OldDataMonitor) r.jenkins.getAdministrativeMonitor("OldData"); Map<Saveable,OldDataMonitor.VersionRange> data = odm.getData(); assertEquals(Collections.singleton(p), data.keySet()); String text = data.values().iterator().next().extra; assertTrue(text, text.contains("Could not call hudson.triggers.TimerTrigger.readResolve")); } // Testing describable object to demonstrate what is expected with RobustReflectionConverter#addCriticalField // This should be configured with a specific keyword, // and should reject configurations with other keywords. // GUI related implementations (@DataBoundConstructor and newInstance) aren't used actually // (no jelly files are provides and they don't work actually), // but written to clarify a use case. public static class AcceptOnlySpecificKeyword extends AbstractDescribableImpl<AcceptOnlySpecificKeyword>{ public static final String ACCEPT_KEYWORD = "accept"; private final String keyword; @DataBoundConstructor public AcceptOnlySpecificKeyword(String keyword) { this.keyword = keyword; } public String getKeyword() { return keyword; } public boolean isAcceptable() { return ACCEPT_KEYWORD.equals(keyword); } public Object readResolve() throws Exception { if (!ACL.SYSTEM.equals(Jenkins.getAuthentication())) { // called via REST / CLI with authentication if (!isAcceptable()) { // Reject invalid configuration via REST / CLI. throw new Exception(String.format("Bad keyword: %s", getKeyword())); } } return this; } @TestExtension public static class DescriptorImpl extends Descriptor<AcceptOnlySpecificKeyword> { @Override public String getDisplayName() { return "AcceptOnlySpecificKeyword"; } @Override public AcceptOnlySpecificKeyword newInstance(StaplerRequest req, JSONObject formData) throws FormException { AcceptOnlySpecificKeyword instance = super.newInstance(req, formData); if (!instance.isAcceptable()) { throw new FormException(String.format("Bad keyword: %s", instance.getKeyword()), "keyword"); } return instance; } } } public static class KeywordProperty extends JobProperty<Job<?,?>> { private final AcceptOnlySpecificKeyword nonCriticalField; private final AcceptOnlySpecificKeyword criticalField; public KeywordProperty(AcceptOnlySpecificKeyword nonCriticalField, AcceptOnlySpecificKeyword criticalField) { this.nonCriticalField = nonCriticalField; this.criticalField = criticalField; } public AcceptOnlySpecificKeyword getNonCriticalField() { return nonCriticalField; } public AcceptOnlySpecificKeyword getCriticalField() { return criticalField; } @TestExtension public static class DescriptorImpl extends JobPropertyDescriptor { @Override public String getDisplayName() { return "KeywordProperty"; } @Override public JobProperty<?> newInstance(StaplerRequest req, JSONObject formData) throws FormException { // unfortunately, default newInstance bypasses newInstances for members. formData = formData.getJSONObject("keywordProperty"); @SuppressWarnings("unchecked") Descriptor<AcceptOnlySpecificKeyword> d = Jenkins.getInstance().getDescriptor(AcceptOnlySpecificKeyword.class); return new KeywordProperty( d.newInstance(req, formData.getJSONObject("nonCriticalField")), d.newInstance(req, formData.getJSONObject("criticalField")) ); } } } private static final String CONFIGURATION_TEMPLATE = "<?xml version='1.0' encoding='UTF-8'?>" + "<project>" + "<properties>" + "<hudson.util.RobustReflectionConverterTest_-KeywordProperty>" + "<nonCriticalField>" + "<keyword>%s</keyword>" + "</nonCriticalField>" + "<criticalField>" + "<keyword>%s</keyword>" + "</criticalField>" + "</hudson.util.RobustReflectionConverterTest_-KeywordProperty>" + "</properties>" + "</project>"; @Test public void testRestInterfaceFailure() throws Exception { Items.XSTREAM2.addCriticalField(KeywordProperty.class, "criticalField"); // without addCriticalField. This is accepted. { FreeStyleProject p = r.createFreeStyleProject(); p.addProperty(new KeywordProperty( new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD), new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD) )); p.save(); // Configure a bad keyword via REST. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); WebClient wc = r.createWebClient(); wc.login("test", "test"); WebRequest req = new WebRequest( wc.createCrumbedUrl(String.format("%s/config.xml", p.getUrl())), HttpMethod.POST ); req.setEncodingType(null); req.setRequestBody(String.format(CONFIGURATION_TEMPLATE, "badvalue", AcceptOnlySpecificKeyword.ACCEPT_KEYWORD)); wc.getPage(req); // AcceptOnlySpecificKeyword with bad value is not instantiated for rejected with readResolve, assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField()); assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword()); // but save to the disk. r.jenkins.reload(); p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class); assertEquals("badvalue", p.getProperty(KeywordProperty.class).getNonCriticalField().getKeyword()); assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword()); } // with addCriticalField. This is not accepted. { FreeStyleProject p = r.createFreeStyleProject(); p.addProperty(new KeywordProperty( new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD), new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD) )); p.save(); // Configure a bad keyword via REST. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); WebClient wc = r.createWebClient(); wc.login("test", "test"); WebRequest req = new WebRequest( wc.createCrumbedUrl(String.format("%s/config.xml", p.getUrl())), HttpMethod.POST ); req.setEncodingType(null); req.setRequestBody(String.format(CONFIGURATION_TEMPLATE, AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, "badvalue")); try { wc.getPage(req); fail("Submitting unacceptable configuration via REST should fail."); } catch (FailingHttpStatusCodeException e) { // pass } // Configuration should not be updated for a failure of the critical field, assertNotEquals("badvalue", p.getProperty(KeywordProperty.class).getCriticalField().getKeyword()); r.jenkins.reload(); // rejected configuration is not saved p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class); assertNotEquals("badvalue", p.getProperty(KeywordProperty.class).getCriticalField().getKeyword()); } } @Test public void testCliFailure() throws Exception { Items.XSTREAM2.addCriticalField(KeywordProperty.class, "criticalField"); // without addCriticalField. This is accepted. { FreeStyleProject p = r.createFreeStyleProject(); p.addProperty(new KeywordProperty( new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD), new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD) )); p.save(); // Configure a bad keyword via CLI. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); CLICommandInvoker.Result ret = new CLICommandInvoker(r, "update-job") .withStdin(new ByteArrayInputStream(String.format(CONFIGURATION_TEMPLATE, "badvalue", AcceptOnlySpecificKeyword.ACCEPT_KEYWORD).getBytes())) .withArgs( p.getFullName(), "--username", "test", "--password", "test" ) .invoke(); assertEquals(0, ret.returnCode()); // AcceptOnlySpecificKeyword with bad value is not instantiated for rejected with readResolve, assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField()); assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword()); // but save to the disk. r.jenkins.reload(); p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class); assertEquals("badvalue", p.getProperty(KeywordProperty.class).getNonCriticalField().getKeyword()); } // with addCriticalField. This is not accepted. { FreeStyleProject p = r.createFreeStyleProject(); p.addProperty(new KeywordProperty( new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD), new AcceptOnlySpecificKeyword(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD) )); p.save(); // Configure a bad keyword via CLI. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); CLICommandInvoker.Result ret = new CLICommandInvoker(r, "update-job") .withStdin(new ByteArrayInputStream(String.format(CONFIGURATION_TEMPLATE, AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, "badvalue").getBytes())) .withArgs( p.getFullName(), "--username", "test", "--password", "test" ) .invoke(); assertNotEquals(0, ret.returnCode()); // Configuration should not be updated for a failure of the critical field, assertNotEquals("badvalue", p.getProperty(KeywordProperty.class).getCriticalField().getKeyword()); r.jenkins.reload(); // rejected configuration is not saved p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class); assertNotEquals("badvalue", p.getProperty(KeywordProperty.class).getCriticalField().getKeyword()); } } }