Skip to content

Commit

Permalink
Merge pull request #222 from svanoort/lazy-load-fixToGetOrNull-JENKIN…
Browse files Browse the repository at this point in the history
…S-50784

Lazy load fix to get or null [JENKINS-50784]
  • Loading branch information
svanoort committed Apr 20, 2018
2 parents 56f3fde + 25bb2f5 commit 9a54fd2
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 18 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -141,7 +141,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
<version>2.18</version>
<version>2.20</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Expand Up @@ -1776,7 +1776,7 @@ public void autopersist(@Nonnull FlowNode n) throws IOException {
if (run instanceof FlowExecutionOwner.Executable) {
FlowExecutionOwner owner = ((FlowExecutionOwner.Executable) run).asFlowExecutionOwner();
if (owner != null) {
FlowExecution exec = owner.getOrNull();
FlowExecution exec = owner.get();
if (exec instanceof CpsFlowExecution) {
Map<String, Long> timings = ((CpsFlowExecution) exec).timings;
if (timings != null) {
Expand Down
Expand Up @@ -106,7 +106,8 @@ private ReplayAction(Run run) {
return isEnabled() || isRebuildEnabled() ? "replay" : null;
}

private @CheckForNull CpsFlowExecution getExecution() {
/** Poke for an execution without blocking - may be null if run is very fresh or has not lazy-loaded yet. */
private @CheckForNull CpsFlowExecution getExecutionLazy() {
FlowExecutionOwner owner = ((FlowExecutionOwner.Executable) run).asFlowExecutionOwner();
if (owner == null) {
return null;
Expand All @@ -115,6 +116,21 @@ private ReplayAction(Run run) {
return exec instanceof CpsFlowExecution ? (CpsFlowExecution) exec : null;
}

/** Fetches execution, blocking if needed while we wait for some of the loading process. */
private @CheckForNull CpsFlowExecution getExecutionBlocking() {
FlowExecutionOwner owner = ((FlowExecutionOwner.Executable) run).asFlowExecutionOwner();
if (owner == null) {
return null;
}
try {
FlowExecution exec = owner.get();
return exec instanceof CpsFlowExecution ? (CpsFlowExecution) exec : null;
} catch (IOException ioe) {
LOGGER.log(Level.WARNING, "Error fetching execution for replay", ioe);
}
return null;
}

/* accessible to Jelly */ public boolean isRebuildEnabled() {
if (!run.hasPermission(Item.BUILD)) {
return false;
Expand All @@ -123,7 +139,7 @@ private ReplayAction(Run run) {
return false;
}

return getExecution() != null;
return true;
}

/* accessible to Jelly */ public boolean isEnabled() {
Expand All @@ -135,27 +151,38 @@ private ReplayAction(Run run) {
return false;
}

CpsFlowExecution exec = getExecution();
if (exec == null) {
return false;
CpsFlowExecution exec = getExecutionLazy();
if (exec != null) {
return exec.isSandbox() || Jenkins.getActiveInstance().hasPermission(Jenkins.RUN_SCRIPTS); // We have to check for ADMIN because un-sandboxed code can execute arbitrary on-master code
} else {
// If the execution hasn't been lazy-loaded then we will wait to do deeper checks until someone tries to lazy load
// OR until isReplayableSandboxTest is invoked b/c they actually try to replay the build
return true;
}
if (exec.isSandbox()) {
}

/** Runs the extra tests for replayability beyond {@link #isEnabled()} that require a blocking load of the execution. */
/* accessible to Jelly */ public boolean isReplayableSandboxTest() {
CpsFlowExecution exec = getExecutionBlocking();
if (exec != null) {
if (!exec.isSandbox()) {
// We have to check for ADMIN because un-sandboxed code can execute arbitrary on-master code
return Jenkins.getActiveInstance().hasPermission(Jenkins.RUN_SCRIPTS);
}
return true;
} else {
// Whole-script approval mode. Can we submit an arbitrary script right here?
return Jenkins.getActiveInstance().hasPermission(Jenkins.RUN_SCRIPTS);
}
return false;
}

/** @see CpsFlowExecution#getScript */
/* accessible to Jelly */ public String getOriginalScript() {
CpsFlowExecution execution = getExecution();
CpsFlowExecution execution = getExecutionBlocking();
return execution != null ? execution.getScript() : "???";
}

/** @see CpsFlowExecution#getLoadedScripts */
/* accessible to Jelly */ public Map<String,String> getOriginalLoadedScripts() {
CpsFlowExecution execution = getExecution();
CpsFlowExecution execution = getExecutionBlocking();
if (execution == null) { // ?
return Collections.<String,String>emptyMap();
}
Expand All @@ -173,7 +200,7 @@ private ReplayAction(Run run) {
@Restricted(DoNotUse.class)
@RequirePOST
public void doRun(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
if (!isEnabled()) {
if (!isEnabled() || !(isReplayableSandboxTest())) {
throw new AccessDeniedException("not allowed to replay"); // AccessDeniedException2 requires us to look up the specific Permission
}
JSONObject form = req.getSubmittedForm();
Expand Down Expand Up @@ -228,7 +255,7 @@ public void doRebuild(StaplerRequest req, StaplerResponse rsp) throws ServletExc
*/
public @CheckForNull Queue.Item run2(@Nonnull String replacementMainScript, @Nonnull Map<String,String> replacementLoadedScripts) {
List<Action> actions = new ArrayList<Action>();
CpsFlowExecution execution = getExecution();
CpsFlowExecution execution = getExecutionBlocking();
if (execution == null) {
return null;
}
Expand Down
Expand Up @@ -67,7 +67,7 @@
// which currently has no protected method allowing getItemByFullName to be replaced.
throw new AbortException("Not a Pipeline build");
}
if (!action.isEnabled()) {
if (!action.isEnabled() || !action.isReplayableSandboxTest()) {
throw new AbortException("Not authorized to replay builds of this job");
}
String text = IOUtils.toString(stdin);
Expand Down
Expand Up @@ -10,7 +10,7 @@
<j:out value="${%blurb}"/>
</p>
<j:choose>
<j:when test="${it.enabled}">
<j:when test="${it.enabled and it.replayableSandboxTest}">
<f:form action="run" method="POST" name="config">
<f:entry field="mainScript" title="Main Script">
<wfe:workflow-editor script="${it.originalScript}" checkUrl="${rootURL}/${it.owner.url}${it.urlName}/checkScript" checkDependsOn=""/>
Expand Down
Expand Up @@ -57,8 +57,9 @@
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
import static org.junit.Assert.*;

import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.model.Statement;
Expand Down Expand Up @@ -133,6 +134,49 @@ public class ReplayActionTest {
});
}

@Issue("JENKINS-50784")
@Test public void lazyLoadExecutionStillReplayable() throws Exception {
story.then( r-> {
WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
WorkflowJob p2 = r.jenkins.createProject(WorkflowJob.class, "p2");
p.setDefinition(new CpsFlowDefinition("echo 'I did a thing'", false));
p2.setDefinition(new CpsFlowDefinition("echo 'I did a thing'", true));
// Start off with a simple run of the first script.
r.buildAndAssertSuccess(p);
r.buildAndAssertSuccess(p2);

r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
GlobalMatrixAuthorizationStrategy gmas = new GlobalMatrixAuthorizationStrategy();
gmas.add(Jenkins.RUN_SCRIPTS, "admin");
gmas.add(Jenkins.ADMINISTER, "admin");
gmas.add(ReplayAction.REPLAY, "normal");
r.jenkins.setAuthorizationStrategy(gmas);
});
story.then( r-> {
WorkflowJob job = r.jenkins.getItemByFullName("p", WorkflowJob.class);
WorkflowJob job2 = r.jenkins.getItemByFullName("p2", WorkflowJob.class);
WorkflowRun run = job.getLastBuild();
WorkflowRun run2 = job2.getLastBuild();

JenkinsRule.WebClient wc = r.createWebClient();
Assert.assertNull(run.asFlowExecutionOwner().getOrNull());
Assert.assertTrue(canReplay(run, "admin"));
Assert.assertTrue(canReplay(run, "normal"));
Assert.assertTrue(canRebuild(run, "admin"));
Assert.assertNull(run.asFlowExecutionOwner().getOrNull());

// After lazy-load we can do deeper checks easily, and the deep test triggers a full load of the execution
Assert.assertTrue(canReplayDeepTest(run, "admin"));
Assert.assertTrue(canReplayDeepTest(run2, "normal"));

Assert.assertNotNull(run.asFlowExecutionOwner().getOrNull());
Assert.assertTrue(canReplay(run, "admin"));
Assert.assertFalse(canReplay(run, "normal")); // Now we know to check if the user can run outside sandbox, and they can't
Assert.assertTrue(canReplay(run2, "normal")); // We can still run stuff inside sandbox
Assert.assertTrue(canRebuild(run, "admin"));
});
}

@Initializer(after=InitMilestone.EXTENSIONS_AUGMENTED, before=InitMilestone.JOB_LOADED) // same time as Jenkins global config is loaded (e.g., AuthorizationStrategy)
public static void assertPermissionId() {
String thePermissionId = "hudson.model.Run.Replay";
Expand Down Expand Up @@ -196,6 +240,15 @@ private static boolean canReplay(WorkflowRun b, String user) {
});
}

private static boolean canReplayDeepTest(WorkflowRun b, String user) {
final ReplayAction a = b.getAction(ReplayAction.class);
return ACL.impersonate(User.get(user).impersonate(), new NotReallyRoleSensitiveCallable<Boolean,RuntimeException>() {
@Override public Boolean call() throws RuntimeException {
return a.isReplayableSandboxTest();
}
});
}

private static boolean canRebuild(WorkflowRun b, String user) {
final ReplayAction a = b.getAction(ReplayAction.class);
return ACL.impersonate(User.get(user).impersonate(), new NotReallyRoleSensitiveCallable<Boolean,RuntimeException>() {
Expand Down

0 comments on commit 9a54fd2

Please sign in to comment.