Skip to content

Commit af73b7c

Browse files
authored
Merge pull request #907 from amuniz/JENKINS-73470
[JENKINS-73470] Option to hide "Use Groovy Sandbox"
2 parents 780f54f + 418ece3 commit af73b7c

File tree

6 files changed

+219
-3
lines changed

6 files changed

+219
-3
lines changed

plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,33 @@
2424

2525
package org.jenkinsci.plugins.workflow.cps;
2626

27+
import edu.umd.cs.findbugs.annotations.CheckForNull;
2728
import edu.umd.cs.findbugs.annotations.NonNull;
29+
import hudson.AbortException;
2830
import hudson.Extension;
2931
import hudson.model.Action;
32+
import hudson.model.Descriptor;
33+
import hudson.model.Failure;
3034
import hudson.model.Item;
3135
import hudson.model.Job;
3236
import hudson.model.Queue;
3337
import hudson.model.Run;
3438
import hudson.model.TaskListener;
3539
import hudson.util.FormValidation;
3640
import hudson.util.StreamTaskListener;
41+
import jenkins.model.Jenkins;
3742
import net.sf.json.JSONObject;
3843
import org.apache.commons.lang.StringUtils;
44+
import org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration;
3945
import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn;
4046
import org.jenkinsci.plugins.workflow.flow.DurabilityHintProvider;
4147
import org.jenkinsci.plugins.workflow.flow.FlowDefinition;
4248
import org.jenkinsci.plugins.workflow.flow.FlowDefinitionDescriptor;
4349
import org.jenkinsci.plugins.workflow.flow.FlowDurabilityHint;
4450
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
4551
import org.jenkinsci.plugins.workflow.flow.GlobalDefaultFlowDurabilityLevel;
52+
import org.kohsuke.accmod.Restricted;
53+
import org.kohsuke.accmod.restrictions.NoExternalUse;
4654
import org.kohsuke.stapler.AncestorInPath;
4755
import org.kohsuke.stapler.DataBoundConstructor;
4856

