Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the GitUsernamePasswordBinding by exporting the credentials suitable for git credential store #1221

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
25 changes: 25 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,31 @@ withCredentials([gitUsernamePassword(credentialsId: 'my-credentials-id',
powershell 'git push'
}
```

===== Docker BuildKit secret mount

To securely forward credentials to `docker.build` the link:https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md#run---mounttypesecret[`--mount=type=secret`] approach should be used. This avoids storing the credentials into the image like `ARG` or `ENV` would. When the optinal parameter `hostName` is specified, this binding also provides `GIT_CREDENTIAL_STORE` environment variable which contains path to link:https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage[git credentials store].

.Docker BuildKit example
```groovy
# Jenkins pipeline
withCredentials([gitUsernamePassword(credentialsId: 'my-credentials-id',
hostName: 'github.com')]) {
withEnv(['DOCKER_BUILDKIT=1']) {
docker.build '', "--secret id=git_store,src=${GIT_CREDENTIAL_STORE} ."
}
}
```
```dockerfile
# Dockerfile
RUN --mount=type=secret,id=git_store \
git config --global credential.helper 'store --file /run/secrets/git_store' && \
git clone https://github.com/private/repo
```
The above configuration is best used with the link:https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc[GitHub App authentication] provided by link:https://plugins.jenkins.io/github-branch-source/[GitHub Branch Source plugin]. This issues scoped temporary token valid for one hour, which is then used in HTTPS Basic Auth.

This removes the need for _service accounts_ configured with static SSH keys or Personal Access Tokens and the management burden associated with these.

[#configuration]
== [[GitPlugin-ProjectConfiguration]]Configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,25 @@ public class GitUsernamePasswordBinding extends MultiBinding<StandardUsernamePas
final static private String GIT_USERNAME_KEY = "GIT_USERNAME";
final static private String GIT_PASSWORD_KEY = "GIT_PASSWORD";
final private String gitToolName;
final private String hostName;
private transient boolean unixNodeType;

@DataBoundConstructor
public GitUsernamePasswordBinding(String gitToolName, String credentialsId) {
public GitUsernamePasswordBinding(String gitToolName, String credentialsId, String hostName) {
super(credentialsId);
this.gitToolName = gitToolName;
this.hostName = hostName;
//Variables could be added if needed
}

public String getGitToolName(){
return this.gitToolName;
}

public String getHostName(){
return this.hostName;
}

private void setUnixNodeType(boolean value) {
this.unixNodeType = value;
}
Expand Down Expand Up @@ -80,6 +86,15 @@ public MultiEnvironment bind(@NonNull Run<?, ?> run, FilePath filePath,
this.unixNodeType);
FilePath gitTempFile = gitScript.write(credentials, unbindTempDir.getDirPath());
secretValues.put("GIT_ASKPASS", gitTempFile.getRemote());
if (this.hostName != null) {
GenerateGitStore gitStore = new GenerateGitStore(
credentials.getUsername(),
credentials.getPassword().getPlainText(),
this.hostName,
credentials.getId());
FilePath gitStoreTempFile = gitStore.write(credentials, unbindTempDir.getDirPath());
secretValues.put("GIT_CREDENTIAL_STORE", gitStoreTempFile.getRemote());
}
return new MultiEnvironment(secretValues, publicValues, unbindTempDir.getUnbinder());
} else {
taskListener.getLogger().println("JGit and JGitApache type Git tools are not supported by this binding");
Expand Down Expand Up @@ -172,6 +187,36 @@ protected Class<StandardUsernamePasswordCredentials> type() {
}
}

protected static final class GenerateGitStore extends AbstractOnDiskBinding<StandardUsernamePasswordCredentials> {

private final String userVariable;
private final String passVariable;

Check warning

Code scanning / Jenkins Security Scan

Jenkins: Plaintext password storage

Variable should be reviewed whether it stored a password and is serialized to disk: passVariable
private final String hostVariable;

protected GenerateGitStore(String gitUsername, String gitPassword,
String hostname, String credentialId) {
super(gitUsername + ":" + gitPassword, credentialId);
this.userVariable = gitUsername;
this.passVariable = gitPassword;
this.hostVariable = hostname;
}

@Override
protected FilePath write(StandardUsernamePasswordCredentials credentials, FilePath workspace)
throws IOException, InterruptedException {
FilePath gitStore;
gitStore = workspace.createTempFile("git_store", "");
gitStore.write("https://" + this.userVariable + ":" + this.passVariable + "@" + this.hostVariable + "\n", null);
gitStore.chmod(0500);
return gitStore;
}

@Override
protected Class<StandardUsernamePasswordCredentials> type() {
return StandardUsernamePasswordCredentials.class;
}
}

// Mistakenly defined GitUsernamePassword in first release, prefer gitUsernamePassword as symbol
@Symbol({"gitUsernamePassword", "GitUsernamePassword"})
@Extension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@
<f:entry title="${%Git Tool Name}" field="gitToolName">
<f:select />
</f:entry>
</j:jelly>
<f:entry title="${%Hostname}" field="hostName">
<f:textbox/>
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public static Collection<Object[]> data() {

private final String password;

private final String hostname;

private final GitTool gitToolInstance;

private final String credentialID = DigestUtils.sha256Hex(("Git Usernanme and Password Binding").getBytes(StandardCharsets.UTF_8));
Expand Down Expand Up @@ -108,6 +110,7 @@ public static Collection<Object[]> data() {
public GitUsernamePasswordBindingTest(String username, String password, GitTool gitToolInstance) {
this.username = username;
this.password = password;
this.hostname = "hostname";
this.gitToolInstance = gitToolInstance;
}

Expand All @@ -122,7 +125,7 @@ public void basicSetup() throws IOException {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), credentials);

//GitUsernamePasswordBinding instance
gitCredBind = new GitUsernamePasswordBinding(gitToolInstance.getName(), credentials.getId());
gitCredBind = new GitUsernamePasswordBinding(gitToolInstance.getName(), credentials.getId(), hostname);
assertThat(gitCredBind.type(), is(StandardUsernamePasswordCredentials.class));

//Setting Git Tool
Expand All @@ -144,7 +147,7 @@ private String shellCheck() {
public void test_EnvironmentVariables_FreeStyleProject() throws Exception {
FreeStyleProject prj = r.createFreeStyleProject();
prj.getBuildWrappersList().add(new SecretBuildWrapper(Collections.<MultiBinding<?>>
singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID))));
singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID, hostname))));
prj.getBuildersList().add(isWindows() ? new BatchFile(batchCheck(isCliGitTool())) : new Shell(shellCheck()));
r.configRoundtrip((Item) prj);

Expand All @@ -158,6 +161,7 @@ public void test_EnvironmentVariables_FreeStyleProject() throws Exception {
}else {
assertThat(((GitUsernamePasswordBinding) binding).getGitToolName(), equalTo(""));
}
assertThat(((GitUsernamePasswordBinding) binding).getHostName(), equalTo(hostname));

FreeStyleBuild b = r.buildAndAssertSuccess(prj);
if(credentials.isUsernameSecret()) {
Expand Down Expand Up @@ -237,7 +241,7 @@ public void test_isCurrentNodeOSUnix(){
public void test_getCliGitTool_using_FreeStyleProject() throws Exception {
FreeStyleProject prj = r.createFreeStyleProject();
prj.getBuildWrappersList().add(new SecretBuildWrapper(Collections.<MultiBinding<?>>
singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID))));
singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID, hostname))));
prj.getBuildersList().add(isWindows() ? new BatchFile(batchCheck(false)) : new Shell(shellCheck()));
r.configRoundtrip((Item) prj);
SecretBuildWrapper wrapper = prj.getBuildWrappersList().get(SecretBuildWrapper.class);
Expand Down Expand Up @@ -283,6 +287,19 @@ public void test_GenerateGitScript_write() throws IOException, InterruptedExcept
assertThat(tempScriptFile.readToString(), containsString(this.password));
}

@Test
public void test_GenerateGitStore_write() throws IOException, InterruptedException {
GitUsernamePasswordBinding.GenerateGitStore tempGenGitStore = new GitUsernamePasswordBinding.GenerateGitStore(this.username, this.password, this.hostname, credentials.getId());
assertThat(tempGenGitStore.type(), is(StandardUsernamePasswordCredentials.class));
FilePath tempStoreFile = tempGenGitStore.write(credentials, rootFilePath);
if (!isWindows()) {
assertThat(tempStoreFile.mode(), is(0500));
}
assertThat(tempStoreFile.readToString(), containsString(this.username));
assertThat(tempStoreFile.readToString(), containsString(this.password));
assertThat(tempStoreFile.readToString(), containsString(this.hostname));
}

/**
* inline ${@link hudson.Functions#isWindows()} to prevent a transient
* remote classloader issue
Expand Down