Skip to content
7 changes: 7 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ This setting can be helpful with link:https://plugins.jenkins.io/swarm/[Jenkins
+
Default is `false` so that `setsid` is not used.

maskUrlCredentials::
When `org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials` is set to `true`, build log messages that contain URLs with credentials will have the first occurrence of credentials masked. URLs that appear in error messages are not masked to aid in troubleshooting.
+
For example, a URL of `https://foo:[email protected]/git/my-repo.git` will be masked as `https://[email protected]/git/my-repo.git`.
+
Default is `false` so that credentials in URLs are not masked.

[#changelog]
== Changelog in https://github.com/jenkinsci/git-client-plugin/releases[GitHub Releases]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,30 @@ private void getGitVersion() {
return gitVersion >= requestedVersion;
}

private static final Pattern MASK_URL_CREDENTIALS_PATTERN = Pattern.compile("://[/]?[^/@]*@");
private static final String MASK_URL_CREDENTIALS_REPLACE = "://xxxxx@";

/**
* Mask the first occurrence of credentials in the supplied URL.
*
* @param url the URL to mask
* @return the masked URL, or null if null was supplied
*/
public static String maskUrlCredentials(String url) {
return url != null
? MASK_URL_CREDENTIALS_PATTERN.matcher(url).replaceFirst(MASK_URL_CREDENTIALS_REPLACE)
: null;
}

/**
* Return the value of the org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials system property.
*
* @return true if maskUrlCredentials is enabled, false otherwise
*/
public static boolean getMaskUrlCredentials() {
return Boolean.parseBoolean(System.getProperty(CliGitAPIImpl.class.getName() + ".maskUrlCredentials", "false"));
}