@@ -75,16 +83,22 @@ public class CpsFlowDefinition extends FlowDefinition {
7583
* @deprecated use {@link #CpsFlowDefinition(String, boolean)} instead
7684
*/
7785
@Deprecated
78-
public CpsFlowDefinition(String script) {
86+
public CpsFlowDefinition(String script) throws Descriptor.FormException {
7987
this(script, false);
8088
}
8189

8290
@DataBoundConstructor
83-
public CpsFlowDefinition(String script, boolean sandbox) {
91+
public CpsFlowDefinition(String script, boolean sandbox) throws Descriptor.FormException {
92+
if (CPSConfiguration.get().isHideSandbox() && !sandbox && !Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
93+
// this will end up in the /oops page until https://github.com/jenkinsci/jenkins/pull/9495 is picked up
94+
throw new Descriptor.FormException("Sandbox cannot be disabled. This Jenkins instance has been configured to not " +
95+
"allow regular users to disable the sandbox in pipelines", "sandbox");
96+
}
8497
StaplerRequest req = Stapler.getCurrentRequest();
8598
this.script = sandbox ? script : ScriptApproval.get().configuring(script, GroovyLanguage.get(),
8699
ApprovalContext.create().withCurrentUser().withItemAsKey(req != null ? req.findAncestorObject(Item.class) : null), req == null);
87100
this.sandbox = sandbox;
101+
88102
}
89103

90104
private Object readResolve() {
@@ -178,5 +192,13 @@ public JSON doCheckScriptCompile(@AncestorInPath Item job, @QueryParameter Strin
178192
// Approval requirements are managed by regular stapler form validation (via doCheckScript)
179193
}
180194

195+
@Restricted(NoExternalUse.class) // stapler
196+
public boolean shouldHideSandbox(@CheckForNull CpsFlowDefinition instance) {
197+
// sandbox checkbox is shown to admins even if the global configuration says otherwise
198+
// it's also shown when sandbox == false, so regular users can enable it
199+
return CPSConfiguration.get().isHideSandbox() && !Jenkins.get().hasPermission(Jenkins.ADMINISTER)
200+
&& (instance == null || instance.sandbox);
201+
}
202+
181203
}
182204
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2024, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package org.jenkinsci.plugins.workflow.cps.config;
26+
27+
import edu.umd.cs.findbugs.annotations.NonNull;
28+
import hudson.Extension;
29+
import hudson.ExtensionList;
30+
import jenkins.model.GlobalConfiguration;
31+
import jenkins.model.GlobalConfigurationCategory;
32+
import org.jenkinsci.Symbol;
33+
34+
@Symbol("cps")
35+
@Extension
36+
public class CPSConfiguration extends GlobalConfiguration {
37+
38+
/**
39+
* Whether to show the sandbox checkbox in jobs to users without Jenkins.ADMINISTER
40+
*/
41+
private boolean hideSandbox;
42+
43+
public CPSConfiguration() {
44+
load();
45+
}
46+
47+
public boolean isHideSandbox() {
48+
return hideSandbox;
49+
}
50+
51+
public void setHideSandbox(boolean hideSandbox) {
52+
this.hideSandbox = hideSandbox;
53+
save();
54+
}
55+
56+
@NonNull
57+
@Override
58+
public GlobalConfigurationCategory getCategory() {
59+
return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
60+
}
61+
62+
public static CPSConfiguration get() {
63+
return ExtensionList.lookupSingleton(CPSConfiguration.class);
64+
}
65+
}

plugin/src/main/resources/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition/config.jelly

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<f:entry title="${%Script}" field="script">
2929
<wfe:workflow-editor />
3030
</f:entry>
31-
<f:entry field="sandbox">
31+
<f:entry field="sandbox" class="${descriptor.shouldHideSandbox(instance) ? 'jenkins-hidden' : ''}">
3232
<f:checkbox title="${%Use Groovy Sandbox}" default="true"/>
3333
</f:entry>
3434
<f:block>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
The MIT License
4+
5+
Copyright 2024 CloudBees, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
24+
-->
25+
26+
<?jelly escape-by-default='true'?>
27+
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
28+
<f:section title="${%Pipeline Sandbox}">
29+
<f:entry field="hideSandbox" title="${%Hide Sandbox checkbox in pipeline jobs}">
30+
<f:checkbox/>
31+
</f:entry>
32+
</f:section>
33+
</j:jelly>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div>
2+
<p>Controls whether the "Use Groovy Sandbox" is shown in pipeline jobs configuration page to users without Overall/Administer permission.</p>
3+
<p>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)</p>
4+
<p>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).
5+
This option is only hiding the checkbox from the HTML UI</p>
6+
</div>

plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinitionTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
package org.jenkinsci.plugins.workflow.cps;
2626

27+
import hudson.util.VersionNumber;
28+
import org.htmlunit.FailingHttpStatusCodeException;
2729
import org.htmlunit.HttpMethod;
2830
import org.htmlunit.WebRequest;
2931
import org.htmlunit.html.HtmlCheckBoxInput;
@@ -41,20 +43,28 @@
4143
import org.apache.tools.ant.filters.StringInputStream;
4244
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
4345
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage;
46+
import org.jenkinsci.plugins.workflow.cps.config.CPSConfiguration;
4447
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
4548
import org.junit.Rule;
4649
import org.junit.Test;
4750
import org.jvnet.hudson.test.Issue;
4851
import org.jvnet.hudson.test.JenkinsRule;
4952
import org.jvnet.hudson.test.MockAuthorizationStrategy;
5053

54+
import java.nio.charset.StandardCharsets;
5155
import java.util.List;
5256

5357
import static org.hamcrest.MatcherAssert.assertThat;
58+
import static org.hamcrest.Matchers.containsStringIgnoringCase;
59+
import static org.hamcrest.Matchers.equalTo;
5460
import static org.hamcrest.Matchers.hasSize;
61+
import static org.hamcrest.Matchers.not;
62+
import static org.hamcrest.Matchers.notNullValue;
63+
import static org.hamcrest.Matchers.nullValue;
5564
import static org.junit.Assert.assertEquals;
5665
import static org.junit.Assert.assertFalse;
5766
import static org.junit.Assert.assertTrue;
67+
import static org.junit.Assert.fail;
5868

5969
public class CpsFlowDefinitionTest {
6070

@@ -280,4 +290,84 @@ public void cpsScriptSubmissionViaRest() throws Exception {
280290
assertFalse(ScriptApproval.get().isScriptApproved(configuredViaRestByNonAdmin, GroovyLanguage.get()));
281291
wc.close();
282292
}
293+
294+
@Test
295+
public void cpsScriptSandboxHide() throws Exception {
296+
jenkins.jenkins.setSecurityRealm(jenkins.createDummySecurityRealm());
297+
298+
MockAuthorizationStrategy mockStrategy = new MockAuthorizationStrategy();
299+
mockStrategy.grant(Jenkins.READ).everywhere().to("devel");
300+
for (Permission p : Item.PERMISSIONS.getPermissions()) {
301+
mockStrategy.grant(p).everywhere().to("devel");
302+
}
303+
mockStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin");
304+
jenkins.jenkins.setAuthorizationStrategy(mockStrategy);
305+
306+
WorkflowJob p = jenkins.createProject(WorkflowJob.class);
307+
p.setDefinition(new CpsFlowDefinition("echo 'Hello'", true));
308+
309+
JenkinsRule.WebClient wcDevel = jenkins.createWebClient();
310+
311+
// non-admins can see the sandbox checkbox in jobs by default
312+
wcDevel.login("devel");
313+
{
314+
HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config");
315+
assertThat(config.getVisibleText(), containsStringIgnoringCase("Use Groovy Sandbox"));
316+
}
317+
318+
// non-admins cannot see the sandbox checkbox in jobs if hideSandbox is On globally
319+
CPSConfiguration.get().setHideSandbox(true);
320+
{
321+
HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config");
322+
assertThat(config.getVisibleText(), not(containsStringIgnoringCase("Use Groovy Sandbox")));
323+
324+
// but, when the sandbox is disabled the checkbox is shown so users can enable it
325+
p.setDefinition(new CpsFlowDefinition("echo 'Hello'", false));
326+
config = wcDevel.getPage(p, "configure").getFormByName("config");
327+
assertThat(config.getVisibleText(), containsStringIgnoringCase("Use Groovy Sandbox"));
328+
}
329+
330+
// admins can always see the sandbox checkbox
331+
CPSConfiguration.get().setHideSandbox(false);
332+
wcDevel.login("admin");
333+
{
334+
HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config");
335+
assertThat(config.getVisibleText(), containsStringIgnoringCase("Use Groovy Sandbox"));
336+
}
337+
338+
// even when set to hide globally
339+
CPSConfiguration.get().setHideSandbox(true);
340+
{
341+
HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config");
342+
assertThat(config.getVisibleText(), containsStringIgnoringCase("Use Groovy Sandbox"));
343+
}
344+
345+
// regular users cannot save jobs if the sandbox is disabled
346+
p.setDefinition(new CpsFlowDefinition("echo 'Hello'", false));
347+
wcDevel.login("devel");
348+
{
349+
HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config");
350+
assertThat(config.getVisibleText(), containsStringIgnoringCase("Use Groovy Sandbox"));
351+
List<HtmlInput> sandboxes = config.getInputsByName("_.sandbox");
352+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
353+
HtmlCheckBoxInput sandbox = (HtmlCheckBoxInput) sandboxes.get(sandboxes.size() - 1);
354+
assertFalse("Sandbox is disabled", sandbox.isChecked());
355+
VersionNumber jenkinsVersion = new VersionNumber(Jenkins.VERSION);
356+
int expectedStatus = 500;
357+
if (jenkinsVersion.isNewerThanOrEqualTo(new VersionNumber("2.470"))) { // TODO pending https://github.com/jenkinsci/jenkins/pull/9495 in baseline
358+
expectedStatus = 400;
359+
}
360+
try {
361+
jenkins.submit(config);
362+
fail("Expected HTTP " + expectedStatus);
363+
} catch (FailingHttpStatusCodeException e) {
364+
// good, expected
365+
assertThat(e.getStatusCode(), equalTo(expectedStatus));
366+
if (expectedStatus == 400) {
367+
assertThat(e.getResponse().getContentAsString(StandardCharsets.UTF_8), containsStringIgnoringCase("Sandbox cannot be disabled"));
368+
}
369+
}
370+
371+
}
372+
}
283373
}

0 commit comments

Comments
 (0)