mirror of https://github.com/jenkinsci/jenkins.git
[JENKINS-73161] Ensure file parameters are retained across Jenkins restarts (#11081)
This commit is contained in:
commit
349d4fc407
|
@ -26,9 +26,11 @@ package hudson.model;
|
|||
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import hudson.EnvVars;
|
||||
import hudson.Extension;
|
||||
import hudson.FilePath;
|
||||
import hudson.Launcher;
|
||||
import hudson.Util;
|
||||
import hudson.model.queue.QueueListener;
|
||||
import hudson.tasks.BuildWrapper;
|
||||
import hudson.util.VariableResolver;
|
||||
import java.io.File;
|
||||
|
@ -39,12 +41,17 @@ import java.io.UnsupportedEncodingException;
|
|||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.util.SystemProperties;
|
||||
import org.apache.commons.fileupload2.core.FileItem;
|
||||
import org.apache.commons.fileupload2.core.FileItemFactory;
|
||||
import org.apache.commons.fileupload2.core.FileItemHeaders;
|
||||
import org.apache.commons.fileupload2.core.FileItemHeadersProvider;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
|
@ -58,6 +65,9 @@ import org.kohsuke.stapler.StaplerResponse2;
|
|||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public class FileParameterValue extends ParameterValue {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(FileParameterValue.class.getName());
|
||||
|
||||
private static final String FOLDER_NAME = "fileParameters";
|
||||
private static final Pattern PROHIBITED_DOUBLE_DOT = Pattern.compile(".*[\\\\/]\\.\\.[\\\\/].*");
|
||||
private static final long serialVersionUID = -143427023159076073L;
|
||||
|
@ -71,13 +81,16 @@ public class FileParameterValue extends ParameterValue {
|
|||
public static /* Script Console modifiable */ boolean ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE =
|
||||
SystemProperties.getBoolean(FileParameterValue.class.getName() + ".allowFolderTraversalOutsideWorkspace");
|
||||
|
||||
private final transient FileItem file;
|
||||
private transient FileItem file;
|
||||
|
||||
/**
|
||||
* The name of the originally uploaded file.
|
||||
*/
|
||||
private final String originalFileName;
|
||||
|
||||
|
||||
private String tmpFileName;
|
||||
|
||||
/**
|
||||
* Overrides the location in the build to place this file. Initially set to {@link #getName()}
|
||||
* The location could be directly the filename or also a hierarchical path.
|
||||
|
@ -106,6 +119,16 @@ public class FileParameterValue extends ParameterValue {
|
|||
|
||||
protected FileParameterValue(String name, FileItem file, String originalFileName) {
|
||||
super(name);
|
||||
try {
|
||||
File dir = new File(Jenkins.get().getRootDir(), "fileParameterValueFiles");
|
||||
Files.createDirectories(dir.toPath());
|
||||
File tmp = Files.createTempFile(dir.toPath(), "jenkins", ".tmp").toFile();
|
||||
FileUtils.copyInputStreamToFile(file.getInputStream(), tmp);
|
||||
tmpFileName = tmp.getAbsolutePath();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
this.file = file;
|
||||
this.originalFileName = originalFileName;
|
||||
setLocation(name);
|
||||
|
@ -149,7 +172,17 @@ public class FileParameterValue extends ParameterValue {
|
|||
return originalFileName;
|
||||
}
|
||||
|
||||
private void createFile() {
|
||||
if (file == null && tmpFileName != null) {
|
||||
File tmp = new File(tmpFileName);
|
||||
if (tmp.exists()) {
|
||||
file = new FileItemImpl2(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FileItem getFile2() {
|
||||
createFile();
|
||||
return file;
|
||||
}
|
||||
|
||||
|
@ -163,31 +196,44 @@ public class FileParameterValue extends ParameterValue {
|
|||
|
||||
@Override
|
||||
public BuildWrapper createBuildWrapper(AbstractBuild<?, ?> build) {
|
||||
createFile();
|
||||
return new BuildWrapper() {
|
||||
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "TODO needs triage")
|
||||
@SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", "PATH_TRAVERSAL_IN"}, justification = "TODO needs triage, False positive, the path is a temporary file")
|
||||
@Override
|
||||
public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
|
||||
if (location != null && !location.isEmpty() && file.getName() != null && !file.getName().isEmpty()) {
|
||||
listener.getLogger().println("Copying file to " + location);
|
||||
FilePath ws = build.getWorkspace();
|
||||
if (ws == null) {
|
||||
throw new IllegalStateException("The workspace should be created when setUp method is called");
|
||||
}
|
||||
if (!ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE && (PROHIBITED_DOUBLE_DOT.matcher(location).matches() || !ws.isDescendant(location))) {
|
||||
listener.error("Rejecting file path escaping base directory with relative path: " + location);
|
||||
// force the build to fail
|
||||
return null;
|
||||
}
|
||||
FilePath locationFilePath = ws.child(location);
|
||||
locationFilePath.getParent().mkdirs();
|
||||
if (location != null && !location.isBlank() && file != null && file.getName() != null && !file.getName().isBlank()) {
|
||||
try {
|
||||
listener.getLogger().println("Copying file to " + location);
|
||||
FilePath ws = build.getWorkspace();
|
||||
if (ws == null) {
|
||||
throw new IllegalStateException("The workspace should be created when setUp method is called");
|
||||
}
|
||||
if (!ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE && (PROHIBITED_DOUBLE_DOT.matcher(location).matches() || !ws.isDescendant(location))) {
|
||||
listener.error("Rejecting file path escaping base directory with relative path: " + location);
|
||||
// force the build to fail
|
||||
return null;
|
||||
}
|
||||
FilePath locationFilePath = ws.child(location);
|
||||
locationFilePath.getParent().mkdirs();
|
||||
|
||||
// TODO Remove this workaround after FILEUPLOAD-293 is resolved.
|
||||
if (locationFilePath.exists() && !locationFilePath.isDirectory()) {
|
||||
locationFilePath.delete();
|
||||
// TODO Remove this workaround after FILEUPLOAD-293 is resolved.
|
||||
if (locationFilePath.exists() && !locationFilePath.isDirectory()) {
|
||||
locationFilePath.delete();
|
||||
}
|
||||
locationFilePath.copyFrom(file);
|
||||
locationFilePath.copyTo(new FilePath(getLocationUnderBuild(build)));
|
||||
} finally {
|
||||
if (tmpFileName != null) {
|
||||
File tmp = new File(tmpFileName);
|
||||
try {
|
||||
Files.deleteIfExists(tmp.toPath());
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.WARNING, "Unable to delete temporary file {0} for parameter {1} of job {2}",
|
||||
new Object[]{tmp.getAbsolutePath(), getName(), build.getParent().getName()});
|
||||
}
|
||||
}
|
||||
tmpFileName = null;
|
||||
}
|
||||
|
||||
locationFilePath.copyFrom(file);
|
||||
locationFilePath.copyTo(new FilePath(getLocationUnderBuild(build)));
|
||||
}
|
||||
return new Environment() {};
|
||||
}
|
||||
|
@ -257,6 +303,36 @@ public class FileParameterValue extends ParameterValue {
|
|||
return new File(build.getRootDir(), FOLDER_NAME);
|
||||
}
|
||||
|
||||
@Extension
|
||||
public static class CancelledQueueListener extends QueueListener {
|
||||
|
||||
@Override
|
||||
public void onLeft(Queue.LeftItem li) {
|
||||
if (li.isCancelled()) {
|
||||
List<ParametersAction> actions = li.getActions(ParametersAction.class);
|
||||
actions.forEach(a -> {
|
||||
a.getAllParameters().stream()
|
||||
.filter(p -> p instanceof FileParameterValue)
|
||||
.map(p -> (FileParameterValue) p)
|
||||
.forEach(this::deleteTmpFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "False positive, the path is a temporary file")
|
||||
private void deleteTmpFile(FileParameterValue p) {
|
||||
if (p.tmpFileName != null) {
|
||||
File tmp = new File(p.tmpFileName);
|
||||
try {
|
||||
Files.deleteIfExists(tmp.toPath());
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.WARNING, "Unable to delete temporary file {0} for parameter {1}",
|
||||
new Object[]{tmp.getAbsolutePath(), p.getName()});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation from {@link File}.
|
||||
*
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2014 Oleg Nenashev <o.v.nenashev@gmail.com>.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package hudson.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import java.io.File;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
|
||||
/**
|
||||
* Test for {@link FileParameterValue}.
|
||||
* @author Oleg Nenashev
|
||||
*/
|
||||
class FileParameterValueTest {
|
||||
|
||||
@Issue("JENKINS-19017")
|
||||
@Test
|
||||
void compareParamsWithSameName() {
|
||||
final String paramName = "MY_FILE_PARAM"; // Same paramName (location) reproduces the bug
|
||||
final FileParameterValue param1 = new FileParameterValue(paramName, new File("ws_param1.txt"), "param1.txt");
|
||||
final FileParameterValue param2 = new FileParameterValue(paramName, new File("ws_param2.txt"), "param2.txt");
|
||||
|
||||
assertNotEquals(param1, param2, "Files with same locations should be considered as different");
|
||||
assertNotEquals(param2, param1, "Files with same locations should be considered as different");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareNullParams() {
|
||||
final String paramName = "MY_FILE_PARAM";
|
||||
FileParameterValue nonNullParam = new FileParameterValue(paramName, new File("ws_param1.txt"), "param1.txt");
|
||||
FileParameterValue nullParam1 = new FileParameterValue(null, new File("null_param1.txt"), "null_param1.txt");
|
||||
FileParameterValue nullParam2 = new FileParameterValue(null, new File("null_param2.txt"), "null_param2.txt");
|
||||
|
||||
// Combine nulls
|
||||
assertEquals(nullParam1, nullParam1);
|
||||
assertEquals(nullParam1, nullParam2);
|
||||
assertEquals(nullParam2, nullParam1);
|
||||
assertEquals(nullParam2, nullParam2);
|
||||
|
||||
// Compare with non-null
|
||||
assertNotEquals(nullParam1, nonNullParam);
|
||||
assertNotEquals(nonNullParam, nullParam1);
|
||||
}
|
||||
}
|
|
@ -5,21 +5,34 @@ import static org.hamcrest.Matchers.containsString;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import hudson.Functions;
|
||||
import hudson.model.queue.CauseOfBlockage;
|
||||
import hudson.model.queue.QueueTaskDispatcher;
|
||||
import hudson.tasks.BatchFile;
|
||||
import hudson.tasks.Shell;
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.htmlunit.FormEncodingType;
|
||||
import org.htmlunit.HttpMethod;
|
||||
import org.htmlunit.WebRequest;
|
||||
import org.htmlunit.html.HtmlForm;
|
||||
import org.htmlunit.html.HtmlInput;
|
||||
import org.htmlunit.html.HtmlPage;
|
||||
import org.htmlunit.util.KeyDataPair;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.MockAuthorizationStrategy;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
import org.jvnet.hudson.test.junit.jupiter.JenkinsSessionExtension;
|
||||
|
||||
class FileParameterValuePersistenceTest {
|
||||
|
@ -78,4 +91,39 @@ class FileParameterValuePersistenceTest {
|
|||
assertThat(page.getWebResponse().getContentAsString(), containsString(FILENAME));
|
||||
}
|
||||
}
|
||||
|
||||
@Issue("JENKINS-73161")
|
||||
@Test
|
||||
void fileParameterValueIsRetained() throws Throwable {
|
||||
sessions.then(r -> {
|
||||
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
|
||||
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin"));
|
||||
FreeStyleProject p = r.createFreeStyleProject("p");
|
||||
p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition("FILE")));
|
||||
WebRequest req = new WebRequest(new URL(r.getURL() + "job/p/buildWithParameters"), HttpMethod.POST);
|
||||
File f = File.createTempFile("junit", null, tmp);
|
||||
FileUtils.write(f, "uploaded content here", "UTF-8");
|
||||
req.setEncodingType(FormEncodingType.MULTIPART);
|
||||
req.setRequestParameters(Collections.singletonList(new KeyDataPair("FILE", f, "myfile.txt", "text/plain", "UTF-8")));
|
||||
r.createWebClient().withBasicApiToken("admin").getPage(req);
|
||||
});
|
||||
sessions.then(r -> {
|
||||
ExtensionList.lookupSingleton(Block.class).ready = true;
|
||||
FreeStyleProject p = r.jenkins.getItemByFullName("p", FreeStyleProject.class);
|
||||
r.waitUntilNoActivity();
|
||||
FreeStyleBuild b = p.getBuildByNumber(1);
|
||||
r.assertBuildStatusSuccess(b);
|
||||
});
|
||||
}
|
||||
|
||||
@TestExtension("fileParameterValueIsRetained")
|
||||
public static final class Block extends QueueTaskDispatcher {
|
||||
private boolean ready;
|
||||
|
||||
@Override
|
||||
public CauseOfBlockage canTake(Node node, Queue.BuildableItem item) {
|
||||
return ready ? null : new CauseOfBlockage.BecauseNodeIsBusy(node);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,16 +31,19 @@ import static org.hamcrest.Matchers.equalTo;
|
|||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import hudson.FilePath;
|
||||
import hudson.Functions;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.htmlunit.Page;
|
||||
import org.htmlunit.html.HtmlPage;
|
||||
import org.htmlunit.util.NameValuePair;
|
||||
|
@ -427,4 +430,45 @@ class FileParameterValueTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Issue("JENKINS-19017")
|
||||
@Test
|
||||
void compareParamsWithSameName() throws IOException {
|
||||
final String paramName = "MY_FILE_PARAM"; // Same paramName (location) reproduces the bug
|
||||
File ws_param1 = createParamFile("ws_param1.txt");
|
||||
File ws_param2 = createParamFile("ws_param2.txt");
|
||||
|
||||
final FileParameterValue param1 = new FileParameterValue(paramName, ws_param1, "param1.txt");
|
||||
final FileParameterValue param2 = new FileParameterValue(paramName, ws_param2, "param2.txt");
|
||||
|
||||
assertNotEquals(param1, param2, "Files with same locations should be considered as different");
|
||||
assertNotEquals(param2, param1, "Files with same locations should be considered as different");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareNullParams() throws IOException {
|
||||
final String paramName = "MY_FILE_PARAM";
|
||||
File ws_param1 = createParamFile("ws_param1.txt");
|
||||
File null_param1 = createParamFile("null_param1.txt");
|
||||
File null_param2 = createParamFile("null_param2.txt");
|
||||
FileParameterValue nonNullParam = new FileParameterValue(paramName, ws_param1, "param1.txt");
|
||||
FileParameterValue nullParam1 = new FileParameterValue(null, null_param1, "null_param1.txt");
|
||||
FileParameterValue nullParam2 = new FileParameterValue(null, null_param2, "null_param2.txt");
|
||||
|
||||
// Combine nulls
|
||||
assertEquals(nullParam1, nullParam1);
|
||||
assertEquals(nullParam1, nullParam2);
|
||||
assertEquals(nullParam2, nullParam1);
|
||||
assertEquals(nullParam2, nullParam2);
|
||||
|
||||
// Compare with non-null
|
||||
assertNotEquals(nullParam1, nonNullParam);
|
||||
assertNotEquals(nonNullParam, nullParam1);
|
||||
}
|
||||
|
||||
private File createParamFile(String fileName) throws IOException {
|
||||
File f = new File(tmp, fileName);
|
||||
FileUtils.writeStringToFile(f, "content", StandardCharsets.UTF_8);
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue