diff --git a/plugin/pom.xml b/plugin/pom.xml index ef29baaea..3c71061f7 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -54,6 +54,11 @@ import pom + + org.jenkins-ci.plugins + script-security + 1367.vdf2fc45f229c + @@ -246,6 +251,16 @@ 4.2.2 test + + io.jenkins + configuration-as-code + test + + + io.jenkins.configuration-as-code + test-harness + test + diff --git a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java index 01f21b798..d73cf4e5a 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java @@ -41,7 +41,6 @@ import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; -import org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration; import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn; import org.jenkinsci.plugins.workflow.flow.DurabilityHintProvider; import org.jenkinsci.plugins.workflow.flow.FlowDefinition; @@ -89,7 +88,7 @@ public CpsFlowDefinition(String script) throws Descriptor.FormException { @DataBoundConstructor public CpsFlowDefinition(String script, boolean sandbox) throws Descriptor.FormException { - if (CPSConfiguration.get().isHideSandbox() && !sandbox && !Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + if (!sandbox && ScriptApproval.get().isForceSandboxForCurrentUser()) { // this will end up in the /oops page until https://github.com/jenkinsci/jenkins/pull/9495 is picked up throw new Descriptor.FormException("Sandbox cannot be disabled. This Jenkins instance has been configured to not " + "allow regular users to disable the sandbox in pipelines", "sandbox"); @@ -98,7 +97,6 @@ public CpsFlowDefinition(String script, boolean sandbox) throws Descriptor.FormE this.script = sandbox ? script : ScriptApproval.get().configuring(script, GroovyLanguage.get(), ApprovalContext.create().withCurrentUser().withItemAsKey(req != null ? req.findAncestorObject(Item.class) : null), req == null); this.sandbox = sandbox; - } private Object readResolve() { @@ -196,7 +194,7 @@ public JSON doCheckScriptCompile(@AncestorInPath Item job, @QueryParameter Strin public boolean shouldHideSandbox(@CheckForNull CpsFlowDefinition instance) { // sandbox checkbox is shown to admins even if the global configuration says otherwise // it's also shown when sandbox == false, so regular users can enable it - return CPSConfiguration.get().isHideSandbox() && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) + return ScriptApproval.get().isForceSandboxForCurrentUser() && (instance == null || instance.sandbox); } diff --git a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java index 9de7f59c6..206a131f7 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration.java @@ -24,33 +24,55 @@ package org.jenkinsci.plugins.workflow.cps.config; +import java.io.IOException; +import java.io.UncheckedIOException; + import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ExtensionList; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; +import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; import org.jenkinsci.Symbol; +/** + * @deprecated + * This class has been deprecated and its only configuration value is ignored. Do not rely on it or use it in any way. + * In order to force using the system sandbox for pipelines, please use {@link ScriptApproval#isForceSandbox} or {@link ScriptApproval#isForceSandboxForCurrentUser} + **/ @Symbol("cps") @Extension +@Deprecated public class CPSConfiguration extends GlobalConfiguration { /** * Whether to show the sandbox checkbox in jobs to users without Jenkins.ADMINISTER */ - private boolean hideSandbox; + private transient boolean hideSandbox; public CPSConfiguration() { + load(); + if (hideSandbox) { + ScriptApproval.get().setForceSandbox(hideSandbox); + } + + // Data migration from this configuration to ScriptApproval should be done only once, + // so removing the config file after the previous migration + try { + this.getConfigFile().delete(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } public boolean isHideSandbox() { - return hideSandbox; + return ScriptApproval.get().isForceSandbox(); } public void setHideSandbox(boolean hideSandbox) { this.hideSandbox = hideSandbox; - save(); + ScriptApproval.get().setForceSandbox(hideSandbox); } @NonNull diff --git a/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly index 0c7a1cebc..afd7376ad 100644 --- a/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly +++ b/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/config.jelly @@ -25,9 +25,4 @@ THE SOFTWARE. - - - - - \ No newline at end of file diff --git a/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/help-hideSandbox.html b/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/help-hideSandbox.html deleted file mode 100644 index 158790b5f..000000000 --- a/plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/config/CPSConfiguration/help-hideSandbox.html +++ /dev/null @@ -1,6 +0,0 @@ -
-

Controls whether the "Use Groovy Sandbox" is shown in pipeline jobs configuration page to users without Overall/Administer permission.

-

This can be used to get a better UX in highly secured environments where all pipelines are required to run in the sandbox (ie. running arbitrary code is never approved)

-

Note that this does not prevent users to configure and run pipelines with sandbox disabled if they create or update jobs by other means (like CLI or HTTP API). - This option is only hiding the checkbox from the HTML UI

-
\ No newline at end of file diff --git a/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest.java b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest.java index 3ec930cc7..cb68e9dbe 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest.java @@ -24,6 +24,7 @@ package org.jenkinsci.plugins.workflow.cps; +import hudson.model.Result; import hudson.util.VersionNumber; import org.htmlunit.FailingHttpStatusCodeException; import org.htmlunit.HttpMethod; @@ -45,12 +46,15 @@ import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage; import org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration; import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; 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.MockAuthorizationStrategy; +import org.jvnet.hudson.test.recipes.LocalData; +import java.io.File; import java.nio.charset.StandardCharsets; import java.util.List; @@ -59,8 +63,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -316,7 +318,7 @@ public void cpsScriptSandboxHide() throws Exception { } // non-admins cannot see the sandbox checkbox in jobs if hideSandbox is On globally - CPSConfiguration.get().setHideSandbox(true); + ScriptApproval.get().setForceSandbox(true); { HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config"); assertThat(config.getVisibleText(), not(containsStringIgnoringCase("Use Groovy Sandbox"))); @@ -328,7 +330,7 @@ public void cpsScriptSandboxHide() throws Exception { } // admins can always see the sandbox checkbox - CPSConfiguration.get().setHideSandbox(false); + ScriptApproval.get().setForceSandbox(false); wcDevel.login("admin"); { HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config"); @@ -336,7 +338,7 @@ public void cpsScriptSandboxHide() throws Exception { } // even when set to hide globally - CPSConfiguration.get().setHideSandbox(true); + ScriptApproval.get().setForceSandbox(true); { HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config"); assertThat(config.getVisibleText(), containsStringIgnoringCase("Use Groovy Sandbox")); @@ -370,4 +372,67 @@ public void cpsScriptSandboxHide() throws Exception { } } + + @Test + public void cpsConfigurationSandboxToScriptApprovalSandbox() throws Exception{ + //Deprecated CPSConfiguration should update ScriptApproval forceSandbox logic to keep casc compatibility + ScriptApproval.get().setForceSandbox(false); + + CPSConfiguration.get().setHideSandbox(true); + assertTrue(ScriptApproval.get().isForceSandbox()); + + ScriptApproval.get().setForceSandbox(false); + assertFalse(CPSConfiguration.get().isHideSandbox()); + } + + @Test + public void cpsScriptSignatureException() throws Exception { + ScriptApproval.get().setForceSandbox(false); + WorkflowJob p = jenkins.createProject(WorkflowJob.class); + String script = "jenkins.model.Jenkins.instance"; + p.setDefinition(new CpsFlowDefinition(script, true)); + WorkflowRun b = jenkins.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + jenkins.assertLogContains("Scripts not permitted to use staticMethod jenkins.model.Jenkins getInstance. " + + "Administrators can decide whether to approve or reject this signature.", b); + + ScriptApproval.get().setForceSandbox(true); + b = jenkins.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + jenkins.assertLogContains("Scripts not permitted to use staticMethod jenkins.model.Jenkins getInstance. " + + "Script signature is not in the default whitelist.", b); + } + + @Test + @LocalData + public void cpsLoadConfiguration() throws Exception { + //CPSConfiguration file containing true + // should be promoted to ScriptApproval.get().isForceSandbox() + assertTrue(ScriptApproval.get().isForceSandbox()); + + //Once the info is promoted, we are removing the config file, so should no longer exist. + //We are checking the injected localData is removed + assertFalse(new File(jenkins.jenkins.getRootDir(), + "org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration.xml").exists()); + } + + @Test + public void cpsRoundTrip() throws Exception { + jenkins.jenkins.setSecurityRealm(jenkins.createDummySecurityRealm()); + + MockAuthorizationStrategy mockStrategy = new MockAuthorizationStrategy(); + mockStrategy.grant(Jenkins.READ).everywhere().to("dev"); + for (Permission p : Item.PERMISSIONS.getPermissions()) { + mockStrategy.grant(p).everywhere().to("dev"); + } + + WorkflowJob p = jenkins.createProject(WorkflowJob.class); + p.setDefinition(new CpsFlowDefinition("echo 'Hello'", true)); + + WorkflowJob roundTrip = jenkins.configRoundtrip(p); + + assertEquals(((CpsFlowDefinition)p.getDefinition()).isSandbox(), + ((CpsFlowDefinition)roundTrip.getDefinition()).isSandbox()); + + assertEquals(((CpsFlowDefinition)p.getDefinition()).getScript(), + ((CpsFlowDefinition)roundTrip.getDefinition()).getScript()); + } } diff --git a/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/JcascTest.java b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/JcascTest.java new file mode 100644 index 000000000..aba7c9ae0 --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/JcascTest.java @@ -0,0 +1,42 @@ +package org.jenkinsci.plugins.workflow.cps; + +import io.jenkins.plugins.casc.ConfigurationContext; +import io.jenkins.plugins.casc.ConfiguratorRegistry; +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import io.jenkins.plugins.casc.model.CNode; +import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static io.jenkins.plugins.casc.misc.Util.getSecurityRoot; +import static io.jenkins.plugins.casc.misc.Util.toStringFromYamlFile; +import static io.jenkins.plugins.casc.misc.Util.toYamlString; + +public class JcascTest { + + @ClassRule(order = 1) + @ConfiguredWithCode("casc_test.yaml") + public static JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule(); + + /** + * Check that CASC for security.cps.hideSandbox is sending the value to ScriptApproval.get().isForceSandbox() + * @throws Exception + */ + @Test + public void cascHideSandBox() throws Exception { + assertTrue(ScriptApproval.get().isForceSandbox()); + } + + @Test + public void cascExport() throws Exception { + ConfiguratorRegistry registry = ConfiguratorRegistry.get(); + ConfigurationContext context = new ConfigurationContext(registry); + CNode yourAttribute = getSecurityRoot(context).get("cps"); + String exported = toYamlString(yourAttribute); + String expected = toStringFromYamlFile(this, "casc_test_expected.yaml"); + assertEquals(exported, expected); + } +} diff --git a/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest/cpsLoadConfiguration/org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration.xml b/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest/cpsLoadConfiguration/org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration.xml new file mode 100644 index 000000000..12a4b3d33 --- /dev/null +++ b/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest/cpsLoadConfiguration/org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/casc_test.yaml b/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/casc_test.yaml new file mode 100644 index 000000000..04fa77f0a --- /dev/null +++ b/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/casc_test.yaml @@ -0,0 +1,3 @@ +security: + cps: + hideSandbox: true \ No newline at end of file diff --git a/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/casc_test_expected.yaml b/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/casc_test_expected.yaml new file mode 100644 index 000000000..1852e7be5 --- /dev/null +++ b/plugin/src/test/resources/org/jenkinsci/plugins/workflow/cps/casc_test_expected.yaml @@ -0,0 +1 @@ +hideSandbox: true