diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java index af8362eeb9..b2d61ead56 100644 --- a/core/src/main/java/hudson/EnvVars.java +++ b/core/src/main/java/hudson/EnvVars.java @@ -23,12 +23,12 @@ */ package hudson; -import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; import hudson.util.CaseInsensitiveComparator; import hudson.util.CyclicGraphDetector; import hudson.util.CyclicGraphDetector.CycleDetectedException; import hudson.util.VariableResolver; +import jenkins.security.MasterToSlaveCallable; import java.io.File; import java.io.IOException; @@ -404,7 +404,7 @@ public class EnvVars extends TreeMap { return channel.call(new GetEnvVars()); } - private static final class GetEnvVars implements Callable { + private static final class GetEnvVars extends MasterToSlaveCallable { public EnvVars call() { return new EnvVars(EnvVars.masterEnvVars); } diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index e42035bd4f..1434920557 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -112,6 +112,13 @@ import java.util.regex.Pattern; import static hudson.FilePath.TarCompression.*; import static hudson.Util.*; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import jenkins.FilePathFilter; +import jenkins.MasterToSlaveFileCallable; +import jenkins.SlaveToMasterFileCallable; +import jenkins.security.MasterToSlaveCallable; +import org.jenkinsci.remoting.RoleChecker; +import org.jenkinsci.remoting.RoleSensitive; /** * {@link File} like object with remoting support. @@ -197,6 +204,17 @@ public final class FilePath implements Serializable { // since the platform of the slave might be different, can't use java.io.File private final String remote; + /** + * If this {@link FilePath} is deserialized to handle file access request from a remote computer, + * this field is set to the filter that performs access control. + * + *

+ * If null, no access control is needed. + * + * @see #filterNonNull() + */ + private transient @Nullable FilePathFilter filter; + /** * Creates a {@link FilePath} that represents a path on the given node. * @@ -423,11 +441,11 @@ public final class FilePath implements Serializable { */ public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException { final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os; - return act(new FileCallable() { + return act(new SecureFileCallable() { public Integer invoke(File f, VirtualChannel channel) throws IOException { Archiver a = factory.create(out); try { - scanner.scan(f,a); + scanner.scan(f,reading(a)); } finally { a.close(); } @@ -455,13 +473,13 @@ public final class FilePath implements Serializable { * @see #unzipFrom(InputStream) */ public void unzip(final FilePath target) throws IOException, InterruptedException { - target.act(new FileCallable() { + target.act(new SecureFileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException { + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { if (FilePath.this.isRemote()) unzip(dir, FilePath.this.read()); // use streams else - unzip(dir, new File(FilePath.this.getRemote())); // shortcut to local file + unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file return null; } private static final long serialVersionUID = 1L; @@ -479,8 +497,8 @@ public final class FilePath implements Serializable { * @see #untarFrom(InputStream, TarCompression) */ public void untar(final FilePath target, final TarCompression compression) throws IOException, InterruptedException { - target.act(new FileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException { + target.act(new SecureFileCallable() { + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { readFromTar(FilePath.this.getName(),dir,compression.extract(FilePath.this.read())); return null; } @@ -498,7 +516,7 @@ public final class FilePath implements Serializable { */ public void unzipFrom(InputStream _in) throws IOException, InterruptedException { final InputStream in = new RemoteInputStream(_in); - act(new FileCallable() { + act(new SecureFileCallable() { public Void invoke(File dir, VirtualChannel channel) throws IOException { unzip(dir, in); return null; @@ -507,7 +525,7 @@ public final class FilePath implements Serializable { }); } - private static void unzip(File dir, InputStream in) throws IOException { + private void unzip(File dir, InputStream in) throws IOException { File tmpFile = File.createTempFile("tmpzip", null); // uses java.io.tmpdir try { // TODO why does this not simply use ZipInputStream? @@ -519,7 +537,7 @@ public final class FilePath implements Serializable { } } - static private void unzip(File dir, File zipFile) throws IOException { + private void unzip(File dir, File zipFile) throws IOException { dir = dir.getAbsoluteFile(); // without absolutization, getParentFile below seems to fail ZipFile zip = new ZipFile(zipFile); @SuppressWarnings("unchecked") @@ -530,15 +548,15 @@ public final class FilePath implements Serializable { ZipEntry e = entries.nextElement(); File f = new File(dir, e.getName()); if (e.isDirectory()) { - f.mkdirs(); + mkdirs(f); } else { File p = f.getParentFile(); if (p != null) { - p.mkdirs(); + mkdirs(p); } InputStream input = zip.getInputStream(e); try { - IOUtils.copy(input, f); + IOUtils.copy(input, writing(f)); } finally { input.close(); } @@ -562,7 +580,7 @@ public final class FilePath implements Serializable { * Absolutizes this {@link FilePath} and returns the new one. */ public FilePath absolutize() throws IOException, InterruptedException { - return new FilePath(channel,act(new FileCallable() { + return new FilePath(channel,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File f, VirtualChannel channel) throws IOException { return f.getAbsolutePath(); @@ -580,9 +598,10 @@ public final class FilePath implements Serializable { * @since 1.456 */ public void symlinkTo(final String target, final TaskListener listener) throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + writing(f); Util.createSymlink(f.getParentFile(),target,f.getName(),listener); return null; } @@ -597,10 +616,10 @@ public final class FilePath implements Serializable { * @since 1.456 */ public String readLink() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - return Util.resolveSymlink(f); + return Util.resolveSymlink(reading(f)); } }); } @@ -666,7 +685,7 @@ public final class FilePath implements Serializable { public void untarFrom(InputStream _in, final TarCompression compression) throws IOException, InterruptedException { try { final InputStream in = new RemoteInputStream(_in); - act(new FileCallable() { + act(new SecureFileCallable() { public Void invoke(File dir, VirtualChannel channel) throws IOException { readFromTar("input stream",dir, compression.extract(in)); return null; @@ -776,7 +795,8 @@ public final class FilePath implements Serializable { } } - private static final class Unpack implements FileCallable { + // this reads from arbitrary URL + private final class Unpack extends MasterToSlaveFileCallable { private final URL archive; Unpack(URL archive) { this.archive = archive; @@ -845,7 +865,7 @@ public final class FilePath implements Serializable { public void copyFrom(FileItem file) throws IOException, InterruptedException { if(channel==null) { try { - file.write(new File(remote)); + file.write(writing(new File(remote))); } catch (IOException e) { throw e; } catch (Exception e) { @@ -870,9 +890,14 @@ public final class FilePath implements Serializable { * Code that gets executed on the machine where the {@link FilePath} is local. * Used to act on {@link FilePath}. * Warning: implementations must be serializable, so prefer a static nested class to an inner class. + * + *

+ * Subtypes would likely want to extend from either {@link MasterToSlaveCallable} + * or {@link SlaveToMasterFileCallable}. + * * @see FilePath#act(FileCallable) */ - public interface FileCallable extends Serializable { + public interface FileCallable extends Serializable, RoleSensitive { /** * Performs the computational task on the node where the data is located. * @@ -888,6 +913,15 @@ public final class FilePath implements Serializable { T invoke(File f, VirtualChannel channel) throws IOException, InterruptedException; } + /** + * {@link FileCallable}s that can be executed anywhere, including the master. + * + * The code is the same as {@link SlaveToMasterFileCallable}, but used as a marker to + * designate those impls that use {@link FilePathFilter}. + */ + /*package*/ static abstract class SecureFileCallable extends SlaveToMasterFileCallable { + } + /** * Executes some program on the machine that this {@link FilePath} exists, * so that one can perform local file operations. @@ -1023,6 +1057,12 @@ public final class FilePath implements Serializable { throw (IOException)new InterruptedIOException().initCause(e); } } + + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + task.checkRoles(checker); + } + private static final long serialVersionUID = 1L; }; } @@ -1032,7 +1072,7 @@ public final class FilePath implements Serializable { * on which this file is available. */ public URI toURI() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public URI invoke(File f, VirtualChannel channel) { return f.toURI(); @@ -1070,10 +1110,10 @@ public final class FilePath implements Serializable { * Creates this directory. */ public void mkdirs() throws IOException, InterruptedException { - if(!act(new FileCallable() { + if(!act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - if(f.mkdirs() || f.exists()) + if(mkdirs(f) || f.exists()) return true; // OK // following Ant task to avoid possible race condition. @@ -1089,10 +1129,10 @@ public final class FilePath implements Serializable { * Deletes this directory, including all its contents recursively. */ public void deleteRecursive() throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteRecursive(f); + deleteRecursive(deleting(f)); return null; } }); @@ -1102,15 +1142,38 @@ public final class FilePath implements Serializable { * Deletes all the contents of this directory, but not the directory itself */ public void deleteContents() throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteContentsRecursive(f); + deleteContentsRecursive(f); return null; } }); } + private void deleteRecursive(File dir) throws IOException { + if(!isSymlink(dir)) + deleteContentsRecursive(dir); + try { + deleteFile(deleting(dir)); + } catch (IOException e) { + // if some of the child directories are big, it might take long enough to delete that + // it allows others to create new files, causing problemsl ike JENKINS-10113 + // so give it one more attempt before we give up. + if(!isSymlink(dir)) + deleteContentsRecursive(dir); + deleteFile(deleting(dir)); + } + } + + private void deleteContentsRecursive(File file) throws IOException { + File[] files = file.listFiles(); + if(files==null) + return; // the directory didn't exist in the first place + for (File child : files) + deleteRecursive(child); + } + /** * Gets the file name portion except the extension. * @@ -1196,10 +1259,10 @@ public final class FilePath implements Serializable { */ public FilePath createTempFile(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new FileCallable() { + return new FilePath(this,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File dir, VirtualChannel channel) throws IOException { - File f = File.createTempFile(prefix, suffix, dir); + File f = writing(File.createTempFile(prefix, suffix, dir)); return f.getName(); } })); @@ -1252,22 +1315,22 @@ public final class FilePath implements Serializable { */ public FilePath createTextTempFile(final String prefix, final String suffix, final String contents, final boolean inThisDirectory) throws IOException, InterruptedException { try { - return new FilePath(channel,act(new FileCallable() { + return new FilePath(channel,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File dir, VirtualChannel channel) throws IOException { if(!inThisDirectory) dir = new File(System.getProperty("java.io.tmpdir")); else - dir.mkdirs(); + mkdirs(dir); File f; try { - f = File.createTempFile(prefix, suffix, dir); + f = creating(File.createTempFile(prefix, suffix, dir)); } catch (IOException e) { throw new IOException("Failed to create a temporary directory in "+dir,e); } - Writer w = new FileWriter(f); + Writer w = new FileWriter(writing(f)); try { w.write(contents); } finally { @@ -1298,7 +1361,7 @@ public final class FilePath implements Serializable { */ public FilePath createTempDir(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new FileCallable() { + return new FilePath(this,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File dir, VirtualChannel channel) throws IOException { File f = File.createTempFile(prefix, suffix, dir); @@ -1318,10 +1381,10 @@ public final class FilePath implements Serializable { * @return true, for a modicum of compatibility */ public boolean delete() throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteFile(f); + Util.deleteFile(deleting(f)); return null; } }); @@ -1332,10 +1395,10 @@ public final class FilePath implements Serializable { * Checks if the file exists. */ public boolean exists() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return f.exists(); + return stating(f).exists(); } }); } @@ -1348,10 +1411,10 @@ public final class FilePath implements Serializable { * @see #touch(long) */ public long lastModified() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.lastModified(); + return stating(f).lastModified(); } }); } @@ -1362,12 +1425,12 @@ public final class FilePath implements Serializable { * @since 1.299 */ public void touch(final long timestamp) throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = -5094638816500738429L; public Void invoke(File f, VirtualChannel channel) throws IOException { if(!f.exists()) - new FileOutputStream(f).close(); - if(!f.setLastModified(timestamp)) + new FileOutputStream(creating(f)).close(); + if(!stating(f).setLastModified(timestamp)) throw new IOException("Failed to set the timestamp of "+f+" to "+timestamp); return null; } @@ -1375,10 +1438,10 @@ public final class FilePath implements Serializable { } private void setLastModifiedIfPossible(final long timestamp) throws IOException, InterruptedException { - String message = act(new FileCallable() { + String message = act(new SecureFileCallable() { private static final long serialVersionUID = -828220335793641630L; public String invoke(File f, VirtualChannel channel) throws IOException { - if(!f.setLastModified(timestamp)) { + if(!writing(f).setLastModified(timestamp)) { if (Functions.isWindows()) { // On Windows this seems to fail often. See JENKINS-11073 // Therefore don't fail, but just log a warning @@ -1400,10 +1463,10 @@ public final class FilePath implements Serializable { * Checks if the file is a directory. */ public boolean isDirectory() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return f.isDirectory(); + return stating(f).isDirectory(); } }); } @@ -1414,10 +1477,10 @@ public final class FilePath implements Serializable { * @since 1.129 */ public long length() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.length(); + return stating(f).length(); } }); } @@ -1427,7 +1490,7 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getFreeDiskSpace() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; @Override public Long invoke(File f, VirtualChannel channel) throws IOException { return f.getFreeSpace(); @@ -1440,7 +1503,7 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getTotalDiskSpace() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; @Override public Long invoke(File f, VirtualChannel channel) throws IOException { return f.getTotalSpace(); @@ -1453,7 +1516,7 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getUsableDiskSpace() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; @Override public Long invoke(File f, VirtualChannel channel) throws IOException { return f.getUsableSpace(); @@ -1478,10 +1541,10 @@ public final class FilePath implements Serializable { */ public void chmod(final int mask) throws IOException, InterruptedException { if(!isUnix() || mask==-1) return; - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - _chmod(f, mask); + _chmod(writing(f), mask); return null; } @@ -1509,10 +1572,10 @@ public final class FilePath implements Serializable { */ public int mode() throws IOException, InterruptedException, PosixException { if(!isUnix()) return -1; - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Integer invoke(File f, VirtualChannel channel) throws IOException { - return IOUtils.mode(f); + return IOUtils.mode(stating(f)); } }); } @@ -1556,10 +1619,10 @@ public final class FilePath implements Serializable { if (filter != null && !(filter instanceof Serializable)) { throw new IllegalArgumentException("Non-serializable filter of " + filter.getClass()); } - return act(new FileCallable>() { + return act(new SecureFileCallable>() { private static final long serialVersionUID = 1L; public List invoke(File f, VirtualChannel channel) throws IOException { - File[] children = f.listFiles(filter); + File[] children = reading(f).listFiles(filter); if(children ==null) return null; ArrayList r = new ArrayList(children.length); @@ -1609,10 +1672,10 @@ public final class FilePath implements Serializable { * @since 1.465 */ public FilePath[] list(final String includes, final String excludes, final boolean defaultExcludes) throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public FilePath[] invoke(File f, VirtualChannel channel) throws IOException { - String[] files = glob(f, includes, excludes, defaultExcludes); + String[] files = glob(reading(f), includes, excludes, defaultExcludes); FilePath[] r = new FilePath[files.length]; for( int i=0; i() { + actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; - public Void call() throws IOException { - FileInputStream fis=null; + + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + FileInputStream fis = null; try { - fis = new FileInputStream(new File(remote)); - Util.copyStream(fis,p.getOut()); + fis = new FileInputStream(reading(f)); + Util.copyStream(fis, p.getOut()); return null; } finally { org.apache.commons.io.IOUtils.closeQuietly(fis); @@ -1668,7 +1733,7 @@ public final class FilePath implements Serializable { /** * Reads this file into a string, by using the current system encoding. */ - public String readToString() throws IOException { + public String readToString() throws IOException, InterruptedException { InputStream in = read(); try { return org.apache.commons.io.IOUtils.toString(in); @@ -1695,16 +1760,16 @@ public final class FilePath implements Serializable { public OutputStream write() throws IOException, InterruptedException { if(channel==null) { File f = new File(remote).getAbsoluteFile(); - f.getParentFile().mkdirs(); - return new FileOutputStream(f); + mkdirs(f.getParentFile()); + return new FileOutputStream(writing(f)); } - return channel.call(new Callable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; - public OutputStream call() throws IOException { - File f = new File(remote).getAbsoluteFile(); - f.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(f); + public OutputStream invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + f = f.getAbsoluteFile(); + mkdirs(f.getParentFile()); + FileOutputStream fos = new FileOutputStream(writing(f)); return new RemoteOutputStream(fos); } }); @@ -1718,11 +1783,11 @@ public final class FilePath implements Serializable { * @since 1.105 */ public void write(final String content, final String encoding) throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - f.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(f); + mkdirs(f.getParentFile()); + FileOutputStream fos = new FileOutputStream(writing(f)); Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos); try { w.write(content); @@ -1739,10 +1804,10 @@ public final class FilePath implements Serializable { * @see Util#getDigestOf(File) */ public String digest() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File f, VirtualChannel channel) throws IOException { - return Util.getDigestOf(f); + return Util.getDigestOf(reading(f)); } }); } @@ -1755,10 +1820,10 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("renameTo target must be on the same host"); } - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - f.renameTo(new File(target.remote)); + reading(f).renameTo(creating(new File(target.remote))); return null; } }); @@ -1773,7 +1838,7 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("pullUpTo target must be on the same host"); } - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { // JENKINS-16846: if f.getName() is the same as one of the files/directories in f, @@ -1784,12 +1849,12 @@ public final class FilePath implements Serializable { File t = new File(target.getRemote()); - for(File child : tmp.listFiles()) { + for(File child : reading(tmp).listFiles()) { File target = new File(t, child.getName()); - if(!child.renameTo(target)) + if(!stating(child).renameTo(creating(target))) throw new IOException("Failed to rename "+child+" to "+target); } - tmp.delete(); + deleting(tmp).delete(); return null; } }); @@ -1828,12 +1893,12 @@ public final class FilePath implements Serializable { public void copyTo(OutputStream os) throws IOException, InterruptedException { final OutputStream out = new RemoteOutputStream(os); - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 4088559042349254141L; public Void invoke(File f, VirtualChannel channel) throws IOException { FileInputStream fis = null; try { - fis = new FileInputStream(f); + fis = new FileInputStream(reading(f)); Util.copyStream(fis,out); return null; } finally { @@ -1875,21 +1940,6 @@ public final class FilePath implements Serializable { channel.syncLocalIO(); } - /** - * Remoting interface used for {@link FilePath#copyRecursiveTo(String, FilePath)}. - * - * TODO: this might not be the most efficient way to do the copy. - */ - interface RemoteCopier { - /** - * @param fileName - * relative path name to the output file. Path separator must be '/'. - */ - void open(String fileName) throws IOException; - void write(byte[] buf, int len) throws IOException; - void close() throws IOException; - } - /** * Copies the contents of this directory recursively into the specified target directory. * @@ -1944,35 +1994,41 @@ public final class FilePath implements Serializable { public int copyRecursiveTo(final DirScanner scanner, final FilePath target, final String description) throws IOException, InterruptedException { if(this.channel==target.channel) { // local to local copy. - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Integer invoke(File base, VirtualChannel channel) throws IOException { if(!base.exists()) return 0; assert target.channel==null; final File dest = new File(target.remote); final AtomicInteger count = new AtomicInteger(); - scanner.scan(base, new FileVisitor() { - @Override public void visit(File f, String relativePath) throws IOException { + scanner.scan(base, reading(new FileVisitor() { + @Override + public void visit(File f, String relativePath) throws IOException { if (f.isFile()) { File target = new File(dest, relativePath); - IOUtils.mkdirs(target.getParentFile()); - Util.copyFile(f, target); + mkdirsE(target.getParentFile()); + Util.copyFile(f, writing(target)); count.incrementAndGet(); } } - @Override public boolean understandsSymlink() { + + @Override + public boolean understandsSymlink() { return true; } - @Override public void visitSymlink(File link, String target, String relativePath) throws IOException { + + @Override + public void visitSymlink(File link, String target, String relativePath) throws IOException { try { - IOUtils.mkdirs(new File(dest, relativePath).getParentFile()); + mkdirsE(new File(dest, relativePath).getParentFile()); + writing(new File(dest, target)); Util.createSymlink(dest, target, relativePath, TaskListener.NULL); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } count.incrementAndGet(); } - }); + })); return count.get(); } }); @@ -1981,7 +2037,7 @@ public final class FilePath implements Serializable { // local -> remote copy final Pipe pipe = Pipe.createLocalToRemote(); - Future future = target.actAsync(new FileCallable() { + Future future = target.actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { try { @@ -1992,7 +2048,7 @@ public final class FilePath implements Serializable { } } }); - Future future2 = actAsync(new FileCallable() { + Future future2 = actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; @Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut())); @@ -2009,7 +2065,7 @@ public final class FilePath implements Serializable { // remote -> local copy final Pipe pipe = Pipe.createRemoteToLocal(); - Future future = actAsync(new FileCallable() { + Future future = actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Integer invoke(File f, VirtualChannel channel) throws IOException { try { @@ -2069,10 +2125,10 @@ public final class FilePath implements Serializable { * @return * number of files/directories that are written. */ - private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException { + private Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException { Archiver tw = ArchiverFactory.TAR.create(out); try { - scanner.scan(baseDir,tw); + scanner.scan(baseDir,reading(tw)); } finally { tw.close(); } @@ -2082,17 +2138,18 @@ public final class FilePath implements Serializable { /** * Reads from a tar stream and stores obtained files to the base dir. */ - private static void readFromTar(String name, File baseDir, InputStream in) throws IOException { + private void readFromTar(String name, File baseDir, InputStream in) throws IOException { TarInputStream t = new TarInputStream(in); try { TarEntry te; while ((te = t.getNextEntry()) != null) { File f = new File(baseDir,te.getName()); if(te.isDirectory()) { - f.mkdirs(); + mkdirs(f); } else { File parent = f.getParentFile(); - if (parent != null) parent.mkdirs(); + if (parent != null) mkdirs(parent); + writing(f); byte linkFlag = (Byte) LINKFLAG_FIELD.get(te); if (linkFlag==TarEntry.LF_SYMLINK) { @@ -2131,7 +2188,7 @@ public final class FilePath implements Serializable { return new RemoteLauncher(listener,channel,channel.call(new IsUnix())); } - private static final class IsUnix implements Callable { + private static final class IsUnix extends MasterToSlaveCallable { public Boolean call() throws IOException { return File.pathSeparatorChar==':'; } @@ -2168,7 +2225,7 @@ public final class FilePath implements Serializable { * @since 1.484 */ public String validateAntFileMask(final String fileMasks, final int bound) throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new MasterToSlaveFileCallable() { private static final long serialVersionUID = 1; public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { if(fileMasks.startsWith("~")) @@ -2209,7 +2266,7 @@ public final class FilePath implements Serializable { {// check the (2) above next as this is more expensive. // Try prepending "**/" to see if that results in a match - FileSet fs = Util.createFileSet(dir,"**/"+fileMask); + FileSet fs = Util.createFileSet(reading(dir),"**/"+fileMask); DirectoryScanner ds = fs.getDirectoryScanner(new Project()); if(ds.getIncludedFilesCount()!=0) { // try shorter name first so that the suggestion results in least amount of changes @@ -2278,7 +2335,7 @@ public final class FilePath implements Serializable { return super.isCaseSensitive(); } }; - ds.setBasedir(dir); + ds.setBasedir(reading(dir)); ds.setIncludes(new String[] {pattern}); try { ds.scan(); @@ -2451,8 +2508,14 @@ public final class FilePath implements Serializable { ois.defaultReadObject(); if(ois.readBoolean()) { this.channel = channel; + this.filter = null; } else { this.channel = null; + // If the remote channel wants us to create a FilePath that points to a local file, + // we need to make sure the access control takes place. + // This covers the immediate case of FileCallables taking FilePath into reference closure implicitly, + // but it also covers more general case of FilePath sent as a return value or argument. + this.filter = FilePathFilter.current(); } } @@ -2487,6 +2550,14 @@ public final class FilePath implements Serializable { } } + /** + * Role check comes from {@link FileCallable}s. + */ + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + callable.checkRoles(checker); + } + public ClassLoader getClassLoader() { return classLoader; } @@ -2529,7 +2600,7 @@ public final class FilePath implements Serializable { * (User's home directory in the Unix sense) of the given channel. */ public static FilePath getHomeDirectory(VirtualChannel ch) throws InterruptedException, IOException { - return ch.call(new Callable() { + return ch.call(new MasterToSlaveCallable() { public FilePath call() throws IOException { return new FilePath(new File(System.getProperty("user.home"))); } @@ -2563,7 +2634,6 @@ public final class FilePath implements Serializable { scanSingle(new File(dir, workspacePath), archivedPath, visitor); } } - } private static final ExecutorService threadPoolForRemoting = new ContextResettingExecutorService( @@ -2573,4 +2643,93 @@ public final class FilePath implements Serializable { )); public static final LocalChannel localChannel = new LocalChannel(threadPoolForRemoting); + + private @Nonnull FilePathFilter filterNonNull() { + return filter!=null ? filter : FilePathFilter.EMPTY; + } + + /** + * Wraps {@link FileVisitor} to notify read access to {@link FilePathFilter}. + */ + private FileVisitor reading(final FileVisitor v) { + final FilePathFilter filter = FilePathFilter.current(); + if (filter==null) return v; + + return new FileVisitor() { + @Override + public void visit(File f, String relativePath) throws IOException { + filter.read(f); + v.visit(f,relativePath); + } + + @Override + public void visitSymlink(File link, String target, String relativePath) throws IOException { + filter.read(link); + v.visitSymlink(link, target, relativePath); + } + + @Override + public boolean understandsSymlink() { + return v.understandsSymlink(); + } + }; + } + + /** + * Pass through 'f' after ensuring that we can read that file. + */ + private File reading(File f) { + filterNonNull().read(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can access the file attributes. + */ + private File stating(File f) { + filterNonNull().stat(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can create that file/dir. + */ + private File creating(File f) { + filterNonNull().create(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can write to that file. + */ + private File writing(File f) { + FilePathFilter filter = filterNonNull(); + if (!f.exists()) + filter.create(f); + filter.write(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can delete that file. + */ + private File deleting(File f) { + filterNonNull().delete(f); + return f; + } + + private boolean mkdirs(File dir) { + if (dir.exists()) return false; + + filterNonNull().mkdirs(dir); + return dir.mkdirs(); + } + + private File mkdirsE(File dir) throws IOException { + if (dir.exists()) { + return dir; + } + filterNonNull().mkdirs(dir); + return IOUtils.mkdirs(dir); + } } diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index 0d50a39166..be003dfb64 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -38,6 +38,7 @@ import hudson.remoting.VirtualChannel; import hudson.util.StreamCopyThread; import hudson.util.ArgumentListBuilder; import hudson.util.ProcessTree; +import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.input.NullInputStream; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -940,7 +941,7 @@ public abstract class Launcher { getChannel().call(new KillTask(modelEnvVars)); } - private static final class KillTask implements Callable { + private static final class KillTask extends MasterToSlaveCallable { private final Map modelEnvVars; public KillTask(Map modelEnvVars) { @@ -1097,7 +1098,7 @@ public abstract class Launcher { IOTriplet getIOtriplet(); } - private static class RemoteLaunchCallable implements Callable { + private static class RemoteLaunchCallable extends MasterToSlaveCallable { private final List cmd; private final boolean[] masks; private final String[] env; @@ -1169,7 +1170,7 @@ public abstract class Launcher { private static final long serialVersionUID = 1L; } - private static class RemoteChannelLaunchCallable implements Callable { + private static class RemoteChannelLaunchCallable extends MasterToSlaveCallable { private final String[] cmd; private final Pipe out; private final String workDir; diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index 410605951a..2c68b0f6ff 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -36,6 +36,7 @@ import hudson.remoting.Channel; import hudson.remoting.ChannelProperty; import hudson.security.CliAuthenticator; import hudson.security.SecurityRealm; +import jenkins.security.MasterToSlaveCallable; import org.acegisecurity.Authentication; import org.acegisecurity.BadCredentialsException; import org.acegisecurity.context.SecurityContext; @@ -409,7 +410,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { return checkChannel().call(new GetSystemProperty(name)); } - private static final class GetSystemProperty implements Callable { + private static final class GetSystemProperty extends MasterToSlaveCallable { private final String name; private GetSystemProperty(String name) { @@ -438,7 +439,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { } } - private static final class GetCharset implements Callable { + private static final class GetCharset extends MasterToSlaveCallable { public String call() throws IOException { return Charset.defaultCharset().name(); } @@ -453,7 +454,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { return checkChannel().call(new GetEnvironmentVariable(name)); } - private static final class GetEnvironmentVariable implements Callable { + private static final class GetEnvironmentVariable extends MasterToSlaveCallable { private final String name; private GetEnvironmentVariable(String name) { diff --git a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java index 3f9479c6e7..a172a236d2 100644 --- a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java +++ b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java @@ -1,10 +1,10 @@ package hudson.cli; import hudson.FilePath; -import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.util.Secret; import jenkins.model.Jenkins; +import jenkins.security.MasterToSlaveCallable; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationException; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; @@ -39,7 +39,7 @@ public class ClientAuthenticationCache implements Serializable { private final Properties props = new Properties(); public ClientAuthenticationCache(Channel channel) throws IOException, InterruptedException { - store = (channel==null ? FilePath.localChannel : channel).call(new Callable() { + store = (channel==null ? FilePath.localChannel : channel).call(new MasterToSlaveCallable() { public FilePath call() throws IOException { File home = new File(System.getProperty("user.home")); File hudsonHome = new File(home, ".hudson"); diff --git a/core/src/main/java/hudson/cli/CommandDuringBuild.java b/core/src/main/java/hudson/cli/CommandDuringBuild.java index f4b4d59d6c..dfa65f487a 100644 --- a/core/src/main/java/hudson/cli/CommandDuringBuild.java +++ b/core/src/main/java/hudson/cli/CommandDuringBuild.java @@ -28,6 +28,7 @@ import jenkins.model.Jenkins; import hudson.model.Job; import hudson.model.Run; import hudson.remoting.Callable; +import jenkins.security.MasterToSlaveCallable; import org.kohsuke.args4j.CmdLineException; import java.io.IOException; @@ -84,7 +85,7 @@ public abstract class CommandDuringBuild extends CLICommand { /** * Gets the environment variables that points to the build being executed. */ - private static final class GetCharacteristicEnvironmentVariables implements Callable { + private static final class GetCharacteristicEnvironmentVariables extends MasterToSlaveCallable { public String[] call() throws IOException { return new String[] { System.getenv("JOB_NAME"), diff --git a/core/src/main/java/hudson/cli/InstallToolCommand.java b/core/src/main/java/hudson/cli/InstallToolCommand.java index f5ad164f95..7e0c37ac28 100644 --- a/core/src/main/java/hudson/cli/InstallToolCommand.java +++ b/core/src/main/java/hudson/cli/InstallToolCommand.java @@ -31,10 +31,7 @@ import hudson.model.AbstractProject; import hudson.model.Run; import hudson.model.Executor; import hudson.model.Node; -import hudson.model.EnvironmentSpecific; import hudson.model.Item; -import hudson.remoting.Callable; -import hudson.slaves.NodeSpecific; import hudson.util.EditDistance; import hudson.util.StreamTaskListener; import hudson.tools.ToolDescriptor; @@ -44,6 +41,7 @@ import java.util.List; import java.util.ArrayList; import java.io.IOException; +import jenkins.security.MasterToSlaveCallable; import org.kohsuke.args4j.Argument; /** @@ -131,7 +129,7 @@ public class InstallToolCommand extends CLICommand { return 0; } - private static final class BuildIDs implements Callable { + private static final class BuildIDs extends MasterToSlaveCallable { String job,number,id; public BuildIDs call() throws IOException { diff --git a/core/src/main/java/hudson/cli/util/ScriptLoader.java b/core/src/main/java/hudson/cli/util/ScriptLoader.java index 08df6f6d68..e2252efbd5 100644 --- a/core/src/main/java/hudson/cli/util/ScriptLoader.java +++ b/core/src/main/java/hudson/cli/util/ScriptLoader.java @@ -1,7 +1,9 @@ package hudson.cli.util; import hudson.AbortException; -import hudson.remoting.Callable; +import jenkins.security.MasterToSlaveCallable; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import java.io.File; import java.io.IOException; @@ -9,15 +11,12 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; - /** - * - * @author vjuranek + * Reads a file (either a path or URL) over a channel. * + * @author vjuranek */ -public class ScriptLoader implements Callable { +public class ScriptLoader extends MasterToSlaveCallable { private final String script; @@ -43,5 +42,4 @@ public class ScriptLoader implements Callable { s.close(); } } - } diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java index 72affc2af6..81c9486e3f 100644 --- a/core/src/main/java/hudson/logging/LogRecorder.java +++ b/core/src/main/java/hudson/logging/LogRecorder.java @@ -40,6 +40,7 @@ import hudson.slaves.ComputerListener; import hudson.util.CopyOnWriteList; import hudson.util.RingBufferLogHandler; import hudson.util.XStream2; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.kohsuke.stapler.*; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -207,7 +208,7 @@ public class LogRecorder extends AbstractModelObject implements Saveable { } - private static final class SetLevel implements Callable { + private static final class SetLevel extends MasterToSlaveCallable { /** known loggers (kept per slave), to avoid GC */ @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private static final Set loggers = new HashSet(); private final String name; diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java index 9d8f3ec1f5..4c1b2f2be5 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -34,7 +34,6 @@ import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; -import hudson.remoting.Callable; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.ACL; @@ -43,6 +42,7 @@ import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.AtomicFileWriter; import hudson.util.IOUtils; import jenkins.model.Jenkins; +import jenkins.security.NotReallyRoleSensitiveCallable; import org.acegisecurity.Authentication; import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; @@ -226,7 +226,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet // the test to see if the project already exists or not needs to be done in escalated privilege // to avoid overwriting - ACL.impersonate(ACL.SYSTEM,new Callable() { + ACL.impersonate(ACL.SYSTEM,new NotReallyRoleSensitiveCallable() { final Authentication user = Jenkins.getAuthentication(); @Override public Void call() throws IOException { @@ -657,7 +657,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet throw new IOException("Expecting "+this.getClass()+" but got "+o.getClass()+" instead"); } - Items.whileUpdatingByXml(new Callable() { + Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable() { @Override public Void call() throws IOException { onLoad(getParent(), getRootDir().getName()); return null; @@ -689,7 +689,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet // try to reflect the changes by reloading getConfigFile().unmarshal(this); - Items.whileUpdatingByXml(new Callable() { + Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable() { @Override public Void call() throws IOException { onLoad(getParent(), getRootDir().getName()); diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index b0c1b39ce8..987f6a2033 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -38,7 +38,6 @@ import hudson.model.queue.WorkUnit; import hudson.node_monitors.NodeMonitor; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; -import hudson.remoting.Callable; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; @@ -61,6 +60,7 @@ import hudson.util.Futures; import hudson.util.NamingThreadFactory; import jenkins.model.Jenkins; import jenkins.util.ContextResettingExecutorService; +import jenkins.security.MasterToSlaveCallable; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.StaplerRequest; @@ -1042,7 +1042,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces oneOffExecutors.remove(e); } - private static class ListPossibleNames implements Callable,IOException> { + private static class ListPossibleNames extends MasterToSlaveCallable,IOException> { public List call() throws IOException { List names = new ArrayList(); @@ -1072,7 +1072,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces private static final long serialVersionUID = 1L; } - private static class GetFallbackName implements Callable { + private static class GetFallbackName extends MasterToSlaveCallable { public String call() throws IOException { return System.getProperty("host.name"); } @@ -1150,7 +1150,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces w.close(); } - private static final class DumpExportTableTask implements Callable { + private static final class DumpExportTableTask extends MasterToSlaveCallable { public String call() throws IOException { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); diff --git a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java index ac97ec1e1c..eb82c67ce3 100644 --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java @@ -25,7 +25,6 @@ package hudson.model; import hudson.FilePath; import hudson.Util; -import hudson.remoting.Callable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -43,6 +42,7 @@ import java.util.logging.Logger; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import jenkins.model.Jenkins; +import jenkins.security.MasterToSlaveCallable; import jenkins.util.VirtualFile; import org.apache.commons.io.IOUtils; import org.apache.tools.zip.ZipEntry; @@ -461,7 +461,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { } } - private static final class BuildChildPaths implements Callable>,IOException> { + private static final class BuildChildPaths extends MasterToSlaveCallable>,IOException> { private final VirtualFile cur; private final Locale locale; BuildChildPaths(VirtualFile cur, Locale locale) { diff --git a/core/src/main/java/hudson/model/ItemGroupMixIn.java b/core/src/main/java/hudson/model/ItemGroupMixIn.java index 131738ed18..d34b5e7f93 100644 --- a/core/src/main/java/hudson/model/ItemGroupMixIn.java +++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java @@ -26,7 +26,6 @@ package hudson.model; import hudson.Util; import hudson.XmlFile; import hudson.model.listeners.ItemListener; -import hudson.remoting.Callable; import hudson.security.AccessControlled; import hudson.util.CopyOnWriteMap; import hudson.util.Function1; @@ -44,6 +43,7 @@ import java.io.InputStream; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.security.NotReallyRoleSensitiveCallable; /** * Defines a bunch of static methods to be used as a "mix-in" for {@link ItemGroup} @@ -222,7 +222,7 @@ public abstract class ItemGroupMixIn { // reload from the new config final File rootDir = result.getRootDir(); - result = Items.whileUpdatingByXml(new Callable() { + result = Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable() { @Override public T call() throws IOException { return (T) Items.load(parent, rootDir); } @@ -253,7 +253,7 @@ public abstract class ItemGroupMixIn { IOUtils.copy(xml,configXml); // load it - TopLevelItem result = Items.whileUpdatingByXml(new Callable() { + TopLevelItem result = Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable() { @Override public TopLevelItem call() throws IOException { return (TopLevelItem) Items.load(parent, dir); } diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java index 0393914e12..92ac16518d 100644 --- a/core/src/main/java/hudson/model/Slave.java +++ b/core/src/main/java/hudson/model/Slave.java @@ -59,6 +59,7 @@ import javax.servlet.ServletException; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import jenkins.model.Jenkins; +import jenkins.security.MasterToSlaveCallable; import jenkins.slaves.WorkspaceLocator; import org.apache.commons.io.IOUtils; @@ -455,7 +456,7 @@ public abstract class Slave extends Node implements Serializable { *

  • When it's read on this side as a return value, it morphs itself into {@link ClockDifference}. * */ - private static final class GetClockDifference1 implements Callable { + private static final class GetClockDifference1 extends MasterToSlaveCallable { public ClockDifference call() { // this method must be being invoked locally, which means the clock is in sync return new ClockDifference(0); @@ -468,7 +469,7 @@ public abstract class Slave extends Node implements Serializable { private static final long serialVersionUID = 1L; } - private static final class GetClockDifference2 implements Callable { + private static final class GetClockDifference2 extends MasterToSlaveCallable { /** * Capture the time on the master when this object is sent to remote, which is when * {@link GetClockDifference1#writeReplace()} is run. diff --git a/core/src/main/java/hudson/node_monitors/ArchitectureMonitor.java b/core/src/main/java/hudson/node_monitors/ArchitectureMonitor.java index 9f127a7f72..ec0618f807 100644 --- a/core/src/main/java/hudson/node_monitors/ArchitectureMonitor.java +++ b/core/src/main/java/hudson/node_monitors/ArchitectureMonitor.java @@ -26,6 +26,7 @@ package hudson.node_monitors; import hudson.model.Computer; import hudson.remoting.Callable; import hudson.Extension; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; @@ -56,7 +57,7 @@ public class ArchitectureMonitor extends NodeMonitor { /** * Obtains the string that represents the architecture. */ - private static class GetArchTask implements Callable { + private static class GetArchTask extends MasterToSlaveCallable { public String call() { String os = System.getProperty("os.name"); String arch = System.getProperty("os.arch"); diff --git a/core/src/main/java/hudson/node_monitors/DiskSpaceMonitorDescriptor.java b/core/src/main/java/hudson/node_monitors/DiskSpaceMonitorDescriptor.java index e67bb3ce0d..37e4b385f1 100644 --- a/core/src/main/java/hudson/node_monitors/DiskSpaceMonitorDescriptor.java +++ b/core/src/main/java/hudson/node_monitors/DiskSpaceMonitorDescriptor.java @@ -23,8 +23,8 @@ */ package hudson.node_monitors; -import hudson.FilePath.FileCallable; import hudson.Functions; +import jenkins.MasterToSlaveFileCallable; import hudson.remoting.VirtualChannel; import hudson.Util; import hudson.slaves.OfflineCause; @@ -154,7 +154,7 @@ public abstract class DiskSpaceMonitorDescriptor extends AbstractAsyncNodeMonito private static final long serialVersionUID = 2L; } - protected static final class GetUsableSpace implements FileCallable { + protected static final class GetUsableSpace extends MasterToSlaveFileCallable { public GetUsableSpace() {} public DiskSpace invoke(File f, VirtualChannel channel) throws IOException { long s = f.getUsableSpace(); diff --git a/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java b/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java index c259f891ee..fa864bae3c 100644 --- a/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java +++ b/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java @@ -28,6 +28,7 @@ import hudson.Extension; import hudson.slaves.OfflineCause; import hudson.model.Computer; import hudson.remoting.Callable; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; @@ -85,7 +86,7 @@ public class ResponseTimeMonitor extends NodeMonitor { } }; - private static final class Step1 implements Callable { + private static final class Step1 extends MasterToSlaveCallable { private Data cur; private Step1(Data cur) { @@ -104,7 +105,7 @@ public class ResponseTimeMonitor extends NodeMonitor { private static final long serialVersionUID = 1L; } - private static final class Step2 implements Callable { + private static final class Step2 extends MasterToSlaveCallable { private final Data cur; private final long start = System.currentTimeMillis(); diff --git a/core/src/main/java/hudson/node_monitors/SwapSpaceMonitor.java b/core/src/main/java/hudson/node_monitors/SwapSpaceMonitor.java index 29bfb0c570..c289d0fbdd 100644 --- a/core/src/main/java/hudson/node_monitors/SwapSpaceMonitor.java +++ b/core/src/main/java/hudson/node_monitors/SwapSpaceMonitor.java @@ -28,7 +28,7 @@ import hudson.Extension; import hudson.Functions; import hudson.model.Computer; import jenkins.model.Jenkins; -import hudson.remoting.Callable; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.jvnet.hudson.MemoryMonitor; import org.jvnet.hudson.MemoryUsage; @@ -100,7 +100,7 @@ public class SwapSpaceMonitor extends NodeMonitor { /** * Obtains the string that represents the architecture. */ - private static class MonitorTask implements Callable { + private static class MonitorTask extends MasterToSlaveCallable { public MemoryUsage call() throws IOException { MemoryMonitor mm; try { diff --git a/core/src/main/java/hudson/node_monitors/TemporarySpaceMonitor.java b/core/src/main/java/hudson/node_monitors/TemporarySpaceMonitor.java index e42951ff12..07e99b8866 100644 --- a/core/src/main/java/hudson/node_monitors/TemporarySpaceMonitor.java +++ b/core/src/main/java/hudson/node_monitors/TemporarySpaceMonitor.java @@ -25,7 +25,7 @@ package hudson.node_monitors; import hudson.Extension; import hudson.FilePath; -import hudson.FilePath.FileCallable; +import jenkins.MasterToSlaveFileCallable; import hudson.model.Computer; import hudson.model.Node; import hudson.remoting.Callable; @@ -83,7 +83,7 @@ public class TemporarySpaceMonitor extends AbstractDiskSpaceMonitor { return DESCRIPTOR; } - protected static final class GetTempSpace implements FileCallable { + protected static final class GetTempSpace extends MasterToSlaveFileCallable { public DiskSpace invoke(File f, VirtualChannel channel) throws IOException { // if the disk is really filled up we can't even create a single file, // so calling File.createTempFile and figuring out the directory won't reliably work. diff --git a/core/src/main/java/hudson/os/solaris/ZFSInstaller.java b/core/src/main/java/hudson/os/solaris/ZFSInstaller.java index ec0d51ccb8..17a3bb48ac 100644 --- a/core/src/main/java/hudson/os/solaris/ZFSInstaller.java +++ b/core/src/main/java/hudson/os/solaris/ZFSInstaller.java @@ -38,6 +38,7 @@ import hudson.util.HudsonIsRestarting; import hudson.util.StreamTaskListener; import static hudson.util.jna.GNUCLibrary.*; +import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.output.ByteArrayOutputStream; import org.jvnet.libpam.impl.CLibrary.passwd; import org.jvnet.solaris.libzfs.ACLBuilder; @@ -167,7 +168,7 @@ public class ZFSInstaller extends AdministrativeMonitor implements Serializable // this is the actual creation of the file system. // return true indicating a success - return SU.execute(listener, rootUsername, rootPassword, new Callable() { + return SU.execute(listener, rootUsername, rootPassword, new MasterToSlaveCallable() { private static final long serialVersionUID = 7731167233498214301L; public String call() throws IOException { diff --git a/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java b/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java index 3cae513ffa..649a4880f2 100644 --- a/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java +++ b/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java @@ -23,13 +23,13 @@ */ package hudson.os.solaris; +import jenkins.MasterToSlaveFileCallable; import hudson.FileSystemProvisioner; import hudson.FilePath; import hudson.WorkspaceSnapshot; import hudson.FileSystemProvisionerDescriptor; import hudson.Extension; import hudson.remoting.VirtualChannel; -import hudson.FilePath.FileCallable; import hudson.model.AbstractBuild; import hudson.model.TaskListener; import hudson.model.AbstractProject; @@ -52,7 +52,7 @@ public class ZFSProvisioner extends FileSystemProvisioner implements Serializabl private final String rootDataset; public ZFSProvisioner(Node node) throws IOException, InterruptedException { - rootDataset = node.getRootPath().act(new FileCallable() { + rootDataset = node.getRootPath().act(new MasterToSlaveFileCallable() { private static final long serialVersionUID = -2142349338699797436L; public String invoke(File f, VirtualChannel channel) throws IOException { @@ -68,7 +68,7 @@ public class ZFSProvisioner extends FileSystemProvisioner implements Serializabl public void prepareWorkspace(AbstractBuild build, FilePath ws, final TaskListener listener) throws IOException, InterruptedException { final String name = build.getProject().getFullName(); - ws.act(new FileCallable() { + ws.act(new MasterToSlaveFileCallable() { private static final long serialVersionUID = 2129531727963121198L; public Void invoke(File f, VirtualChannel channel) throws IOException { @@ -87,7 +87,7 @@ public class ZFSProvisioner extends FileSystemProvisioner implements Serializabl } public void discardWorkspace(AbstractProject project, FilePath ws) throws IOException, InterruptedException { - ws.act(new FileCallable() { + ws.act(new MasterToSlaveFileCallable() { private static final long serialVersionUID = 1916618107019257530L; public Void invoke(File f, VirtualChannel channel) throws IOException { diff --git a/core/src/main/java/hudson/security/AbstractPasswordBasedSecurityRealm.java b/core/src/main/java/hudson/security/AbstractPasswordBasedSecurityRealm.java index 65ec05992c..9cc2b6fdd5 100644 --- a/core/src/main/java/hudson/security/AbstractPasswordBasedSecurityRealm.java +++ b/core/src/main/java/hudson/security/AbstractPasswordBasedSecurityRealm.java @@ -3,13 +3,13 @@ package hudson.security; import groovy.lang.Binding; import hudson.FilePath; import hudson.cli.CLICommand; -import hudson.remoting.Callable; import hudson.util.spring.BeanBuilder; import java.io.Console; import java.io.IOException; import jenkins.model.Jenkins; import jenkins.security.ImpersonatingUserDetailsService; import jenkins.security.SecurityListener; +import jenkins.security.MasterToSlaveCallable; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationException; import org.acegisecurity.AuthenticationManager; @@ -152,7 +152,7 @@ public abstract class AbstractPasswordBasedSecurityRealm extends SecurityRealm i /** * Asks for the password. */ - private static class InteractivelyAskForPassword implements Callable { + private static class InteractivelyAskForPassword extends MasterToSlaveCallable { public String call() throws IOException { Console console = System.console(); if (console == null) return null; // no terminal diff --git a/core/src/main/java/hudson/slaves/ChannelPinger.java b/core/src/main/java/hudson/slaves/ChannelPinger.java index 1e3837a96c..726e577672 100644 --- a/core/src/main/java/hudson/slaves/ChannelPinger.java +++ b/core/src/main/java/hudson/slaves/ChannelPinger.java @@ -28,9 +28,9 @@ import hudson.FilePath; import hudson.model.Computer; import hudson.model.Slave; import hudson.model.TaskListener; -import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.PingThread; +import jenkins.security.MasterToSlaveCallable; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; @@ -89,7 +89,7 @@ public class ChannelPinger extends ComputerListener { setUpPingForChannel(channel, pingInterval); } - private static class SetUpRemotePing implements Callable { + private static class SetUpRemotePing extends MasterToSlaveCallable { private static final long serialVersionUID = -2702219700841759872L; private int pingInterval; public SetUpRemotePing(int pingInterval) { diff --git a/core/src/main/java/hudson/slaves/Channels.java b/core/src/main/java/hudson/slaves/Channels.java index a8d26aa444..1f2878a57c 100644 --- a/core/src/main/java/hudson/slaves/Channels.java +++ b/core/src/main/java/hudson/slaves/Channels.java @@ -29,14 +29,18 @@ import hudson.FilePath; import hudson.model.Computer; import hudson.model.TaskListener; import hudson.remoting.Channel; +import hudson.remoting.ChannelBuilder; +import hudson.remoting.CommandTransport; import hudson.remoting.Launcher; import hudson.remoting.SocketChannelStream; import hudson.util.ClasspathBuilder; import hudson.util.JVMBuilder; import hudson.util.StreamCopyThread; +import jenkins.security.ChannelConfigurator; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -67,66 +71,84 @@ public class Channels { * we kill the process. */ public static Channel forProcess(String name, ExecutorService execService, InputStream in, OutputStream out, OutputStream header, final Proc proc) throws IOException { - return new Channel(name, execService, in, out, header) { - /** - * Kill the process when the channel is severed. - */ + ChannelBuilder cb = new ChannelBuilder(name,execService) { @Override - public synchronized void terminate(IOException e) { - super.terminate(e); - try { - proc.kill(); - } catch (IOException x) { - // we are already in the error recovery mode, so just record it and move on - LOGGER.log(Level.INFO, "Failed to terminate the severed connection",x); - } catch (InterruptedException x) { - // process the interrupt later - Thread.currentThread().interrupt(); - } - } + public Channel build(CommandTransport transport) throws IOException { + return new Channel(this,transport) { + /** + * Kill the process when the channel is severed. + */ + @Override + public synchronized void terminate(IOException e) { + super.terminate(e); + try { + proc.kill(); + } catch (IOException x) { + // we are already in the error recovery mode, so just record it and move on + LOGGER.log(Level.INFO, "Failed to terminate the severed connection",x); + } catch (InterruptedException x) { + // process the interrupt later + Thread.currentThread().interrupt(); + } + } - @Override - public synchronized void close() throws IOException { - super.close(); - // wait for the child process to complete - try { - proc.join(); - } catch (InterruptedException e) { - // process the interrupt later - Thread.currentThread().interrupt(); - } + @Override + public synchronized void join() throws InterruptedException { + super.join(); + // wait for the child process to complete, too + try { + proc.join(); + } catch (IOException e) { + throw new IOError(e); + } + } + }; } }; + cb.withHeaderStream(header); + + for (ChannelConfigurator cc : ChannelConfigurator.all()) { + cc.onChannelBuilding(cb,null); // TODO: what to pass as a context? + } + + return cb.build(in,out); } public static Channel forProcess(String name, ExecutorService execService, final Process proc, OutputStream header) throws IOException { final Thread thread = new StreamCopyThread(name + " stderr", proc.getErrorStream(), header); thread.start(); - return new Channel(name, execService, proc.getInputStream(), proc.getOutputStream(), header) { - /** - * Kill the process when the channel is severed. - */ + ChannelBuilder cb = new ChannelBuilder(name,execService) { @Override - public synchronized void terminate(IOException e) { - super.terminate(e); - proc.destroy(); - // the stderr copier should exit by itself - } + public Channel build(CommandTransport transport) throws IOException { + return new Channel(this,transport) { + /** + * Kill the process when the channel is severed. + */ + @Override + public synchronized void terminate(IOException e) { + super.terminate(e); + proc.destroy(); + // the stderr copier should exit by itself + } - @Override - public synchronized void close() throws IOException { - super.close(); - // wait for Maven to complete - try { - proc.waitFor(); - thread.join(); - } catch (InterruptedException e) { - // process the interrupt later - Thread.currentThread().interrupt(); - } + @Override + public synchronized void join() throws InterruptedException { + super.join(); + // wait for the child process to complete, too + proc.waitFor(); + thread.join(); + } + }; } }; + cb.withHeaderStream(header); + + for (ChannelConfigurator cc : ChannelConfigurator.all()) { + cc.onChannelBuilding(cb,null); // TODO: what to pass as a context? + } + + return cb.build(proc.getInputStream(),proc.getOutputStream()); } /** diff --git a/core/src/main/java/hudson/slaves/ComputerListener.java b/core/src/main/java/hudson/slaves/ComputerListener.java index e3df0ba63d..5af844859e 100644 --- a/core/src/main/java/hudson/slaves/ComputerListener.java +++ b/core/src/main/java/hudson/slaves/ComputerListener.java @@ -23,16 +23,18 @@ */ package hudson.slaves; -import hudson.model.Computer; -import jenkins.model.Jenkins; -import hudson.model.TaskListener; -import hudson.model.Node; -import hudson.ExtensionPoint; +import hudson.AbortException; import hudson.Extension; import hudson.ExtensionList; +import hudson.ExtensionPoint; import hudson.FilePath; +import hudson.model.Computer; +import hudson.model.Node; +import hudson.model.TaskListener; +import org.jenkinsci.remoting.CallableDecorator; import hudson.remoting.Channel; -import hudson.AbortException; +import hudson.remoting.ChannelBuilder; +import jenkins.model.Jenkins; import java.io.IOException; @@ -46,6 +48,7 @@ import javax.annotation.Nonnull; * @since 1.246 */ public abstract class ComputerListener implements ExtensionPoint { + /** * Called before a {@link ComputerLauncher} is asked to launch a connection with {@link Computer}. * diff --git a/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java b/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java index 03890303d1..41d9aaa431 100644 --- a/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java +++ b/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java @@ -30,8 +30,8 @@ import hudson.model.Computer; import hudson.util.TimeUnit2; import hudson.remoting.VirtualChannel; import hudson.remoting.Channel; -import hudson.remoting.Callable; import hudson.Extension; +import jenkins.security.SlaveToMasterCallable; import java.io.IOException; import java.util.logging.Logger; @@ -102,7 +102,7 @@ public class ConnectionActivityMonitor extends AsyncPeriodicWork { public boolean enabled = Boolean.getBoolean(ConnectionActivityMonitor.class.getName()+".enabled"); private static final PingCommand PING_COMMAND = new PingCommand(); - private static final class PingCommand implements Callable { + private static final class PingCommand extends SlaveToMasterCallable { public Void call() throws RuntimeException { return null; } diff --git a/core/src/main/java/hudson/slaves/SlaveComputer.java b/core/src/main/java/hudson/slaves/SlaveComputer.java index 6f641dd92c..b08f646570 100644 --- a/core/src/main/java/hudson/slaves/SlaveComputer.java +++ b/core/src/main/java/hudson/slaves/SlaveComputer.java @@ -24,6 +24,8 @@ package hudson.slaves; import hudson.AbortException; +import hudson.remoting.ChannelBuilder; +import hudson.util.IOUtils; import hudson.FilePath; import hudson.Util; import hudson.model.Computer; @@ -34,7 +36,6 @@ import hudson.model.Queue; import hudson.model.Slave; import hudson.model.TaskListener; import hudson.model.User; -import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.Launcher; import hudson.remoting.VirtualChannel; @@ -46,18 +47,10 @@ import hudson.util.RingBufferLogHandler; import hudson.util.StreamTaskListener; import hudson.util.io.ReopenableFileOutputStream; import hudson.util.io.ReopenableRotatingFileOutputStream; -import jenkins.model.Jenkins; import jenkins.slaves.EncryptedSlaveAgentJnlpFile; -import jenkins.slaves.JnlpSlaveAgentProtocol; import jenkins.slaves.systemInfo.SlaveSystemInfo; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; -import org.apache.commons.io.IOUtils; -import org.kohsuke.stapler.HttpRedirect; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.WebMethod; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -81,6 +74,14 @@ import java.util.logging.Logger; import javax.annotation.CheckForNull; import jenkins.model.Jenkins; import static hudson.slaves.SlaveComputer.LogHolder.*; +import jenkins.security.ChannelConfigurator; +import jenkins.security.MasterToSlaveCallable; +import jenkins.slaves.JnlpSlaveAgentProtocol; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.HttpRedirect; /** @@ -345,7 +346,15 @@ public class SlaveComputer extends Computer { * so the implementation of the listener doesn't need to do that again. */ public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Channel.Listener listener) throws IOException, InterruptedException { - Channel channel = new Channel(nodeName,threadPoolForRemoting, Channel.Mode.NEGOTIATE, in,out, launchLog); + ChannelBuilder cb = new ChannelBuilder(nodeName,threadPoolForRemoting) + .withMode(Channel.Mode.NEGOTIATE) + .withHeaderStream(launchLog); + + for (ChannelConfigurator cc : ChannelConfigurator.all()) { + cc.onChannelBuilding(cb,this); + } + + Channel channel = cb.build(in,out); setChannel(channel,launchLog,listener); } @@ -393,7 +402,7 @@ public class SlaveComputer extends Computer { return channel.call(new LoadingTime(true)); } - static class LoadingCount implements Callable { + static class LoadingCount extends MasterToSlaveCallable { private final boolean resource; LoadingCount(boolean resource) { this.resource = resource; @@ -404,13 +413,13 @@ public class SlaveComputer extends Computer { } } - static class LoadingPrefetchCacheCount implements Callable { + static class LoadingPrefetchCacheCount extends MasterToSlaveCallable { @Override public Integer call() { return Channel.current().classLoadingPrefetchCacheCount.get(); } } - static class LoadingTime implements Callable { + static class LoadingTime extends MasterToSlaveCallable { private final boolean resource; LoadingTime(boolean resource) { this.resource = resource; @@ -677,19 +686,19 @@ public class SlaveComputer extends Computer { private static final Logger logger = Logger.getLogger(SlaveComputer.class.getName()); - private static final class SlaveVersion implements Callable { + private static final class SlaveVersion extends MasterToSlaveCallable { public String call() throws IOException { try { return Launcher.VERSION; } catch (Throwable ex) { return "< 1.335"; } // Older slave.jar won't have VERSION } } - private static final class DetectOS implements Callable { + private static final class DetectOS extends MasterToSlaveCallable { public Boolean call() throws IOException { return File.pathSeparatorChar==':'; } } - private static final class DetectDefaultCharset implements Callable { + private static final class DetectDefaultCharset extends MasterToSlaveCallable { public String call() throws IOException { return Charset.defaultCharset().name(); } @@ -706,7 +715,7 @@ public class SlaveComputer extends Computer { static final RingBufferLogHandler SLAVE_LOG_HANDLER = new RingBufferLogHandler(); } - private static class SlaveInitializer implements Callable { + private static class SlaveInitializer extends MasterToSlaveCallable { public Void call() { // avoid double installation of the handler. JNLP slaves can reconnect to the master multiple times // and each connection gets a different RemoteClassLoader, so we need to evict them by class name, @@ -760,7 +769,7 @@ public class SlaveComputer extends Computer { return SlaveSystemInfo.all(); } - private static class SlaveLogFetcher implements Callable,RuntimeException> { + private static class SlaveLogFetcher extends MasterToSlaveCallable,RuntimeException> { public List call() { return new ArrayList(SLAVE_LOG_HANDLER.getView()); } diff --git a/core/src/main/java/hudson/tasks/ArtifactArchiver.java b/core/src/main/java/hudson/tasks/ArtifactArchiver.java index 00aa79b9b1..43839ce936 100644 --- a/core/src/main/java/hudson/tasks/ArtifactArchiver.java +++ b/core/src/main/java/hudson/tasks/ArtifactArchiver.java @@ -24,6 +24,7 @@ package hudson.tasks; import hudson.FilePath; +import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; import hudson.Util; import hudson.Extension; @@ -247,7 +248,7 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { } } - private static final class ListFiles implements FilePath.FileCallable> { + private static final class ListFiles extends MasterToSlaveFileCallable> { private static final long serialVersionUID = 1; private final String includes, excludes; private final boolean defaultExcludes; diff --git a/core/src/main/java/hudson/tasks/Fingerprinter.java b/core/src/main/java/hudson/tasks/Fingerprinter.java index 7320838ec4..cb38f02496 100644 --- a/core/src/main/java/hudson/tasks/Fingerprinter.java +++ b/core/src/main/java/hudson/tasks/Fingerprinter.java @@ -27,7 +27,7 @@ import com.google.common.collect.ImmutableMap; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; -import hudson.FilePath.FileCallable; +import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; @@ -209,7 +209,7 @@ public class Fingerprinter extends Recorder implements Serializable, DependencyD final long buildTimestamp = build.getTimeInMillis(); - List records = ws.act(new FileCallable>() { + List records = ws.act(new MasterToSlaveFileCallable>() { public List invoke(File baseDir, VirtualChannel channel) throws IOException { List results = new ArrayList(); diff --git a/core/src/main/java/hudson/tasks/Maven.java b/core/src/main/java/hudson/tasks/Maven.java index f4428c78ae..44016c6d3d 100644 --- a/core/src/main/java/hudson/tasks/Maven.java +++ b/core/src/main/java/hudson/tasks/Maven.java @@ -24,13 +24,13 @@ package hudson.tasks; import hudson.Extension; +import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; import hudson.Functions; import hudson.EnvVars; import hudson.Util; import hudson.CopyOnWrite; import hudson.Launcher.LocalLauncher; -import hudson.FilePath.FileCallable; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; @@ -42,7 +42,6 @@ import jenkins.mvn.GlobalMavenConfig; import jenkins.mvn.GlobalSettingsProvider; import jenkins.mvn.SettingsProvider; import hudson.model.TaskListener; -import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; import hudson.slaves.NodeSpecific; import hudson.tasks._maven.MavenConsoleAnnotator; @@ -59,6 +58,7 @@ import hudson.util.VariableResolver.ByMap; import hudson.util.VariableResolver.Union; import hudson.util.FormValidation; import hudson.util.XStream2; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; @@ -217,7 +217,7 @@ public class Maven extends Builder { * Looks for pom.xlm or project.xml to determine the maven executable * name. */ - private static final class DecideDefaultMavenCommand implements FileCallable { + private static final class DecideDefaultMavenCommand extends MasterToSlaveFileCallable { private static final long serialVersionUID = -2327576423452215146L; // command line arguments. private final String arguments; @@ -506,7 +506,7 @@ public class Maven extends Builder { public boolean meetsMavenReqVersion(Launcher launcher, int mavenReqVersion) throws IOException, InterruptedException { // FIXME using similar stuff as in the maven plugin could be better // olamy : but will add a dependency on maven in core -> so not so good - String mavenVersion = launcher.getChannel().call(new Callable() { + String mavenVersion = launcher.getChannel().call(new MasterToSlaveCallable() { private static final long serialVersionUID = -4143159957567745621L; public String call() throws IOException { @@ -562,7 +562,7 @@ public class Maven extends Builder { * Gets the executable path of this maven on the given target system. */ public String getExecutable(Launcher launcher) throws IOException, InterruptedException { - return launcher.getChannel().call(new Callable() { + return launcher.getChannel().call(new MasterToSlaveCallable() { private static final long serialVersionUID = 2373163112639943768L; public String call() throws IOException { diff --git a/core/src/main/java/hudson/tasks/Shell.java b/core/src/main/java/hudson/tasks/Shell.java index 17ce0b8819..a3652bc009 100644 --- a/core/src/main/java/hudson/tasks/Shell.java +++ b/core/src/main/java/hudson/tasks/Shell.java @@ -28,12 +28,12 @@ import hudson.Functions; import hudson.Util; import hudson.Extension; import hudson.model.AbstractProject; -import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; import hudson.util.FormValidation; import java.io.IOException; import java.io.ObjectStreamException; import hudson.util.LineEndingConversion; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; @@ -172,7 +172,7 @@ public class Shell extends CommandInterpreter { return FormValidation.validateExecutable(value); } - private static final class Shellinterpreter implements Callable { + private static final class Shellinterpreter extends MasterToSlaveCallable { private static final long serialVersionUID = 1L; diff --git a/core/src/main/java/hudson/tools/JDKInstaller.java b/core/src/main/java/hudson/tools/JDKInstaller.java index af34178ebf..28d9947abe 100644 --- a/core/src/main/java/hudson/tools/JDKInstaller.java +++ b/core/src/main/java/hudson/tools/JDKInstaller.java @@ -40,6 +40,7 @@ import hudson.util.FormValidation; import hudson.util.HttpResponses; import hudson.util.Secret; import jenkins.model.Jenkins; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; import org.apache.commons.httpclient.Cookie; import org.apache.commons.httpclient.HttpClient; @@ -297,7 +298,7 @@ public class JDKInstaller extends ToolInstaller { public interface FileSystem { void delete(String file) throws IOException, InterruptedException; void chmod(String file,int mode) throws IOException, InterruptedException; - InputStream read(String file) throws IOException; + InputStream read(String file) throws IOException, InterruptedException; /** * List sub-directories of the given directory and just return the file name portion. */ @@ -320,7 +321,7 @@ public class JDKInstaller extends ToolInstaller { $(file).chmod(mode); } - public InputStream read(String file) throws IOException { + public InputStream read(String file) throws IOException, InterruptedException { return $(file).read(); } @@ -530,7 +531,7 @@ public class JDKInstaller extends ToolInstaller { throw new DetectionFailedException("Unknown CPU name: "+arch); } - static class GetCurrentPlatform implements Callable { + static class GetCurrentPlatform extends MasterToSlaveCallable { private static final long serialVersionUID = 1L; public Platform call() throws DetectionFailedException { return current(); @@ -592,7 +593,7 @@ public class JDKInstaller extends ToolInstaller { throw new DetectionFailedException("Unknown CPU architecture: "+arch); } - static class GetCurrentCPU implements Callable { + static class GetCurrentCPU extends MasterToSlaveCallable { private static final long serialVersionUID = 1L; public CPU call() throws DetectionFailedException { return current(); diff --git a/core/src/main/java/hudson/tools/ZipExtractionInstaller.java b/core/src/main/java/hudson/tools/ZipExtractionInstaller.java index 90b1b5e8bf..38e536bfd3 100644 --- a/core/src/main/java/hudson/tools/ZipExtractionInstaller.java +++ b/core/src/main/java/hudson/tools/ZipExtractionInstaller.java @@ -26,7 +26,7 @@ package hudson.tools; import hudson.Extension; import hudson.FilePath; -import hudson.FilePath.FileCallable; +import jenkins.MasterToSlaveFileCallable; import hudson.ProxyConfiguration; import hudson.Util; import hudson.Functions; @@ -116,7 +116,7 @@ public class ZipExtractionInstaller extends ToolInstaller { * Sets execute permission on all files, since unzip etc. might not do this. * Hackish, is there a better way? */ - static class ChmodRecAPlusX implements FileCallable { + static class ChmodRecAPlusX extends MasterToSlaveFileCallable { private static final long serialVersionUID = 1L; public Void invoke(File d, VirtualChannel channel) throws IOException { if(!Functions.isWindows()) diff --git a/core/src/main/java/hudson/util/ProcessTree.java b/core/src/main/java/hudson/util/ProcessTree.java index 7e87519482..00d64f5d75 100644 --- a/core/src/main/java/hudson/util/ProcessTree.java +++ b/core/src/main/java/hudson/util/ProcessTree.java @@ -29,14 +29,13 @@ import com.sun.jna.ptr.IntByReference; import hudson.EnvVars; import hudson.FilePath; import hudson.Util; -import jenkins.model.Jenkins; -import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.slaves.SlaveComputer; import hudson.util.ProcessTree.OSProcess; import hudson.util.ProcessTreeRemoting.IOSProcess; import hudson.util.ProcessTreeRemoting.IProcessTree; +import jenkins.security.SlaveToMasterCallable; import org.jvnet.winp.WinProcess; import org.jvnet.winp.WinpException; @@ -150,7 +149,7 @@ public abstract class ProcessTree implements Iterable, IProcessTree, try { VirtualChannel channelToMaster = SlaveComputer.getChannelToMaster(); if (channelToMaster!=null) { - killers = channelToMaster.call(new Callable, IOException>() { + killers = channelToMaster.call(new SlaveToMasterCallable, IOException>() { public List call() throws IOException { return new ArrayList(ProcessKiller.all()); } diff --git a/core/src/main/java/hudson/util/RemotingDiagnostics.java b/core/src/main/java/hudson/util/RemotingDiagnostics.java index ac71de50e8..ba383e2081 100644 --- a/core/src/main/java/hudson/util/RemotingDiagnostics.java +++ b/core/src/main/java/hudson/util/RemotingDiagnostics.java @@ -29,11 +29,11 @@ import hudson.FilePath; import hudson.Functions; import jenkins.model.Jenkins; import hudson.remoting.AsyncFutureImpl; -import hudson.remoting.Callable; import hudson.remoting.DelegatingCallable; import hudson.remoting.Future; import hudson.remoting.VirtualChannel; import hudson.security.AccessControlled; +import jenkins.security.MasterToSlaveCallable; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.kohsuke.stapler.StaplerRequest; @@ -70,7 +70,7 @@ public final class RemotingDiagnostics { return channel.call(new GetSystemProperties()); } - private static final class GetSystemProperties implements Callable,RuntimeException> { + private static final class GetSystemProperties extends MasterToSlaveCallable,RuntimeException> { public Map call() { return new TreeMap(System.getProperties()); } @@ -89,7 +89,7 @@ public final class RemotingDiagnostics { return channel.callAsync(new GetThreadDump()); } - private static final class GetThreadDump implements Callable,RuntimeException> { + private static final class GetThreadDump extends MasterToSlaveCallable,RuntimeException> { public Map call() { Map r = new LinkedHashMap(); ThreadInfo[] data = Functions.getThreadInfos(); @@ -108,7 +108,7 @@ public final class RemotingDiagnostics { return channel.call(new Script(script)); } - private static final class Script implements DelegatingCallable { + private static final class Script extends MasterToSlaveCallable implements DelegatingCallable { private final String script; private transient ClassLoader cl; @@ -150,7 +150,7 @@ public final class RemotingDiagnostics { * Obtains the heap dump in an HPROF file. */ public static FilePath getHeapDump(VirtualChannel channel) throws IOException, InterruptedException { - return channel.call(new Callable() { + return channel.call(new MasterToSlaveCallable() { public FilePath call() throws IOException { final File hprof = File.createTempFile("hudson-heapdump", "hprof"); hprof.delete(); diff --git a/core/src/main/java/hudson/util/io/ParserConfigurator.java b/core/src/main/java/hudson/util/io/ParserConfigurator.java index 98f1397eeb..bac60c7e79 100644 --- a/core/src/main/java/hudson/util/io/ParserConfigurator.java +++ b/core/src/main/java/hudson/util/io/ParserConfigurator.java @@ -25,9 +25,9 @@ package hudson.util.io; import hudson.ExtensionList; import hudson.ExtensionPoint; -import hudson.remoting.Callable; import hudson.remoting.Channel; import jenkins.model.Jenkins; +import jenkins.security.SlaveToMasterCallable; import org.dom4j.io.SAXReader; import java.io.IOException; @@ -78,7 +78,7 @@ public abstract class ParserConfigurator implements ExtensionPoint, Serializable if (Jenkins.getInstance()==null) { Channel ch = Channel.current(); if (ch!=null) - all = ch.call(new Callable, IOException>() { + all = ch.call(new SlaveToMasterCallable, IOException>() { private static final long serialVersionUID = -2178106894481500733L; diff --git a/core/src/main/java/jenkins/FilePathFilter.java b/core/src/main/java/jenkins/FilePathFilter.java new file mode 100644 index 0000000000..32dd7a3396 --- /dev/null +++ b/core/src/main/java/jenkins/FilePathFilter.java @@ -0,0 +1,106 @@ +package jenkins; + +import hudson.FilePath; +import hudson.remoting.Channel; +import hudson.remoting.ChannelBuilder; + +import javax.annotation.CheckForNull; +import java.io.File; + +/** + * Inspects {@link FilePath} access from remote channels. + * + * @author Kohsuke Kawaguchi + * @see FilePath + * @since 1.THU + */ +public abstract class FilePathFilter { + /** + * Checks if the given file/directory can be read. + * + * On POSIX, this corresponds to the 'r' permission of the file/directory itself. + */ + public void read(File f) throws SecurityException {} + + /** + * Checks if the given file can be written. + * + * On POSIX, this corresponds to the 'w' permission of the file itself. + */ + public void write(File f) throws SecurityException {} + + /** + * Checks if the given directory can be created. + * + * On POSIX, this corresponds to the 'w' permission of the parent directory. + */ + public void mkdirs(File f) throws SecurityException {} + + /** + * Checks if the given file can be created. + * + * On POSIX, this corresponds to the 'w' permission of the parent directory. + */ + public void create(File f) throws SecurityException {} + + /** + * Checks if the given file/directory can be deleted. + * + * On POSIX, this corresponds to the 'w' permission of the parent directory. + */ + public void delete(File f) throws SecurityException {} + + /** + * Checks if the metadata of the given file/directory (as opposed to the content) can be accessed. + * + * On POSIX, this corresponds to the 'r' permission of the parent directory. + */ + public void stat(File f) throws SecurityException {} + + + public final void installTo(ChannelBuilder cb) { + synchronized (cb) { + FilePathFilterAggregator filters = (FilePathFilterAggregator) cb.getProperties().get(FilePathFilterAggregator.KEY); + if (filters==null) { + filters = new FilePathFilterAggregator(); + cb.withProperty(FilePathFilterAggregator.KEY,filters); + } + filters.add(this); + } + } + + public final void uninstallFrom(Channel ch) { + synchronized (ch) { + FilePathFilterAggregator filters = ch.getProperty(FilePathFilterAggregator.KEY); + if (filters!=null) { + filters.remove(this); + } + } + } + + /** + * Returns an {@link FilePathFilter} object that represents all the in-scope filters, + * or null if none is needed. + */ + public static @CheckForNull FilePathFilter current() { + Channel ch = Channel.current(); + if (ch==null) return null; + + return ch.getProperty(FilePathFilterAggregator.KEY); + } + + /** + * Immutable instance that represents the empty filter. + */ + public static final FilePathFilter EMPTY = new FilePathFilterAggregator() { + @Override + public void add(FilePathFilter f) { + // noop + } + + @Override + public String toString() { + return "None"; + } + }; +} diff --git a/core/src/main/java/jenkins/FilePathFilterAggregator.java b/core/src/main/java/jenkins/FilePathFilterAggregator.java new file mode 100644 index 0000000000..3796904ca2 --- /dev/null +++ b/core/src/main/java/jenkins/FilePathFilterAggregator.java @@ -0,0 +1,68 @@ +package jenkins; + +import hudson.FilePath; +import hudson.remoting.ChannelProperty; + +import java.io.File; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Maintains a bundle of {@link FilePathFilter} and implement a hook that broadcasts to all the filters. + * + * Accessible as channel property. + * + * @author Kohsuke Kawaguchi + * @see FilePath + * @since 1.THU + */ +class FilePathFilterAggregator extends FilePathFilter { + private final CopyOnWriteArrayList all = new CopyOnWriteArrayList(); + + public void add(FilePathFilter f) { + all.add(f); + } + + public void remove(FilePathFilter f) { + all.remove(f); + } + + public void read(File f) throws SecurityException { + for (FilePathFilter filter : all) { + filter.read(f); + } + } + + @Override + public void mkdirs(File f) throws SecurityException { + for (FilePathFilter filter : all) { + filter.mkdirs(f); + } + } + + public void write(File f) throws SecurityException { + for (FilePathFilter filter : all) { + filter.write(f); + } + } + public void create(File f) throws SecurityException { + for (FilePathFilter filter : all) { + filter.create(f); + } + } + public void delete(File f) throws SecurityException { + for (FilePathFilter filter : all) { + filter.delete(f); + } + } + public void stat(File f) throws SecurityException { + for (FilePathFilter filter : all) { + filter.stat(f); + } + } + + @Override public String toString() { + return "FilePathFilterAggregator" + all; + } + + static final ChannelProperty KEY = new ChannelProperty(FilePathFilterAggregator.class, "FilePathFilters"); +} diff --git a/core/src/main/java/jenkins/MasterToSlaveFileCallable.java b/core/src/main/java/jenkins/MasterToSlaveFileCallable.java new file mode 100644 index 0000000000..cad6813c18 --- /dev/null +++ b/core/src/main/java/jenkins/MasterToSlaveFileCallable.java @@ -0,0 +1,18 @@ +package jenkins; + +import hudson.FilePath.FileCallable; +import jenkins.security.Roles; +import org.jenkinsci.remoting.RoleChecker; + +/** + * {@link FileCallable}s that are meant to be only used on the master. + * + * @since 1.THU + */ +public abstract class MasterToSlaveFileCallable implements FileCallable { + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + checker.check(this, Roles.SLAVE); + } + private static final long serialVersionUID = 1L; +} diff --git a/core/src/main/java/jenkins/SlaveToMasterFileCallable.java b/core/src/main/java/jenkins/SlaveToMasterFileCallable.java new file mode 100644 index 0000000000..f196a88cf3 --- /dev/null +++ b/core/src/main/java/jenkins/SlaveToMasterFileCallable.java @@ -0,0 +1,18 @@ +package jenkins; + +import hudson.FilePath.FileCallable; +import jenkins.security.Roles; +import org.jenkinsci.remoting.RoleChecker; + +/** + * {@link FileCallable}s that can be executed on the master, sent by the slave. + * + * @since 1.THU + */ +public abstract class SlaveToMasterFileCallable implements FileCallable { + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + checker.check(this, Roles.MASTER); + } + private static final long serialVersionUID = 1L; +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 415e037f95..ffd693e6fd 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -196,6 +196,7 @@ import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy; import jenkins.security.ConfidentialKey; import jenkins.security.ConfidentialStore; import jenkins.security.SecurityListener; +import jenkins.security.MasterToSlaveCallable; import jenkins.slaves.WorkspaceLocator; import jenkins.util.Timer; import jenkins.util.io.FileBoolean; @@ -2034,7 +2035,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @Override public Callable getClockDifferenceCallable() { - return new Callable() { + return new MasterToSlaveCallable() { public ClockDifference call() throws IOException { return new ClockDifference(0); } diff --git a/core/src/main/java/jenkins/security/CallableDirectionChecker.java b/core/src/main/java/jenkins/security/CallableDirectionChecker.java new file mode 100644 index 0000000000..e9698a1cc9 --- /dev/null +++ b/core/src/main/java/jenkins/security/CallableDirectionChecker.java @@ -0,0 +1,132 @@ +package jenkins.security; + +import hudson.Extension; +import hudson.remoting.Callable; +import hudson.remoting.ChannelBuilder; +import org.jenkinsci.remoting.Role; +import org.jenkinsci.remoting.RoleChecker; +import org.jenkinsci.remoting.RoleSensitive; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Inspects {@link Callable}s that run on the master. + * + * @author Kohsuke Kawaguchi + * @since 1.THU + */ +@Restricted(NoExternalUse.class) // used implicitly via listener +public class CallableDirectionChecker extends RoleChecker { + + private static final String BYPASS_PROP = CallableDirectionChecker.class.getName()+".allow"; + + /** + * Switch to disable all the defense mechanism completely. + * + * This is an escape hatch in case the fix breaks something critical, to allow the user + * to keep operation. + */ + public static boolean BYPASS = Boolean.getBoolean(BYPASS_PROP); + + /** + * Whitelist of {@link RoleSensitive} class names (mostly {@link Callable} classes) that are allowed + * to pass through. + */ + public static ConcurrentMap BYPASS_CALLABLES = new ConcurrentHashMap(); + + /** + * Test feature that is meant to replace all logging, and log what would have been violations. + * TODO delete before release + */ + private static final PrintWriter BYPASS_LOG; + + static { + String log = System.getProperty(CallableDirectionChecker.class.getName()+".log"); + if (log == null) { + BYPASS_LOG = null; + } else { + try { + BYPASS_LOG = new PrintWriter(new OutputStreamWriter(new FileOutputStream(log, true)), true); + } catch (FileNotFoundException x) { + throw new ExceptionInInitializerError(x); + } + } + } + + private CallableDirectionChecker() {} + + @Override + public void check(RoleSensitive subject, @Nonnull Collection expected) throws SecurityException { + final String name = subject.getClass().getName(); + + if (expected.contains(Roles.MASTER)) { + if (BYPASS_LOG == null) { + LOGGER.log(Level.FINE, "Executing {0} is allowed since it is targeted for the master role", name); + } + return; // known to be safe + } + + if (BYPASS_LOG != null) { + BYPASS_LOG.println(name); + return; + } + + if (isWhitelisted(name)) { + // this subject is dubious, but we are letting it through as per whitelisting + LOGGER.log(Level.FINE, "Explicitly allowing {0} to be sent from slave to master", name); + return; + } + + throw new SecurityException("Sending " + name + " from slave to master is prohibited"); + } + + /** + * Is this subject class name whitelisted? + */ + private boolean isWhitelisted(String name) { + if (BYPASS) // everything is whitelisted + return true; + + if (BYPASS_CALLABLES.containsKey(name)) + return true; + + if (Boolean.getBoolean(BYPASS_PROP+"."+name)) { + BYPASS_CALLABLES.put(name,null); + return true; + } + + return false; + } + + /** + * Installs {@link CallableDirectionChecker} to every channel. + */ + @Restricted(DoNotUse.class) // impl + @Extension + public static class ChannelConfiguratorImpl extends ChannelConfigurator { + @Override + public void onChannelBuilding(ChannelBuilder builder, Object context) { + // if the big red emergency button is pressed, then we need to disable the defense mechanism, + // including enabling classloading. + if (!BYPASS && BYPASS_LOG == null) { + builder.withRemoteClassLoadingAllowed(false); + } + // In either of the above cases, the check method will return normally, but may log things. + builder.withRoleChecker(new CallableDirectionChecker()); + } + } + + private static final Logger LOGGER = Logger.getLogger(CallableDirectionChecker.class.getName()); +} diff --git a/core/src/main/java/jenkins/security/ChannelConfigurator.java b/core/src/main/java/jenkins/security/ChannelConfigurator.java new file mode 100644 index 0000000000..a371ae5401 --- /dev/null +++ b/core/src/main/java/jenkins/security/ChannelConfigurator.java @@ -0,0 +1,44 @@ +package jenkins.security; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.remoting.Channel; +import hudson.remoting.ChannelBuilder; +import hudson.slaves.SlaveComputer; +import jenkins.model.Jenkins; + +import javax.annotation.Nullable; + +/** + * Intercepts the new creation of {@link Channel} and tweak its configuration. + * + * @author Kohsuke Kawaguchi + * @since 1.THU + */ +public abstract class ChannelConfigurator implements ExtensionPoint { + /** + * Called whenever a new channel is being built. + * + * @param builder + * Configures the newly built channel. The callee + * can call its methods to modify its settings. + * @param context + * The parameter that helps the callee determines what this channel is for. + * Legacy callers do not always provide this information, in which case this value might be null. + * + * Possible known values include: + * + *
    + *
    {@link SlaveComputer} + *
    When a channel is being established to talk to a slave. + *
    + */ + public void onChannelBuilding(ChannelBuilder builder, @Nullable Object context) {} + + /** + * All the registered {@link ChannelConfigurator}s. + */ + public static ExtensionList all() { + return Jenkins.getInstance().getExtensionList(ChannelConfigurator.class); + } +} diff --git a/core/src/main/java/jenkins/security/DefaultFilePathFilter.java b/core/src/main/java/jenkins/security/DefaultFilePathFilter.java new file mode 100644 index 0000000000..e6c2ddd7da --- /dev/null +++ b/core/src/main/java/jenkins/security/DefaultFilePathFilter.java @@ -0,0 +1,106 @@ +/* + * The MIT License + * + * Copyright 2014 Jesse Glick. + * + * 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 jenkins.security; + +import hudson.Extension; +import hudson.FilePath; +import hudson.model.Computer; +import hudson.model.TaskListener; +import hudson.remoting.Channel; +import hudson.remoting.ChannelBuilder; +import hudson.slaves.ComputerListener; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.FilePathFilter; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; + +/** + * Blocks slaves from writing to files on the master by default. + */ +@Restricted(DoNotUse.class) // impl +@Extension public class DefaultFilePathFilter extends ChannelConfigurator { + + /** + * Escape hatch to disable this check completely. + */ + public static boolean BYPASS = Boolean.getBoolean(DefaultFilePathFilter.class.getName()+".allow"); + private static final PrintWriter BYPASS_LOG; // TODO delete before release + static { + String log = System.getProperty("jenkins.security.DefaultFilePathFilter.log"); + if (log == null) { + BYPASS_LOG = null; + } else { + try { + BYPASS_LOG = new PrintWriter(new OutputStreamWriter(new FileOutputStream(log, true)), true); + } catch (FileNotFoundException x) { + throw new ExceptionInInitializerError(x); + } + } + } + private static final Logger LOGGER = Logger.getLogger(DefaultFilePathFilter.class.getName()); + + @Override + public void onChannelBuilding(ChannelBuilder builder, Object context) { + new FilePathFilter() { + private void op(String op, File f) throws SecurityException { + if (BYPASS_LOG != null) { + BYPASS_LOG.println(op + " " + f); + return; + } + if (BYPASS) { + LOGGER.log(Level.FINE, "slave allowed to {0} {1}", new Object[] {op, f}); + } else { + // TODO allow finer-grained control, for example by regexp (or Ant pattern) of relative path inside $JENKINS_HOME + throw new SecurityException("slave may not " + op + " " + f); + } + } + @Override public void read(File f) throws SecurityException { + op("read", f); + } + @Override public void write(File f) throws SecurityException { + op("write", f); + } + @Override public void mkdirs(File f) throws SecurityException { + op("mkdirs", f); + } + @Override public void create(File f) throws SecurityException { + op("create", f); + } + @Override public void delete(File f) throws SecurityException { + op("delete", f); + } + @Override public void stat(File f) throws SecurityException { + op("stat", f); + } + }.installTo(builder); + } +} diff --git a/core/src/main/java/jenkins/security/MasterToSlaveCallable.java b/core/src/main/java/jenkins/security/MasterToSlaveCallable.java new file mode 100644 index 0000000000..8be5b98305 --- /dev/null +++ b/core/src/main/java/jenkins/security/MasterToSlaveCallable.java @@ -0,0 +1,22 @@ +package jenkins.security; + +import hudson.remoting.Callable; +import org.jenkinsci.remoting.Role; +import org.jenkinsci.remoting.RoleChecker; + +import java.util.Collection; + +/** + * Convenient {@link Callable} meant to be run on slave. + * + * @author Kohsuke Kawaguchi + * @since 1.THU + */ +public abstract class MasterToSlaveCallable implements Callable { + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + checker.check(this,Roles.SLAVE); + } + + private static final long serialVersionUID = 1L; +} diff --git a/core/src/main/java/jenkins/security/NotReallyRoleSensitiveCallable.java b/core/src/main/java/jenkins/security/NotReallyRoleSensitiveCallable.java new file mode 100644 index 0000000000..953c5ba69d --- /dev/null +++ b/core/src/main/java/jenkins/security/NotReallyRoleSensitiveCallable.java @@ -0,0 +1,19 @@ +package jenkins.security; + +import hudson.remoting.Callable; +import org.jenkinsci.remoting.RoleChecker; + +/** + * {@link Callable} adapter for situations where Callable is not used for remoting but + * just as a convenient function that has parameterized return value and exception type. + * + * @author Kohsuke Kawaguchi + * @since 1.THU + */ +public abstract class NotReallyRoleSensitiveCallable implements Callable { + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + // not meant to be used where this matters + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/jenkins/security/Roles.java b/core/src/main/java/jenkins/security/Roles.java new file mode 100644 index 0000000000..cca26873a5 --- /dev/null +++ b/core/src/main/java/jenkins/security/Roles.java @@ -0,0 +1,34 @@ +package jenkins.security; + +import org.jenkinsci.remoting.Role; + +/** + * Predefined {@link Role}s in Jenkins. + * + *

    + * In Jenkins, there is really only one interesting role, which is the Jenkins master. + * Slaves, CLI, and Maven processes are all going to load classes from the master, + * which means it accepts anything that the master asks for, and thus they need + * not have any role. + * + * @author Kohsuke Kawaguchi + * @since 1.THU + */ +public class Roles { + /** + * Indicates that a callable runs on masters, requested by slaves/CLI/maven/whatever. + */ + public static final Role MASTER = new Role("master"); + + /** + * Indicates that a callable is meant to run on slaves. + * + * This isn't used to reject callables to run on the slave, but rather to allow + * the master to promptly reject callables that are really not meant to be run on + * the master (as opposed to ones that do not have that information, which gets + * {@link Role#UNKNOWN}) + */ + public static final Role SLAVE = new Role("slave"); + + private Roles() {} +} diff --git a/core/src/main/java/jenkins/security/SlaveToMasterCallable.java b/core/src/main/java/jenkins/security/SlaveToMasterCallable.java new file mode 100644 index 0000000000..61265cbdea --- /dev/null +++ b/core/src/main/java/jenkins/security/SlaveToMasterCallable.java @@ -0,0 +1,22 @@ +package jenkins.security; + +import hudson.remoting.Callable; +import org.jenkinsci.remoting.Role; +import org.jenkinsci.remoting.RoleChecker; + +import java.util.Collection; + +/** + * Convenient {@link Callable} that are meant to run on the master (sent by slave/CLI/etc). + * + * @author Kohsuke Kawaguchi + * @since 1.THU + */ +public abstract class SlaveToMasterCallable implements Callable { + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + checker.check(this,Roles.MASTER); + } + + private static final long serialVersionUID = 1L; +} diff --git a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java index 4abcf3c409..51b7e2af74 100644 --- a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java +++ b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java @@ -4,11 +4,11 @@ import hudson.Extension; import hudson.FilePath; import hudson.model.Computer; import hudson.model.TaskListener; -import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.StandardOutputStream; import hudson.slaves.ComputerListener; import hudson.util.jna.GNUCLibrary; +import jenkins.security.MasterToSlaveCallable; import java.io.File; import java.io.FileDescriptor; @@ -35,7 +35,7 @@ public class StandardOutputSwapper extends ComputerListener { } } - private static final class ChannelSwapper implements Callable { + private static final class ChannelSwapper extends MasterToSlaveCallable { public Boolean call() throws Exception { if (File.pathSeparatorChar==';') return false; // Windows diff --git a/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java b/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java index 4ee82bee31..aee8ffcd45 100644 --- a/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java +++ b/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java @@ -3,7 +3,6 @@ package jenkins.slaves.restarter; import hudson.Extension; import hudson.model.Computer; import hudson.model.TaskListener; -import hudson.remoting.Callable; import hudson.remoting.Engine; import hudson.remoting.EngineListener; import hudson.remoting.EngineListenerAdapter; @@ -19,6 +18,7 @@ import java.util.List; import java.util.logging.Logger; import static java.util.logging.Level.*; +import jenkins.security.MasterToSlaveCallable; /** * Actual slave restart logic. @@ -49,7 +49,7 @@ public class JnlpSlaveRestarterInstaller extends ComputerListener implements Ser VirtualChannel ch = c.getChannel(); if (ch==null) return; // defensive check - List effective = ch.call(new Callable, IOException>() { + List effective = ch.call(new MasterToSlaveCallable, IOException>() { public List call() throws IOException { Engine e = Engine.current(); if (e == null) return null; // not running under Engine diff --git a/core/src/main/java/jenkins/util/VirtualFile.java b/core/src/main/java/jenkins/util/VirtualFile.java index 1a54d08545..3ccd3f09c6 100644 --- a/core/src/main/java/jenkins/util/VirtualFile.java +++ b/core/src/main/java/jenkins/util/VirtualFile.java @@ -41,6 +41,8 @@ import java.util.ArrayList; import java.util.List; import javax.annotation.Nonnull; +import jenkins.MasterToSlaveFileCallable; + /** * Abstraction over {@link File}, {@link FilePath}, or other items such as network resources or ZIP entries. * Assumed to be read-only and makes very limited assumptions, just enough to display content and traverse directories. @@ -354,7 +356,11 @@ public abstract class VirtualFile implements Comparable, Serializab } } @Override public InputStream open() throws IOException { - return f.read(); + try { + return f.read(); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } } @Override public V run(Callable callable) throws IOException { try { @@ -364,7 +370,7 @@ public abstract class VirtualFile implements Comparable, Serializab } } } - private static final class Scanner implements FilePath.FileCallable { + private static final class Scanner extends MasterToSlaveFileCallable { private final String glob; Scanner(String glob) { this.glob = glob; @@ -381,7 +387,7 @@ public abstract class VirtualFile implements Comparable, Serializab } } - private static final class Readable implements FilePath.FileCallable { + private static final class Readable extends MasterToSlaveFileCallable { @Override public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { return f.canRead(); } diff --git a/core/src/test/java/hudson/LauncherTest.java b/core/src/test/java/hudson/LauncherTest.java index a6eacda33a..bba913d8e6 100644 --- a/core/src/test/java/hudson/LauncherTest.java +++ b/core/src/test/java/hudson/LauncherTest.java @@ -29,6 +29,7 @@ import hudson.model.TaskListener; import hudson.remoting.Callable; import hudson.util.ProcessTree; import hudson.util.StreamTaskListener; +import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.FileUtils; import org.jvnet.hudson.test.Bug; @@ -73,7 +74,7 @@ public class LauncherTest extends ChannelTestCase { } } - private static final Callable NOOP = new Callable() { + private static final Callable NOOP = new MasterToSlaveCallable() { public Object call() throws RuntimeException { return null; } diff --git a/core/src/test/java/hudson/os/SUTester.java b/core/src/test/java/hudson/os/SUTester.java index 2f28a41303..c89d447c0c 100644 --- a/core/src/test/java/hudson/os/SUTester.java +++ b/core/src/test/java/hudson/os/SUTester.java @@ -2,6 +2,7 @@ package hudson.os; import hudson.remoting.Callable; import hudson.util.StreamTaskListener; +import jenkins.security.MasterToSlaveCallable; import java.io.FileOutputStream; @@ -10,7 +11,7 @@ import java.io.FileOutputStream; */ public class SUTester { public static void main(String[] args) throws Throwable { - SU.execute(StreamTaskListener.fromStdout(),"kohsuke","bogus",new Callable() { + SU.execute(StreamTaskListener.fromStdout(),"kohsuke","bogus",new MasterToSlaveCallable() { public Object call() throws Throwable { System.out.println("Touching /tmp/x"); new FileOutputStream("/tmp/x").close(); diff --git a/core/src/test/java/hudson/util/ProcessTreeTest.java b/core/src/test/java/hudson/util/ProcessTreeTest.java index 759d73bb3c..6b398f366e 100644 --- a/core/src/test/java/hudson/util/ProcessTreeTest.java +++ b/core/src/test/java/hudson/util/ProcessTreeTest.java @@ -5,6 +5,7 @@ import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; import hudson.util.ProcessTree.OSProcess; import hudson.util.ProcessTree.ProcessCallable; +import jenkins.security.MasterToSlaveCallable; import java.io.IOException; import java.io.Serializable; @@ -39,7 +40,7 @@ public class ProcessTreeTest extends ChannelTestCase { t.p.act(new ProcessCallableImpl()); } - private static class MyCallable implements Callable, Serializable { + private static class MyCallable extends MasterToSlaveCallable implements Serializable { public Tag call() throws IOException { Tag t = new Tag(); t.tree = ProcessTree.get(); diff --git a/core/src/test/java/hudson/util/io/ReopenableRotatingFileOutputStreamTest.java b/core/src/test/java/hudson/util/io/ReopenableRotatingFileOutputStreamTest.java index 4750cab10c..9129d06888 100644 --- a/core/src/test/java/hudson/util/io/ReopenableRotatingFileOutputStreamTest.java +++ b/core/src/test/java/hudson/util/io/ReopenableRotatingFileOutputStreamTest.java @@ -11,7 +11,7 @@ import java.io.PrintWriter; * @author Kohsuke Kawaguchi */ public class ReopenableRotatingFileOutputStreamTest extends TestCase { - public void testRotation() throws IOException { + public void testRotation() throws IOException, InterruptedException { File base = File.createTempFile("test", "log"); ReopenableRotatingFileOutputStream os = new ReopenableRotatingFileOutputStream(base,3); PrintWriter w = new PrintWriter(os,true); diff --git a/pom.xml b/pom.xml index be8e956e54..020222d53e 100644 --- a/pom.xml +++ b/pom.xml @@ -95,7 +95,7 @@ THE SOFTWARE. jenkins-jira 1.7.7 - 2.5 + 2.6.1-SNAPSHOT ${skipTests} 6 @@ -173,7 +173,7 @@ THE SOFTWARE. org.jenkins-ci.main remoting - 2.45 + 2.46-SNAPSHOT diff --git a/test/src/test/java/hudson/ProcTest.java b/test/src/test/java/hudson/ProcTest.java index 5eb7752768..6159fc6edd 100644 --- a/test/src/test/java/hudson/ProcTest.java +++ b/test/src/test/java/hudson/ProcTest.java @@ -3,13 +3,12 @@ package hudson; import hudson.Launcher.LocalLauncher; import hudson.Launcher.RemoteLauncher; import hudson.Proc.RemoteProc; -import hudson.remoting.Callable; -import hudson.remoting.Future; import hudson.remoting.Pipe; import hudson.remoting.VirtualChannel; import hudson.slaves.DumbSlave; import hudson.util.IOUtils; import hudson.util.StreamTaskListener; +import jenkins.security.MasterToSlaveCallable; import org.jvnet.hudson.test.Bug; import org.jvnet.hudson.test.HudsonTestCase; @@ -70,7 +69,7 @@ public class ProcTest extends HudsonTestCase { return ch; } - private static class ChannelFiller implements Callable { + private static class ChannelFiller extends MasterToSlaveCallable { private final OutputStream o; private ChannelFiller(OutputStream o) { diff --git a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java index 4bcd2469e7..d27ea875c4 100644 --- a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java +++ b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java @@ -23,6 +23,7 @@ */ package hudson.logging; +import jenkins.security.MasterToSlaveCallable; import org.jvnet.hudson.test.Url; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlForm; @@ -109,7 +110,7 @@ public class LogRecorderManagerTest { assertFalse(text, text.contains("msg #4")); } - private static final class Log implements Callable { + private static final class Log extends MasterToSlaveCallable { private final Level level; private final String logger; private final String message; diff --git a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java index ab9afba1ee..26e7c1a1a2 100644 --- a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java +++ b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java @@ -35,6 +35,7 @@ import hudson.remoting.Callable; import hudson.remoting.Which; import hudson.util.ArgumentListBuilder; +import jenkins.security.SlaveToMasterCallable; import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.TestExtension; @@ -154,7 +155,7 @@ public class JNLPLauncherTest extends HudsonTestCase { return c; } - private static class NoopTask implements Callable { + private static class NoopTask extends SlaveToMasterCallable { public String call() { return "done"; } diff --git a/test/src/test/java/jenkins/security/DefaultFilePathFilterTest.java b/test/src/test/java/jenkins/security/DefaultFilePathFilterTest.java new file mode 100644 index 0000000000..0b1a40b680 --- /dev/null +++ b/test/src/test/java/jenkins/security/DefaultFilePathFilterTest.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * + * Copyright 2014 Jesse Glick. + * + * 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 jenkins.security; + +import hudson.FilePath; +import hudson.model.Slave; +import hudson.remoting.Callable; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collection; + +import org.jenkinsci.remoting.Role; +import org.jenkinsci.remoting.RoleChecker; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsRule; + +public class DefaultFilePathFilterTest { + + @Rule public JenkinsRule r = new JenkinsRule(); + + @Test public void remotePath() throws Exception { + Slave s = r.createOnlineSlave(); + FilePath forward = s.getRootPath().child("forward"); + forward.write("hello", null); + assertEquals("hello", s.getRootPath().act(new LocalCallable(forward))); + FilePath reverse = new FilePath(new File(r.jenkins.root, "reverse")); + assertFalse(reverse.exists()); + try { + s.getChannel().call(new ReverseCallable(reverse)); + fail("should have failed"); + } catch (SecurityException x) { + // good + + // make sure that the stack trace contains the call site info to help assist diagnosis + StringWriter sw = new StringWriter(); + x.printStackTrace(new PrintWriter(sw)); + assertTrue(sw.toString().contains(DefaultFilePathFilterTest.class.getName()+".remotePath")); + } + assertFalse(reverse.exists()); + DefaultFilePathFilter.BYPASS = true; + s.getChannel().call(new ReverseCallable(reverse)); + assertTrue(reverse.exists()); + assertEquals("goodbye", reverse.readToString()); + } + + private static class LocalCallable implements Callable { + private final FilePath p; + LocalCallable(FilePath p) { + this.p = p; + } + @Override public String call() throws Exception { + assertFalse(p.isRemote()); + return p.readToString(); + } + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + throw new NoSuchMethodError(); // simulate legacy Callable impls + } + } + + private static class ReverseCallable implements Callable { + private final FilePath p; + ReverseCallable(FilePath p) { + this.p = p; + } + @Override public Void call() throws Exception { + assertTrue(p.isRemote()); + p.write("goodbye", null); + return null; + } + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + throw new NoSuchMethodError(); // simulate legacy Callable impls + } + } +}