diff --git a/pom.xml b/pom.xml
index f9f1f56..2b69c2c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
org.jenkins-ci.plugins
plugin
- 4.86
+ 4.88
command-launcher
@@ -13,7 +13,7 @@
999999-SNAPSHOT
jenkinsci/${project.artifactId}-plugin
- 2.440.3
+ 2.452.4
Command Agent Launcher Plugin
Allows agents to be launched using a specified command.
@@ -46,8 +46,8 @@
io.jenkins.tools.bom
- bom-2.440.x
- 3234.v5ca_5154341ef
+ bom-2.452.x
+ 3654.v237e4a_f2d8da_
pom
import
diff --git a/src/main/java/hudson/slaves/CommandLauncher.java b/src/main/java/hudson/slaves/CommandLauncher.java
index e7f3d7c..d8dc9bb 100644
--- a/src/main/java/hudson/slaves/CommandLauncher.java
+++ b/src/main/java/hudson/slaves/CommandLauncher.java
@@ -23,6 +23,7 @@
*/
package hudson.slaves;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.AbortException;
@@ -30,7 +31,9 @@
import hudson.Extension;
import hudson.Functions;
import hudson.Util;
+import hudson.model.ComputerSet;
import hudson.model.Descriptor;
+import hudson.model.DescriptorVisibilityFilter;
import hudson.model.Slave;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
@@ -39,6 +42,7 @@
import hudson.util.StreamCopyThread;
import java.io.IOException;
import java.util.Date;
+import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
@@ -49,6 +53,7 @@
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException;
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.SystemCommandLanguage;
+import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
@@ -80,7 +85,8 @@ public class CommandLauncher extends ComputerLauncher {
* @see #CommandLauncher(String command, EnvVars env)
*/
@DataBoundConstructor
- public CommandLauncher(String command) {
+ public CommandLauncher(String command) throws Descriptor.FormException {
+ checkSandbox();
agentCommand = command;
env = null;
// TODO add withKey if we can determine the Slave.nodeName being configured
@@ -105,6 +111,18 @@ public CommandLauncher(String command, EnvVars env) {
this.agentCommand = command;
this.env = env;
}
+
+ /**
+ * Check if the current user is forced to use the Sandbox when creating a new instance.
+ * In this case, we don't allow saving new instances of the CommandLauncher object by throwing a new exception
+ */
+ private void checkSandbox() throws Descriptor.FormException {
+ if (ScriptApproval.get().isForceSandboxForCurrentUser()) {
+ throw new Descriptor.FormException(
+ "This Launch Method requires scripts executions out of the sandbox."
+ + " This Jenkins instance has been configured to not allow regular users to disable the sandbox", "command");
+ }
+ }
private Object readResolve() {
ScriptApproval.get().configuring(agentCommand, SystemCommandLanguage.get(), ApprovalContext.create(), true);
@@ -250,4 +268,48 @@ public FormValidation doCheckCommand(@QueryParameter String value, @QueryParamet
return ScriptApproval.get().checking(value, SystemCommandLanguage.get(), !StringUtils.equals(value, oldCommand));
}
}
+
+ /**
+ * In case the flag
+ * {@link ScriptApproval#isForceSandboxForCurrentUser} is true, we don't show the {@link DescriptorImpl descriptor}
+ * for the current user, except if we are editing a node that already has the launcher {@link CommandLauncher}
+ */
+ @Extension
+ public static class DescriptorVisibilityFilterForceSandBox extends DescriptorVisibilityFilter {
+ @Override
+ public boolean filter(@CheckForNull Object context, @NonNull Descriptor descriptor) {
+ if(descriptor instanceof DescriptorImpl) {
+ return !ScriptApproval.get().isForceSandboxForCurrentUser() ||
+ (context instanceof Slave && ((Slave) context).getLauncher() instanceof CommandLauncher);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean filterType(@NonNull Class> contextClass, @NonNull Descriptor descriptor) {
+ if(descriptor instanceof DescriptorImpl)
+ {
+ //If we are creating a new object, check ScriptApproval.get().isForceSandboxForCurrentUser()
+ //If we are NOT creating a new object, return true, and delegate the logic to #filter
+ return !(isCreatingNewObject() && ScriptApproval.get().isForceSandboxForCurrentUser());
+ }
+ return true;
+ }
+
+ private boolean isCreatingNewObject() {
+ var req = Stapler.getCurrentRequest();
+ if (req != null) {
+ List ancs = req.getAncestors();
+ for (Ancestor anc : ancs) {
+ if (anc.getObject() instanceof ComputerSet) {
+ String uri = req.getOriginalRequestURI();
+ if (uri.endsWith("createItem")) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }
}
diff --git a/src/test/java/hudson/slaves/CommandLauncherForceSandboxTest.java b/src/test/java/hudson/slaves/CommandLauncherForceSandboxTest.java
new file mode 100644
index 0000000..63f42d1
--- /dev/null
+++ b/src/test/java/hudson/slaves/CommandLauncherForceSandboxTest.java
@@ -0,0 +1,202 @@
+package hudson.slaves;
+
+import java.io.IOException;
+
+import org.htmlunit.html.HtmlForm;
+import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.JenkinsRule.WebClient;
+import org.jvnet.hudson.test.MockAuthorizationStrategy;
+
+import hudson.model.Computer;
+import hudson.model.Descriptor;
+import hudson.model.User;
+import hudson.security.ACL;
+import hudson.security.ACLContext;
+import jenkins.model.Jenkins;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class CommandLauncherForceSandboxTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Before
+ public void configureTest() throws IOException {
+ Jenkins.MANAGE.setEnabled(true);
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+
+ MockAuthorizationStrategy strategy = new MockAuthorizationStrategy().
+ grant(Jenkins.ADMINISTER).everywhere().to("admin").
+ grant(Jenkins.MANAGE).everywhere().to("devel").
+ grant(Jenkins.READ, Computer.CONFIGURE).everywhere().to("devel");
+
+ SlaveComputer.PERMISSIONS.getPermissions().forEach(p -> strategy.grant(p).everywhere().to("devel"));
+
+ j.jenkins.setAuthorizationStrategy(strategy);
+ }
+
+ @Test
+ public void newCommandLauncher() throws Exception {
+ try (ACLContext ctx = ACL.as(User.getById("devel", true))) {
+ //With forceSandbox enabled, nonadmin users should not create agents with Launcher = CommandLauncher
+ ScriptApproval.get().setForceSandbox(true);
+ Descriptor.FormException ex = assertThrows(Descriptor.FormException.class, () ->
+ new DumbSlave("s", "/",new CommandLauncher("echo unconfigured")));
+
+ assertEquals("This Launch Method requires scripts executions out of the sandbox."
+ + " This Jenkins instance has been configured to not allow regular users to disable the sandbox",
+ ex.getMessage());
+
+ //With forceSandbox disabled, nonadmin users can create agents with Launcher = CommandLauncher
+ ScriptApproval.get().setForceSandbox(false);
+ new DumbSlave("s", "/", new CommandLauncher("echo unconfigured"));
+ }
+
+ try (ACLContext ctx = ACL.as(User.getById("admin", true))) {
+ //admin users can create agents with Launcher = CommandLauncher independently of forceSandbox flag.
+ ScriptApproval.get().setForceSandbox(true);
+ new DumbSlave("s", "/", new CommandLauncher("echo unconfigured"));
+
+ ScriptApproval.get().setForceSandbox(false);
+ new DumbSlave("s", "/", new CommandLauncher("echo unconfigured"));
+ }
+ }
+
+ @Test
+ public void editCommandLauncherUI_ForceSandboxTrue() throws Exception {
+ ScriptApproval.get().setForceSandbox(true);
+
+ DumbSlave commandLauncherAgent = new DumbSlave("commandLauncherAgent", "/", new CommandLauncher("echo unconfigured"));
+ DumbSlave noCommandLauncherAgent = new DumbSlave("noCommandLauncherAgent", "/", new JNLPLauncher());
+ j.jenkins.addNode(commandLauncherAgent);
+ j.jenkins.addNode(noCommandLauncherAgent);
+
+ try (WebClient wc = j.createWebClient().login("devel")) {
+ //Edit noCommandLauncher Agent.
+ //We are not admin and Sandbox is true,
+ //We don't have any html object for CommandLauncher
+ HtmlForm form = wc.getPage(noCommandLauncherAgent, "configure").getFormByName("config");
+ assertTrue(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+
+ //Edit CommandLauncher Agent.
+ //We are not admin and Sandbox is true
+ // As the agent is already a commandLauncher one we have some html object for CommandLauncher
+ form = wc.getPage(commandLauncherAgent, "configure").getFormByName("config");
+ assertFalse(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ }
+
+ try (WebClient wc = j.createWebClient().login("admin")) {
+ //Edit noCommandLauncher Agent.
+ //We areadmin and Sandbox is true,
+ //We have some html object for CommandLauncher
+ HtmlForm form = wc.getPage(noCommandLauncherAgent, "configure").getFormByName("config");
+ assertFalse(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+
+ //Edit CommandLauncher Agent.
+ //Wwe not admin and Sandbox is true
+ //We have some html object for CommandLauncher
+ form = wc.getPage(commandLauncherAgent, "configure").getFormByName("config");
+ assertFalse(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ } }
+
+ @Test
+ public void editCommandLauncherUI_ForceSandboxFalse() throws Exception {
+ ScriptApproval.get().setForceSandbox(false);
+
+ DumbSlave commandLauncherAgent = new DumbSlave("commandLauncherAgent", "/", new CommandLauncher("echo unconfigured"));
+ DumbSlave noCommandLauncherAgent = new DumbSlave("noCommandLauncherAgent", "/", new JNLPLauncher());
+ j.jenkins.addNode(commandLauncherAgent);
+ j.jenkins.addNode(noCommandLauncherAgent);
+
+ try (WebClient wc = j.createWebClient().login("devel")) {
+ //Edit noCommandLauncher Agent.
+ //We are not admin and Sandbox is false,
+ //We have some html object for CommandLauncher
+ HtmlForm form = wc.getPage(noCommandLauncherAgent, "configure").getFormByName("config");
+ assertFalse(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+
+ //Edit CommandLauncher Agent.
+ //Wwe are not admin and Sandbox is false
+ //We have some html object for CommandLauncher
+ form = wc.getPage(commandLauncherAgent, "configure").getFormByName("config");
+ assertFalse(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ }
+
+ try (WebClient wc = j.createWebClient().login("admin")) {
+ //Edit noCommandLauncher Agent.
+ //We areadmin and Sandbox is false,
+ //We have some html object for CommandLauncher
+ HtmlForm form = wc.getPage(noCommandLauncherAgent, "configure").getFormByName("config");
+ assertFalse(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+
+ //Edit CommandLauncher Agent.
+ //Wwe not admin and Sandbox is false
+ //We have some html object for CommandLauncher
+ form = wc.getPage(commandLauncherAgent, "configure").getFormByName("config");
+ assertFalse(form.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ }
+ }
+
+ @Test
+ public void createCommandLauncherUI_ForceSandboxTrue() throws Exception {
+ ScriptApproval.get().setForceSandbox(true);
+
+ try (WebClient wc = j.createWebClient().login("devel")) {
+ //Create Permanent Agent.
+ //We are not admin and Sandbox is true,
+ //We don't have any html object for CommandLauncher
+ HtmlForm form = wc.goTo("computer/new").getFormByName("createItem");
+ form.getInputByName("name").setValue("devel_ComandLauncher");
+ form.getInputsByValue(DumbSlave.class.getName()).stream().findFirst().get().setChecked(true);
+ HtmlForm createNodeForm = j.submit(form).getFormByName("config");
+ assertTrue(createNodeForm.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ }
+
+ try (WebClient wc = j.createWebClient().login("admin")) {
+ //Create Permanent Agent.
+ //We are admin and Sandbox is true,
+ //We have some html object for CommandLauncher
+ HtmlForm form = wc.goTo("computer/new").getFormByName("createItem");
+ form.getInputByName("name").setValue("devel_ComandLauncher");
+ form.getInputsByValue(DumbSlave.class.getName()).stream().findFirst().get().setChecked(true);
+ HtmlForm createNodeForm = j.submit(form).getFormByName("config");
+ assertFalse(createNodeForm.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ }
+ }
+
+ @Test
+ public void createCommandLauncherUI_ForceSandboxFalse() throws Exception {
+ ScriptApproval.get().setForceSandbox(false);
+
+ try (WebClient wc = j.createWebClient().login("devel")) {
+ //Create Permanent Agent.
+ //We are not admin and Sandbox is false,
+ //We have some html object for CommandLauncher
+ HtmlForm form = wc.goTo("computer/new").getFormByName("createItem");
+ form.getInputByName("name").setValue("devel_ComandLauncher");
+ form.getInputsByValue(DumbSlave.class.getName()).stream().findFirst().get().setChecked(true);
+ HtmlForm createNodeForm = j.submit(form).getFormByName("config");
+ assertFalse(createNodeForm.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ }
+
+ try (WebClient wc = j.createWebClient().login("admin")) {
+ //Create Permanent Agent.
+ //We are admin and Sandbox is true,
+ //We have some html object for CommandLauncher
+ HtmlForm form = wc.goTo("computer/new").getFormByName("createItem");
+ form.getInputByName("name").setValue("devel_ComandLauncher");
+ form.getInputsByValue(DumbSlave.class.getName()).stream().findFirst().get().setChecked(true);
+ HtmlForm createNodeForm = j.submit(form).getFormByName("config");
+ assertFalse(createNodeForm.getInputsByValue(CommandLauncher.class.getName()).isEmpty());
+ }
+ }
+}