From 83ba9e2fbf118923af4c8e69115011b1e48cb7bb Mon Sep 17 00:00:00 2001 From: Thomas de Grenier de Latour Date: Fri, 24 Mar 2017 14:59:09 +0100 Subject: [PATCH 1/2] ondisk-binding: start again from where we left in pull request #25 --- .../impl/AbstractOnDiskBinding.java | 84 +++++++++++++++++++ .../credentialsbinding/impl/FileBinding.java | 72 +++++----------- .../impl/ZipFileBinding.java | 11 ++- 3 files changed, 115 insertions(+), 52 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java new file mode 100644 index 00000000..37ad1bbc --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java @@ -0,0 +1,84 @@ +package org.jenkinsci.plugins.credentialsbinding.impl; + +import java.io.IOException; +import java.util.UUID; + +import javax.annotation.Nonnull; + +import org.jenkinsci.plugins.credentialsbinding.Binding; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import com.cloudbees.plugins.credentials.common.StandardCredentials; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.slaves.WorkspaceList; + +/** + * Base class for writing credentials to a file or directory, and binding its path to a single variable. Handles + * creation of a -rwx------ temporary directory, and its full deletion when unbinding. + * @param a kind of credentials + */ +public abstract class AbstractOnDiskBinding extends Binding { + + protected AbstractOnDiskBinding(String variable, String credentialsId) { + super(variable, credentialsId); + } + + @Override + public final SingleEnvironment bindSingle(@Nonnull Run build, + FilePath workspace, + Launcher launcher, + @Nonnull TaskListener listener) throws IOException, InterruptedException { + final FilePath secrets = secretsDir(workspace); + final String dirName = UUID.randomUUID().toString(); + final FilePath dir = secrets.child(dirName); + dir.mkdirs(); + secrets.chmod(0700); + dir.chmod(0700); + final FilePath secret = write(getCredentials(build), dir); + return new SingleEnvironment(secret.getRemote(), new UnbinderImpl(dirName)); + } + + /** + * Writes credentials under a given temporary directory, and returns their path (will be bound to the variable). + * @param credentials the credentials to bind + * @param dir a temporary directory where credentials should be written. You can assume it has already been created, + * with secure permissions. + * @return the path to the on-disk credentials, to be bound to the variable + * @throws IOException + * @throws InterruptedException + */ + abstract protected FilePath write(C credentials, FilePath dir) throws IOException, InterruptedException; + + @Restricted(NoExternalUse.class) + protected static class UnbinderImpl implements Unbinder { + private static final long serialVersionUID = 1; + private final String dirName; + + protected UnbinderImpl(String dirName) { + this.dirName = dirName; + } + + @Override + public void unbind(@Nonnull Run build, + FilePath workspace, + Launcher launcher, + @Nonnull TaskListener listener) throws IOException, InterruptedException { + secretsDir(workspace).child(dirName).deleteRecursive(); + } + } + + private static FilePath secretsDir(FilePath workspace) { + return tempDir(workspace).child("secretFiles"); + } + + // TODO 1.652 use WorkspaceList.tempDir + private static FilePath tempDir(FilePath ws) { + return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp"); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java index d8006282..0b883517 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java @@ -29,21 +29,17 @@ import hudson.Launcher; import hudson.model.Run; import hudson.model.TaskListener; -import hudson.slaves.WorkspaceList; + import java.io.IOException; -import java.util.UUID; -import org.jenkinsci.Symbol; -import org.jenkinsci.plugins.credentialsbinding.Binding; +import org.jenkinsci.Symbol; import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor; import org.jenkinsci.plugins.plaincredentials.FileCredentials; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -public class FileBinding extends Binding { +public class FileBinding extends AbstractOnDiskBinding { @DataBoundConstructor public FileBinding(String variable, String credentialsId) { super(variable, credentialsId); @@ -53,58 +49,34 @@ public class FileBinding extends Binding { return FileCredentials.class; } - @Override public SingleEnvironment bindSingle(@Nonnull Run build, - FilePath workspace, - Launcher launcher, - @Nonnull TaskListener listener) throws IOException, InterruptedException { - FileCredentials credentials = getCredentials(build); - FilePath secrets = secretsDir(workspace); - String dirName = UUID.randomUUID().toString(); - final FilePath dir = secrets.child(dirName); - dir.mkdirs(); - secrets.chmod(/*0700*/448); + @Override protected final FilePath write(FileCredentials credentials, FilePath dir) throws IOException, InterruptedException { FilePath secret = dir.child(credentials.getFileName()); - copy(secret, credentials); - if (secret.isDirectory()) { /* ZipFileBinding */ - // needs to be writable so we can delete its contents - // needs to be executable so we can list the contents - secret.chmod(0700); - } else { - secret.chmod(0400); - } - return new SingleEnvironment(secret.getRemote(), new UnbinderImpl(dirName)); + secret.copyFrom(credentials.getContent()); + secret.chmod(0400); + return secret; } - - private static class UnbinderImpl implements Unbinder { + @SuppressWarnings("unused") + @Deprecated + private static class UnbinderImpl implements Unbinder { private static final long serialVersionUID = 1; - private final String dirName; - - UnbinderImpl(String dirName) { + + private UnbinderImpl(String dirName) { this.dirName = dirName; } - - @Override public void unbind(@Nonnull Run build, - FilePath workspace, - Launcher launcher, - @Nonnull TaskListener listener) throws IOException, InterruptedException { - secretsDir(workspace).child(dirName).deleteRecursive(); - } - - } - - private static FilePath secretsDir(FilePath workspace) { - return tempDir(workspace).child("secretFiles"); - } - // TODO 1.652 use WorkspaceList.tempDir - private static FilePath tempDir(FilePath ws) { - return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp"); - } + protected Object readResolve() { + return new AbstractOnDiskBinding.UnbinderImpl(dirName); + } - protected void copy(FilePath secret, FileCredentials credentials) throws IOException, InterruptedException { - secret.copyFrom(credentials.getContent()); + @Override + public void unbind(@Nonnull Run build, + FilePath workspace, + Launcher launcher, + @Nonnull TaskListener listener) throws IOException, InterruptedException { + // replaced by the AbstractOnDiskBinding.UnbinderImpl implementation + } } @Symbol("file") diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/ZipFileBinding.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/ZipFileBinding.java index cfd7d358..28d391dd 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/ZipFileBinding.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/ZipFileBinding.java @@ -44,14 +44,21 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; -public class ZipFileBinding extends FileBinding { +public class ZipFileBinding extends AbstractOnDiskBinding { @DataBoundConstructor public ZipFileBinding(String variable, String credentialsId) { super(variable, credentialsId); } - @Override protected void copy(FilePath secret, FileCredentials credentials) throws IOException, InterruptedException { + @Override protected Class type() { + return FileCredentials.class; + } + + @Override protected final FilePath write(FileCredentials credentials, FilePath dir) throws IOException, InterruptedException { + FilePath secret = dir.child(credentials.getFileName()); secret.unzipFrom(credentials.getContent()); + secret.chmod(0700); // note: it's a directory + return secret; } @Symbol("zip") From 8c50bd361e364f223704f073863002dfa7f62388 Mon Sep 17 00:00:00 2001 From: Thomas de Grenier de Latour Date: Thu, 18 May 2017 18:44:33 +0200 Subject: [PATCH 2/2] UnbindableDir is a convenience class for creating a temp dir and its Unbinder --- .../impl/AbstractOnDiskBinding.java | 50 +++-------- .../credentialsbinding/impl/FileBinding.java | 4 +- .../impl/UnbindableDir.java | 88 +++++++++++++++++++ 3 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/UnbindableDir.java diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java index 37ad1bbc..cccaee1d 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/AbstractOnDiskBinding.java @@ -1,13 +1,11 @@ package org.jenkinsci.plugins.credentialsbinding.impl; import java.io.IOException; -import java.util.UUID; import javax.annotation.Nonnull; import org.jenkinsci.plugins.credentialsbinding.Binding; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor; import com.cloudbees.plugins.credentials.common.StandardCredentials; @@ -15,11 +13,12 @@ import hudson.Launcher; import hudson.model.Run; import hudson.model.TaskListener; -import hudson.slaves.WorkspaceList; /** * Base class for writing credentials to a file or directory, and binding its path to a single variable. Handles - * creation of a -rwx------ temporary directory, and its full deletion when unbinding. + * creation of a -rwx------ temporary directory, and its full deletion when unbinding, using {@link UnbindableDir}. + * This can only safely be used for binding implementations for which {@link BindingDescriptor#requiresWorkspace()} + * is true. * @param a kind of credentials */ public abstract class AbstractOnDiskBinding extends Binding { @@ -33,14 +32,12 @@ public final SingleEnvironment bindSingle(@Nonnull Run build, FilePath workspace, Launcher launcher, @Nonnull TaskListener listener) throws IOException, InterruptedException { - final FilePath secrets = secretsDir(workspace); - final String dirName = UUID.randomUUID().toString(); - final FilePath dir = secrets.child(dirName); - dir.mkdirs(); - secrets.chmod(0700); - dir.chmod(0700); - final FilePath secret = write(getCredentials(build), dir); - return new SingleEnvironment(secret.getRemote(), new UnbinderImpl(dirName)); + if (workspace == null) { + throw new IllegalArgumentException("This Binding implementation requires a non-null workspace"); + } + final UnbindableDir dir = UnbindableDir.create(workspace); + final FilePath secret = write(getCredentials(build), dir.getDirPath()); + return new SingleEnvironment(secret.getRemote(), dir.getUnbinder()); } /** @@ -54,31 +51,4 @@ public final SingleEnvironment bindSingle(@Nonnull Run build, */ abstract protected FilePath write(C credentials, FilePath dir) throws IOException, InterruptedException; - @Restricted(NoExternalUse.class) - protected static class UnbinderImpl implements Unbinder { - private static final long serialVersionUID = 1; - private final String dirName; - - protected UnbinderImpl(String dirName) { - this.dirName = dirName; - } - - @Override - public void unbind(@Nonnull Run build, - FilePath workspace, - Launcher launcher, - @Nonnull TaskListener listener) throws IOException, InterruptedException { - secretsDir(workspace).child(dirName).deleteRecursive(); - } - } - - private static FilePath secretsDir(FilePath workspace) { - return tempDir(workspace).child("secretFiles"); - } - - // TODO 1.652 use WorkspaceList.tempDir - private static FilePath tempDir(FilePath ws) { - return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp"); - } - } diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java index 0b883517..56d7ec3d 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/FileBinding.java @@ -67,7 +67,7 @@ private UnbinderImpl(String dirName) { } protected Object readResolve() { - return new AbstractOnDiskBinding.UnbinderImpl(dirName); + return new UnbindableDir.UnbinderImpl(dirName); } @Override @@ -75,7 +75,7 @@ public void unbind(@Nonnull Run build, FilePath workspace, Launcher launcher, @Nonnull TaskListener listener) throws IOException, InterruptedException { - // replaced by the AbstractOnDiskBinding.UnbinderImpl implementation + // replaced by the UnbindableDir.UnbinderImpl implementation } } diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/UnbindableDir.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/UnbindableDir.java new file mode 100644 index 00000000..ccfa9b79 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/UnbindableDir.java @@ -0,0 +1,88 @@ +package org.jenkinsci.plugins.credentialsbinding.impl; + +import java.io.IOException; +import java.util.UUID; + +import javax.annotation.Nonnull; + +import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor; +import org.jenkinsci.plugins.credentialsbinding.MultiBinding.Unbinder; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.slaves.WorkspaceList; + +/** + * Convenience class for creating a secure temporary directory dedicated to writing credentials file(s), and getting a + * corresponding {@link Unbinder} instance. + */ +public class UnbindableDir { + + private final FilePath dirPath; + private final Unbinder unbinder; + + private UnbindableDir(FilePath dirPath) { + this.dirPath = dirPath; + this.unbinder = new UnbinderImpl(dirPath.getName()); + } + + public Unbinder getUnbinder() { + return unbinder; + } + + public FilePath getDirPath() { + return dirPath; + } + + /** + * Creates a new, secure, directory under a base workspace temporary directory. Also instantiates + * an {@link Unbinder} for deleting this directory later. This can only safely be used for binding + * implementations for which {@link BindingDescriptor#requiresWorkspace()} is true. + * @param workspace The workspace, can't be null (temporary dirs are created next to it) + * @return + * @throws IOException + * @throws InterruptedException + */ + public static UnbindableDir create(@Nonnull FilePath workspace) + throws IOException, InterruptedException { + final FilePath secrets = secretsDir(workspace); + final String dirName = UUID.randomUUID().toString(); + final FilePath dir = secrets.child(dirName); + dir.mkdirs(); + secrets.chmod(0700); + dir.chmod(0700); + return new UnbindableDir(dir); + } + + private static FilePath secretsDir(FilePath workspace) { + return tempDir(workspace).child("secretFiles"); + } + + // TODO 1.652 use WorkspaceList.tempDir + private static FilePath tempDir(FilePath ws) { + return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp"); + } + + @Restricted(NoExternalUse.class) + protected static class UnbinderImpl implements Unbinder { + private static final long serialVersionUID = 1; + private final String dirName; + + protected UnbinderImpl(String dirName) { + this.dirName = dirName; + } + + @Override + public void unbind(@Nonnull Run build, + FilePath workspace, + Launcher launcher, + @Nonnull TaskListener listener) throws IOException, InterruptedException { + secretsDir(workspace).child(dirName).deleteRecursive(); + } + } + +}