Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.jenkinsci.plugins.credentialsbinding.impl;

import java.io.IOException;

import javax.annotation.Nonnull;

import org.jenkinsci.plugins.credentialsbinding.Binding;
import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor;

import com.cloudbees.plugins.credentials.common.StandardCredentials;

import hudson.FilePath;
import hudson.Launcher;
import hudson.model.Run;
import hudson.model.TaskListener;

/**
* 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, using {@link UnbindableDir}.
* This can only safely be used for binding implementations for which {@link BindingDescriptor#requiresWorkspace()}
* is true.
* @param <C> a kind of credentials
*/
public abstract class AbstractOnDiskBinding<C extends StandardCredentials> extends Binding<C> {

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

/**
* 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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileCredentials> {
public class FileBinding extends AbstractOnDiskBinding<FileCredentials> {

@DataBoundConstructor public FileBinding(String variable, String credentialsId) {
super(variable, credentialsId);
Expand All @@ -53,58 +49,34 @@ public class FileBinding extends Binding<FileCredentials> {
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 UnbindableDir.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 UnbindableDir.UnbinderImpl implementation
}
}

@Symbol("file")
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,21 @@
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

public class ZipFileBinding extends FileBinding {
public class ZipFileBinding extends AbstractOnDiskBinding<FileCredentials> {

@DataBoundConstructor public ZipFileBinding(String variable, String credentialsId) {
super(variable, credentialsId);
}

@Override protected void copy(FilePath secret, FileCredentials credentials) throws IOException, InterruptedException {
@Override protected Class<FileCredentials> 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")
Expand Down