/**
* Compare the current cli git version with the required version.
* Finds if the current cli git version is at-least the required version.
Expand Down Expand Up @@ -343,6 +367,9 @@ protected CliGitAPIImpl(String gitExe, File workspace, TaskListener listener, En
}

launcher = new LocalLauncher(IGitAPI.verbose ? listener : TaskListener.NULL);
if (this.getMaskUrlCredentials()) {
this.listener.getLogger().println("Masking credentials in URLs");
}
}

/** {@inheritDoc} */
Expand Down Expand Up @@ -568,7 +595,9 @@ public FetchCommand depth(Integer depth) {

@Override
public void execute() throws GitException, InterruptedException {
listener.getLogger().println("Fetching upstream changes from " + url);
// Mask credentials in URLs before writing to the build log, if requested
final String displayUrl = getMaskUrlCredentials() ? maskUrlCredentials(url.toString()) : url.toString();
listener.getLogger().println("Fetching upstream changes from " + displayUrl);

ArgumentListBuilder args = new ArgumentListBuilder();
args.add("fetch");
Expand Down Expand Up @@ -813,7 +842,10 @@ public void execute() throws GitException, InterruptedException {
throw new IllegalArgumentException("Invalid repository " + url, e);
}

listener.getLogger().println("Cloning repository " + url);
// Mask credentials in URLs before writing to the build log, if requested
final String displayUrl =
getMaskUrlCredentials() ? maskUrlCredentials(urIish.toString()) : urIish.toString();
listener.getLogger().println("Cloning repository " + displayUrl);

try {
Util.deleteContentsRecursive(workspace);
Expand Down Expand Up @@ -2808,7 +2840,10 @@ private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars e
args.prepend("setsid");
}
int usedTimeout = timeout == null ? TIMEOUT : timeout;
listener.getLogger().println(" > " + command + TIMEOUT_LOG_PREFIX + usedTimeout);

// Mask credentials in URLs before writing to the build log, if requested
final String displayCommand = getMaskUrlCredentials() ? maskUrlCredentials(command) : command;
listener.getLogger().println(" > " + displayCommand + TIMEOUT_LOG_PREFIX + usedTimeout);

Launcher.ProcStarter p =
launcher.launch().cmds(args.toCommandArray()).envs(freshEnv);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,77 @@ public void test_clone_huge_timeout_logging() throws Exception {
assertTimeout(testGitClient, "fetch", expectedValue);
}

@Test
public void test_clone_maskUrlCredentials_enabled() throws Exception {
if (!gitImplName.equals("git")) {
return;
}
System.setProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials", "true");
try {
final CliGitAPIImpl newTestGitClient =
new CliGitAPIImpl("git", repo.getRoot(), listener, new hudson.EnvVars());
assertThat(newTestGitClient.getMaskUrlCredentials(), is(true));
newTestGitClient
.clone_()
.url("https://foo:bar@localhost/git/my-repo.git")
.repositoryName("origin")
.execute();
} catch (Exception e) {
System.out.println("(ignored) clone exception: " + e);
} finally {
System.clearProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials");
}
assertThat(handler.containsMessageSubstring("Masking credentials in URLs"), is(true));
assertThat(handler.containsMessageSubstring("https://xxxxx@localhost/git/my-repo.git"), is(true));
}

@Test
public void test_clone_maskUrlCredentials_disabled() throws Exception {
if (!gitImplName.equals("git")) {
return;
}
System.setProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials", "false");
try {
final CliGitAPIImpl newTestGitClient =
new CliGitAPIImpl("git", repo.getRoot(), listener, new hudson.EnvVars());
assertThat(newTestGitClient.getMaskUrlCredentials(), is(false));
newTestGitClient
.clone_()
.url("https://foo:bar@localhost/git/my-repo.git")
.repositoryName("origin")
.execute();
} catch (Exception e) {
System.out.println("(ignored) clone exception: " + e);
} finally {
System.clearProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials");
}
assertThat(handler.containsMessageSubstring("Masking credentials in URLs"), is(false));
assertThat(handler.containsMessageSubstring("https://xxxxx@localhost/git/my-repo.git"), is(false));
assertThat(handler.containsMessageSubstring("https://foo:bar@localhost/git/my-repo.git"), is(true));
}

@Test
public void test_clone_maskUrlCredentials_unset() throws Exception {
if (!gitImplName.equals("git")) {
return;
}
System.clearProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials");
final CliGitAPIImpl newTestGitClient = new CliGitAPIImpl("git", repo.getRoot(), listener, new hudson.EnvVars());
assertThat(newTestGitClient.getMaskUrlCredentials(), is(false));
try {
newTestGitClient
.clone_()
.url("https://foo:bar@localhost/git/my-repo.git")
.repositoryName("origin")
.execute();
} catch (Exception e) {
System.out.println("(ignored) clone exception: " + e);
}
assertThat(handler.containsMessageSubstring("Masking credentials in URLs"), is(false));
assertThat(handler.containsMessageSubstring("https://xxxxx@localhost/git/my-repo.git"), is(false));
assertThat(handler.containsMessageSubstring("https://foo:bar@localhost/git/my-repo.git"), is(true));
}

private void assertAlternatesFileExists() {
final String alternates =
".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,84 @@ public void test_fetch_noTags() throws Exception {
assertThat("Tags have been found : " + tags, tags.isEmpty(), is(true));
}

@Test
public void test_fetch_maskUrlCredentials_enabled() throws Exception {
if (!gitImplName.equals("git")) {
return;
}
System.setProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials", "true");
try {
final CliGitAPIImpl newTestGitClient =
new CliGitAPIImpl("git", repo.getRoot(), listener, new hudson.EnvVars());
assertThat(newTestGitClient.getMaskUrlCredentials(), is(true));
newTestGitClient.setRemoteUrl("origin", "https://foo:bar@localhost/git/my-repo.git");
newTestGitClient
.fetch_()
.from(
new URIish("origin"),
Collections.singletonList(new RefSpec("refs/heads/*:refs/remotes/origin/*")))
.execute();
} catch (Exception e) {
System.out.println("(ignored) fetch exception: " + e);
} finally {
System.clearProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials");
}
assertThat(handler.containsMessageSubstring("Masking credentials in URLs"), is(true));
assertThat(handler.containsMessageSubstring("https://xxxxx@localhost/git/my-repo.git"), is(true));
}

@Test
public void test_fetch_maskUrlCredentials_disabled() throws Exception {
if (!gitImplName.equals("git")) {
return;
}
System.setProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials", "false");
try {
final CliGitAPIImpl newTestGitClient =
new CliGitAPIImpl("git", repo.getRoot(), listener, new hudson.EnvVars());
assertThat(newTestGitClient.getMaskUrlCredentials(), is(false));
newTestGitClient.setRemoteUrl("origin", "https://foo:bar@localhost/git/my-repo.git");
newTestGitClient
.fetch_()
.from(
new URIish("origin"),
Collections.singletonList(new RefSpec("refs/heads/*:refs/remotes/origin/*")))
.execute();
} catch (Exception e) {
System.out.println("(ignored) fetch exception: " + e);
} finally {
System.clearProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials");
}
assertThat(handler.containsMessageSubstring("Masking credentials in URLs"), is(false));
assertThat(handler.containsMessageSubstring("https://xxxxx@localhost/git/my-repo.git"), is(false));
assertThat(handler.containsMessageSubstring("https://foo:bar@localhost/git/my-repo.git"), is(true));
}

@Test
public void test_fetch_maskUrlCredentials_unset() throws Exception {
if (!gitImplName.equals("git")) {
return;
}
System.clearProperty("org.jenkinsci.plugins.gitclient.CliGitAPIImpl.maskUrlCredentials");
try {
final CliGitAPIImpl newTestGitClient =
new CliGitAPIImpl("git", repo.getRoot(), listener, new hudson.EnvVars());
assertThat(newTestGitClient.getMaskUrlCredentials(), is(false));
newTestGitClient.setRemoteUrl("origin", "https://foo:bar@localhost/git/my-repo.git");
newTestGitClient
.fetch_()
.from(
new URIish("origin"),
Collections.singletonList(new RefSpec("refs/heads/*:refs/remotes/origin/*")))
.execute();
} catch (Exception e) {
System.out.println("(ignored) fetch exception: " + e);
}
assertThat(handler.containsMessageSubstring("Masking credentials in URLs"), is(false));
assertThat(handler.containsMessageSubstring("https://xxxxx@localhost/git/my-repo.git"), is(false));
assertThat(handler.containsMessageSubstring("https://foo:bar@localhost/git/my-repo.git"), is(true));
}

/* JENKINS-33258 detected many calls to git rev-parse. This checks
* those calls are not being made. The checkoutRandomBranch call
* creates a branch with a random name. The later assertion checks that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3343,4 +3343,95 @@ public void test_git_branch_with_line_breaks_and_long_strings() throws Exception
private boolean isWindows() {
return File.pathSeparatorChar == ';';
}

@Test
public void testMaskUrlCredentialsNoCredentials() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String url = "https://dev.baz/git/repo.git";
final String expected = url;
assertThat(cliGitClient.maskUrlCredentials(url), is(expected));
}

@Test
public void testMaskUrlCredentialsNoCredentialSeparator() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String url = "https://[email protected]/git/repo.git";
final String expected = "https://[email protected]/git/repo.git";
assertThat(cliGitClient.maskUrlCredentials(url), is(expected));
}

@Test
public void testMaskUrlCredentialsUsernameAndPassword() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String url = "https://foo:[email protected]/git/repo.git";
final String expected = "https://[email protected]/git/repo.git";
assertThat(cliGitClient.maskUrlCredentials(url), is(expected));
}

@Test
public void testMaskUrlCredentialsUsernameOnly() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String url = "https://foo:@dev.baz/git/repo.git";
final String expected = "https://[email protected]/git/repo.git";
assertThat(cliGitClient.maskUrlCredentials(url), is(expected));
}

@Test
public void testMaskUrlCredentialsPasswordOnly() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String url = "https://:[email protected]/git/repo.git";
final String expected = "https://[email protected]/git/repo.git";
assertThat(cliGitClient.maskUrlCredentials(url), is(expected));
}

@Test
public void testMaskUrlCredentialsNullUrl() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String url = null;
final String expected = null;
assertThat(cliGitClient.maskUrlCredentials(url), is(expected));
}

@Test
public void testMaskUrlCredentialsCommand() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String command =
"git fetch --no-tags --force --progress --depth=1 -- https://foo:[email protected]/git/repo.git +refs/heads/main:refs/remotes/origin/main";
final String expected =
"git fetch --no-tags --force --progress --depth=1 -- https://[email protected]/git/repo.git +refs/heads/main:refs/remotes/origin/main";
assertThat(cliGitClient.maskUrlCredentials(command), is(expected));
}

@Test
public void testMaskUrlCredentialsCommandTwoUrls() throws Exception {
if (!gitImplName.equals("git") || !(gitClient instanceof CliGitAPIImpl)) {
return;
}
CliGitAPIImpl cliGitClient = (CliGitAPIImpl) gitClient;
final String command = "git config url.ssh://[email protected]/foobar.insteadof https://baz:[email protected]/foobar";
final String expected =
"git config url.ssh://[email protected]/foobar.insteadof https://baz:[email protected]/foobar";
assertThat(cliGitClient.maskUrlCredentials(command), is(expected));
}
}
Loading