Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JENKINS-74820 - forceSandBox - Hide command-launcher drop down from non-administrators #103

Merged
merged 17 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0b18ac3
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 6, 2024
c5ab3e9
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 8, 2024
1e1d95d
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 11, 2024
230ae87
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 11, 2024
ee82534
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 11, 2024
95404f1
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 11, 2024
2945dec
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 11, 2024
886fb9e
BEE-52312 - forceSandBox - Hide command-launcher drop down from non-a…
jgarciacloudbees Nov 11, 2024
7f12293
JENKINS-74820 - Apply suggestions from code review
jgarciacloudbees Nov 12, 2024
905f8e8
BEE-52312 - change constructor signature to avoid breaking changes.
jgarciacloudbees Nov 12, 2024
5ba1477
BEE-52312 - bump pom version
jgarciacloudbees Nov 12, 2024
a70002a
JENKINS-74820 - Apply suggestions from code review
jgarciacloudbees Nov 12, 2024
ff5a9dc
BEE-52312 - add pom dependecies
jgarciacloudbees Nov 12, 2024
d5d3cba
BEE-52312 - add pom dependecies
jgarciacloudbees Nov 12, 2024
a1700c5
BEE-52312 - SuggestedChanges
jgarciacloudbees Nov 12, 2024
f31f604
BEE-52312 - SuggestedChanges - Test
jgarciacloudbees Nov 12, 2024
51eaba7
BEE-52312 - final changes
jgarciacloudbees Nov 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1367.vdf2fc45f229c</version>
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
Expand All @@ -68,5 +73,10 @@
<artifactId>test-harness</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>matrix-auth</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
74 changes: 70 additions & 4 deletions src/main/java/hudson/slaves/CommandLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@
*/
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;
import hudson.EnvVars;
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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -80,10 +85,11 @@
* @see #CommandLauncher(String command, EnvVars env)
*/
@DataBoundConstructor
public CommandLauncher(String command) {
public CommandLauncher(String command) throws Descriptor.FormException {
checkSandbox();
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
agentCommand = command;
env = null;
// TODO add withKey if we can determine the Slave.nodeName being configured

Check warning on line 92 in src/main/java/hudson/slaves/CommandLauncher.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: add withKey if we can determine the Slave.nodeName being configured
ScriptApproval.get().configuring(command, SystemCommandLanguage.get(), ApprovalContext.create().withCurrentUser(), Stapler.getCurrentRequest() == null);
}

Expand All @@ -94,17 +100,32 @@
* "sh -c" or write the expression into a script and point to the script)
* @param env environment variables for the launcher to include when it runs the command
*/
public CommandLauncher(String command, EnvVars env) {
this.agentCommand = command;
public CommandLauncher(String command, EnvVars env) throws Descriptor.FormException {
checkSandbox();
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
this.agentCommand = command;
this.env = env;
ScriptApproval.get().preapprove(command, SystemCommandLanguage.get());
}

/** Constructor for use from {@link CommandConnector}. Never approves the script. */
/** Constructor for use from {@link CommandConnector}. Never approves the script.
* We don't execute the {@link #checkSandbox()} for backward compatibility, as this is just for running the Scripts
*/
CommandLauncher(EnvVars env, String command) {
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", "sandbox");
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
}
}

private Object readResolve() {
ScriptApproval.get().configuring(agentCommand, SystemCommandLanguage.get(), ApprovalContext.create(), true);
Expand Down Expand Up @@ -250,4 +271,49 @@
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() ||

Check warning on line 285 in src/main/java/hudson/slaves/CommandLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 285 is only partially covered, one branch is missing
(context instanceof DumbSlave && ((DumbSlave) context).getLauncher() instanceof CommandLauncher);
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
}
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() {
boolean isCreating = false;

if (Stapler.getCurrentRequest() != null) {

Check warning on line 305 in src/main/java/hudson/slaves/CommandLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 305 is only partially covered, one branch is missing
List<Ancestor> ancs = Stapler.getCurrentRequest().getAncestors();
for (Ancestor anc : ancs) {
if (!isCreating && anc.getObject() instanceof ComputerSet) {

Check warning on line 308 in src/main/java/hudson/slaves/CommandLauncher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 308 is only partially covered, one branch is missing
String uri = Stapler.getCurrentRequest().getOriginalRequestURI();
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
if (uri.endsWith("createItem")) {
isCreating = true;
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
return isCreating;
}
}
}
206 changes: 206 additions & 0 deletions src/test/java/hudson/slaves/CommandLauncherForceSandboxTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package hudson.slaves;

import java.io.IOException;

import org.htmlunit.html.HtmlForm;
import org.jenkinsci.plugins.matrixauth.AuthorizationType;
import org.jenkinsci.plugins.matrixauth.PermissionEntry;
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 hudson.model.Descriptor;
import hudson.model.User;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.GlobalMatrixAuthorizationStrategy;
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());

PermissionEntry adminPermission = new PermissionEntry(AuthorizationType.USER, "admin");
PermissionEntry develPermission = new PermissionEntry(AuthorizationType.USER, "devel");

GlobalMatrixAuthorizationStrategy strategy = new GlobalMatrixAuthorizationStrategy();
strategy.add(Jenkins.ADMINISTER, adminPermission);
strategy.add(Jenkins.MANAGE, adminPermission);
strategy.add(Jenkins.READ, adminPermission);
strategy.add(Jenkins.MANAGE, develPermission);
strategy.add(Jenkins.READ, develPermission);
SlaveComputer.PERMISSIONS.getPermissions().forEach(p -> strategy.add(p,develPermission));
j.jenkins.setAuthorizationStrategy(strategy);
jgarciacloudbees marked this conversation as resolved.
Show resolved Hide resolved
}

@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());
}
}
}
Loading