From 628c808be3a1934e4ff47f7884156cfdcd469779 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Mon, 22 Dec 2025 20:26:52 +0530 Subject: [PATCH 1/8] [JENKINS-69507] JGit fails 2nd and later https fetch with embedded username & password --- .../plugins/gitclient/JGitAPIImpl.java | 85 ++++++++++ .../plugins/gitclient/CredentialsTest.java | 4 +- .../JGitEmbeddedCredentialsTest.java | 157 ++++++++++++++++++ 3 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 94e63ed330..fcfc8abdac 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -15,8 +15,11 @@ import static org.jenkinsci.plugins.gitclient.CliGitAPIImpl.TIMEOUT_LOG_PREFIX; import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsDescriptor; +import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.common.UsernameCredentials; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -727,6 +730,68 @@ private boolean unsupportedProtocol(URIish url) { return url != null && unsupportedProtocol(url.toString()); } + /** + * Extracts embedded credentials from a URL and adds them to the credentials provider. + * This is necessary because JGit doesn't automatically use credentials embedded in URLs + * stored in git config (JENKINS-69507). + * + * @param url the URL which may contain embedded credentials + */ + private void extractAndAddEmbeddedCredentials(URIish url) { + if (url == null) { + return; + } + + String user = url.getUser(); + String pass = url.getPass(); + + // Only add credentials if both username and password are present in the URL + if (user != null && !user.isEmpty() && pass != null && !pass.isEmpty()) { + StandardUsernamePasswordCredentials embeddedCredentials = new StandardUsernamePasswordCredentials() { + @Serial + private static final long serialVersionUID = 1L; + + @Override + @NonNull + public String getDescription() { + return "Credentials extracted from repository URL"; + } + + @Override + @NonNull + public String getId() { + return "embedded-url-credentials-" + url.getHost(); + } + + @Override + public CredentialsScope getScope() { + return CredentialsScope.GLOBAL; + } + + @Override + @NonNull + public CredentialsDescriptor getDescriptor() { + throw new UnsupportedOperationException("Descriptor not available for embedded credentials"); + } + + @Override + @NonNull + public String getUsername() { + return user; + } + + @Override + @NonNull + public Secret getPassword() { + return Secret.fromString(pass); + } + }; + + // Add the credentials to the provider so they can be used for authentication + addCredentials(url.toString(), embeddedCredentials); + } + } + /** * fetch_. * @@ -816,6 +881,26 @@ public void execute() throws GitException { if (unsupportedProtocol(url)) { throw new GitException("unsupported protocol in URL " + url); } + + // JENKINS-69507: Handle embedded credentials in URLs + // If the URL looks like a remote name (not a full URL), resolve it from git config first + URIish urlForCredentials = url; + if (!url.isRemote() && !org.apache.commons.lang3.StringUtils.containsAny(url.toString(), ":@/\\")) { + try { + String resolvedUrl = getRemoteUrl(url.toString()); + if (resolvedUrl != null) { + urlForCredentials = new URIish(resolvedUrl); + } + } catch (URISyntaxException e) { + // If resolution fails, continue with original URL + } + } + + // Extract and add credentials from the resolved URL if embedded + // This handles the case where a URL with embedded credentials is stored in git config + // and used in subsequent fetches + extractAndAddEmbeddedCredentials(urlForCredentials); + fetch.setRemote(url.toString()); fetch.setCredentialsProvider(getProvider()); fetch.setTransportConfigCallback(getTransportConfigCallback()); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java index 07b65878ad..b511f9c82f 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java @@ -341,8 +341,8 @@ static List gitRepoUrls() throws Exception { password != null && !password.matches(".*[@:].*") && // Skip special cases of password - implementation.equals("git") - && // Embedded credentials only implemented for CLI git + (implementation.equals("git") || implementation.equals("jgit")) + && // Embedded credentials implemented for both CLI git and JGit (JENKINS-69507) repoURL.startsWith("http")) { /* Use existing username and password to create an embedded credentials test case */ String repoURLwithCredentials = repoURL.replaceAll( diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java new file mode 100644 index 0000000000..261f83aaaf --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java @@ -0,0 +1,157 @@ +package org.jenkinsci.plugins.gitclient; + +import static org.junit.jupiter.api.Assertions.*; + +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; +import java.io.File; +import org.eclipse.jgit.transport.URIish; +import org.jenkinsci.plugins.gitclient.jgit.SmartCredentialsProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Test that verifies JENKINS-69507 fix: JGit should extract and use + * credentials embedded in URLs (like https://user:pass@host/repo.git) + * for subsequent fetch operations. + */ +class JGitEmbeddedCredentialsTest { + + @TempDir + File tempDir; + + /** + * Test that extractAndAddEmbeddedCredentials properly extracts username and password + * from a URL and adds them to the credentials provider. + */ + @Test + void testExtractEmbeddedCredentials() throws Exception { + TaskListener listener = StreamTaskListener.fromStdout(); + JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + + // Create a URL with embedded credentials + URIish urlWithCredentials = new URIish("https://testuser:testpass@example.com/repo.git"); + + // Get the credentials provider before extraction + SmartCredentialsProvider provider = gitClient.getProvider(); + + // Verify credentials map is initially empty or doesn't have our URL + var credsBefore = provider.getCredentials(); + assertFalse(credsBefore.containsKey("https://testuser:testpass@example.com/repo.git") + || credsBefore.containsKey("https://example.com/repo.git"), + "Credentials should not exist before extraction"); + + // Trigger the credential extraction by calling addCredentials + // (the fix calls extractAndAddEmbeddedCredentials before fetch) + gitClient.addCredentials(urlWithCredentials.toString(), createTestCredentials("testuser", "testpass")); + + // Verify credentials were added + var credsAfter = provider.getCredentials(); + assertTrue(credsAfter.size() > credsBefore.size(), "Credentials should be added"); + } + + /** + * Test that URLs without credentials don't cause issues + */ + @Test + void testUrlWithoutCredentials() throws Exception { + TaskListener listener = StreamTaskListener.fromStdout(); + JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + + // Create a URL without embedded credentials + URIish urlWithoutCredentials = new URIish("https://example.com/repo.git"); + + // This should not throw an exception + assertDoesNotThrow(() -> { + gitClient.addCredentials(urlWithoutCredentials.toString(), createTestCredentials("user", "pass")); + }); + } + + /** + * Test that URLs with only username (no password) are handled correctly + */ + @Test + void testUrlWithOnlyUsername() throws Exception { + TaskListener listener = StreamTaskListener.fromStdout(); + JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + + // Create a URL with only username + URIish urlWithOnlyUser = new URIish("https://testuser@example.com/repo.git"); + + // This should not throw an exception (credentials won't be extracted as there's no password) + assertDoesNotThrow(() -> { + gitClient.addCredentials(urlWithOnlyUser.toString(), createTestCredentials("testuser", "pass")); + }); + } + + /** + * Test the scenario described in JENKINS-69507: credentials should be extracted + * when a remote name is resolved to a URL with embedded credentials. + * This simulates the case where: + * 1. First fetch works with URL containing credentials + * 2. URL is stored in git config as remote "origin" + * 3. Second fetch using remote name "origin" should extract credentials from the stored URL + */ + @Test + void testRemoteNameResolutionWithEmbeddedCredentials() throws Exception { + TaskListener listener = StreamTaskListener.fromStdout(); + JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + + // Initialize a git repository + gitClient.init_().workspace(tempDir.getAbsolutePath()).execute(); + + // Set a remote URL with embedded credentials (simulating what happens after first clone) + String urlWithCreds = "https://testuser:testpass@example.com/repo.git"; + gitClient.setRemoteUrl("origin", urlWithCreds); + + // Verify the remote URL was stored + String storedUrl = gitClient.getRemoteUrl("origin"); + assertNotNull(storedUrl, "Remote URL should be stored"); + assertEquals(urlWithCreds, storedUrl, "Stored URL should match"); + + // The fix should extract credentials from this URL when fetch is called with remote name + // Note: We can't actually test the fetch without a real repository, but we can verify + // that the URL resolution works correctly + URIish resolvedUrl = new URIish(storedUrl); + assertEquals("testuser", resolvedUrl.getUser(), "Username should be in URL"); + assertEquals("testpass", resolvedUrl.getPass(), "Password should be in URL"); + } + + /** + * Helper method to create test credentials + */ + private StandardUsernamePasswordCredentials createTestCredentials(String username, String password) { + return new StandardUsernamePasswordCredentials() { + @Override + public String getDescription() { + return "Test credentials"; + } + + @Override + public String getId() { + return "test-id"; + } + + @Override + public com.cloudbees.plugins.credentials.CredentialsScope getScope() { + return com.cloudbees.plugins.credentials.CredentialsScope.GLOBAL; + } + + @Override + public com.cloudbees.plugins.credentials.CredentialsDescriptor getDescriptor() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUsername() { + return username; + } + + @Override + public hudson.util.Secret getPassword() { + return hudson.util.Secret.fromString(password); + } + }; + } +} From dd5a0ebc6f3bd2dad025c50300b02125a5668a37 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Thu, 1 Jan 2026 23:59:33 +0530 Subject: [PATCH 2/8] apply mvn spotless:apply --- .../plugins/gitclient/JGitAPIImpl.java | 24 ++++----- .../JGitEmbeddedCredentialsTest.java | 51 ++++++++++--------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index fcfc8abdac..00ada912a9 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -741,52 +741,52 @@ private void extractAndAddEmbeddedCredentials(URIish url) { if (url == null) { return; } - + String user = url.getUser(); String pass = url.getPass(); - + // Only add credentials if both username and password are present in the URL if (user != null && !user.isEmpty() && pass != null && !pass.isEmpty()) { StandardUsernamePasswordCredentials embeddedCredentials = new StandardUsernamePasswordCredentials() { @Serial private static final long serialVersionUID = 1L; - + @Override @NonNull public String getDescription() { return "Credentials extracted from repository URL"; } - + @Override @NonNull public String getId() { return "embedded-url-credentials-" + url.getHost(); } - + @Override public CredentialsScope getScope() { return CredentialsScope.GLOBAL; } - + @Override @NonNull public CredentialsDescriptor getDescriptor() { throw new UnsupportedOperationException("Descriptor not available for embedded credentials"); } - + @Override @NonNull public String getUsername() { return user; } - + @Override @NonNull public Secret getPassword() { return Secret.fromString(pass); } }; - + // Add the credentials to the provider so they can be used for authentication addCredentials(url.toString(), embeddedCredentials); } @@ -881,7 +881,7 @@ public void execute() throws GitException { if (unsupportedProtocol(url)) { throw new GitException("unsupported protocol in URL " + url); } - + // JENKINS-69507: Handle embedded credentials in URLs // If the URL looks like a remote name (not a full URL), resolve it from git config first URIish urlForCredentials = url; @@ -895,12 +895,12 @@ public void execute() throws GitException { // If resolution fails, continue with original URL } } - + // Extract and add credentials from the resolved URL if embedded // This handles the case where a URL with embedded credentials is stored in git config // and used in subsequent fetches extractAndAddEmbeddedCredentials(urlForCredentials); - + fetch.setRemote(url.toString()); fetch.setCredentialsProvider(getProvider()); fetch.setTransportConfigCallback(getTransportConfigCallback()); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java index 261f83aaaf..79491535fc 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java @@ -29,28 +29,29 @@ class JGitEmbeddedCredentialsTest { void testExtractEmbeddedCredentials() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - + // Create a URL with embedded credentials URIish urlWithCredentials = new URIish("https://testuser:testpass@example.com/repo.git"); - + // Get the credentials provider before extraction SmartCredentialsProvider provider = gitClient.getProvider(); - + // Verify credentials map is initially empty or doesn't have our URL var credsBefore = provider.getCredentials(); - assertFalse(credsBefore.containsKey("https://testuser:testpass@example.com/repo.git") - || credsBefore.containsKey("https://example.com/repo.git"), - "Credentials should not exist before extraction"); - + assertFalse( + credsBefore.containsKey("https://testuser:testpass@example.com/repo.git") + || credsBefore.containsKey("https://example.com/repo.git"), + "Credentials should not exist before extraction"); + // Trigger the credential extraction by calling addCredentials // (the fix calls extractAndAddEmbeddedCredentials before fetch) gitClient.addCredentials(urlWithCredentials.toString(), createTestCredentials("testuser", "testpass")); - + // Verify credentials were added var credsAfter = provider.getCredentials(); assertTrue(credsAfter.size() > credsBefore.size(), "Credentials should be added"); } - + /** * Test that URLs without credentials don't cause issues */ @@ -58,16 +59,16 @@ void testExtractEmbeddedCredentials() throws Exception { void testUrlWithoutCredentials() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - + // Create a URL without embedded credentials URIish urlWithoutCredentials = new URIish("https://example.com/repo.git"); - + // This should not throw an exception assertDoesNotThrow(() -> { gitClient.addCredentials(urlWithoutCredentials.toString(), createTestCredentials("user", "pass")); }); } - + /** * Test that URLs with only username (no password) are handled correctly */ @@ -75,16 +76,16 @@ void testUrlWithoutCredentials() throws Exception { void testUrlWithOnlyUsername() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - + // Create a URL with only username URIish urlWithOnlyUser = new URIish("https://testuser@example.com/repo.git"); - + // This should not throw an exception (credentials won't be extracted as there's no password) assertDoesNotThrow(() -> { gitClient.addCredentials(urlWithOnlyUser.toString(), createTestCredentials("testuser", "pass")); }); } - + /** * Test the scenario described in JENKINS-69507: credentials should be extracted * when a remote name is resolved to a URL with embedded credentials. @@ -97,19 +98,19 @@ void testUrlWithOnlyUsername() throws Exception { void testRemoteNameResolutionWithEmbeddedCredentials() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - + // Initialize a git repository gitClient.init_().workspace(tempDir.getAbsolutePath()).execute(); - + // Set a remote URL with embedded credentials (simulating what happens after first clone) String urlWithCreds = "https://testuser:testpass@example.com/repo.git"; gitClient.setRemoteUrl("origin", urlWithCreds); - + // Verify the remote URL was stored String storedUrl = gitClient.getRemoteUrl("origin"); assertNotNull(storedUrl, "Remote URL should be stored"); assertEquals(urlWithCreds, storedUrl, "Stored URL should match"); - + // The fix should extract credentials from this URL when fetch is called with remote name // Note: We can't actually test the fetch without a real repository, but we can verify // that the URL resolution works correctly @@ -117,7 +118,7 @@ void testRemoteNameResolutionWithEmbeddedCredentials() throws Exception { assertEquals("testuser", resolvedUrl.getUser(), "Username should be in URL"); assertEquals("testpass", resolvedUrl.getPass(), "Password should be in URL"); } - + /** * Helper method to create test credentials */ @@ -127,27 +128,27 @@ private StandardUsernamePasswordCredentials createTestCredentials(String usernam public String getDescription() { return "Test credentials"; } - + @Override public String getId() { return "test-id"; } - + @Override public com.cloudbees.plugins.credentials.CredentialsScope getScope() { return com.cloudbees.plugins.credentials.CredentialsScope.GLOBAL; } - + @Override public com.cloudbees.plugins.credentials.CredentialsDescriptor getDescriptor() { throw new UnsupportedOperationException(); } - + @Override public String getUsername() { return username; } - + @Override public hudson.util.Secret getPassword() { return hudson.util.Secret.fromString(password); From 640df515df48c64bec0f6964993649c9ac9fef4c Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Fri, 16 Jan 2026 01:08:00 +0530 Subject: [PATCH 3/8] Refactor credential handling: Introduce EmbeddedCredentials class to extract and use credentials from URLs, and update tests to avoid SpotBugs warnings. --- .../plugins/gitclient/JGitAPIImpl.java | 95 +++++++++++-------- .../JGitEmbeddedCredentialsTest.java | 75 +++++++++------ 2 files changed, 100 insertions(+), 70 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 00ada912a9..11a4c6d512 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -730,6 +730,60 @@ private boolean unsupportedProtocol(URIish url) { return url != null && unsupportedProtocol(url.toString()); } + /** + * Static inner class to hold embedded credentials extracted from URLs. + * This avoids SpotBugs warnings about serializable inner classes. + */ + private static class EmbeddedCredentials implements StandardUsernamePasswordCredentials { + @Serial + private static final long serialVersionUID = 1L; + + private final String username; + private final String password; + private final String host; + + EmbeddedCredentials(String username, String password, String host) { + this.username = username; + this.password = password; + this.host = host; + } + + @Override + @NonNull + public String getDescription() { + return "Credentials extracted from repository URL"; + } + + @Override + @NonNull + public String getId() { + return "embedded-url-credentials-" + host; + } + + @Override + public CredentialsScope getScope() { + return CredentialsScope.GLOBAL; + } + + @Override + @NonNull + public CredentialsDescriptor getDescriptor() { + throw new UnsupportedOperationException("Descriptor not available for embedded credentials"); + } + + @Override + @NonNull + public String getUsername() { + return username; + } + + @Override + @NonNull + public Secret getPassword() { + return Secret.fromString(password); + } + } + /** * Extracts embedded credentials from a URL and adds them to the credentials provider. * This is necessary because JGit doesn't automatically use credentials embedded in URLs @@ -747,45 +801,8 @@ private void extractAndAddEmbeddedCredentials(URIish url) { // Only add credentials if both username and password are present in the URL if (user != null && !user.isEmpty() && pass != null && !pass.isEmpty()) { - StandardUsernamePasswordCredentials embeddedCredentials = new StandardUsernamePasswordCredentials() { - @Serial - private static final long serialVersionUID = 1L; - - @Override - @NonNull - public String getDescription() { - return "Credentials extracted from repository URL"; - } - - @Override - @NonNull - public String getId() { - return "embedded-url-credentials-" + url.getHost(); - } - - @Override - public CredentialsScope getScope() { - return CredentialsScope.GLOBAL; - } - - @Override - @NonNull - public CredentialsDescriptor getDescriptor() { - throw new UnsupportedOperationException("Descriptor not available for embedded credentials"); - } - - @Override - @NonNull - public String getUsername() { - return user; - } - - @Override - @NonNull - public Secret getPassword() { - return Secret.fromString(pass); - } - }; + StandardUsernamePasswordCredentials embeddedCredentials = + new EmbeddedCredentials(user, pass, url.getHost()); // Add the credentials to the provider so they can be used for authentication addCredentials(url.toString(), embeddedCredentials); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java index 79491535fc..997185c7f5 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java @@ -119,40 +119,53 @@ void testRemoteNameResolutionWithEmbeddedCredentials() throws Exception { assertEquals("testpass", resolvedUrl.getPass(), "Password should be in URL"); } + /** + * Static inner class for test credentials to avoid SpotBugs warnings. + */ + private static class TestCredentials implements StandardUsernamePasswordCredentials { + private final String username; + private final String password; + + TestCredentials(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public String getDescription() { + return "Test credentials"; + } + + @Override + public String getId() { + return "test-id"; + } + + @Override + public com.cloudbees.plugins.credentials.CredentialsScope getScope() { + return com.cloudbees.plugins.credentials.CredentialsScope.GLOBAL; + } + + @Override + public com.cloudbees.plugins.credentials.CredentialsDescriptor getDescriptor() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUsername() { + return username; + } + + @Override + public hudson.util.Secret getPassword() { + return hudson.util.Secret.fromString(password); + } + } + /** * Helper method to create test credentials */ private StandardUsernamePasswordCredentials createTestCredentials(String username, String password) { - return new StandardUsernamePasswordCredentials() { - @Override - public String getDescription() { - return "Test credentials"; - } - - @Override - public String getId() { - return "test-id"; - } - - @Override - public com.cloudbees.plugins.credentials.CredentialsScope getScope() { - return com.cloudbees.plugins.credentials.CredentialsScope.GLOBAL; - } - - @Override - public com.cloudbees.plugins.credentials.CredentialsDescriptor getDescriptor() { - throw new UnsupportedOperationException(); - } - - @Override - public String getUsername() { - return username; - } - - @Override - public hudson.util.Secret getPassword() { - return hudson.util.Secret.fromString(password); - } - }; + return new TestCredentials(username, password); } } From 2595ee1ca4bb6f80106b638ba0d2b9fe375b058e Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Fri, 16 Jan 2026 02:03:49 +0530 Subject: [PATCH 4/8] Refactor password handling in EmbeddedCredentials to use Secret class for enhanced security --- .../java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 11a4c6d512..7d86a2b1a0 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -739,12 +739,12 @@ private static class EmbeddedCredentials implements StandardUsernamePasswordCred private static final long serialVersionUID = 1L; private final String username; - private final String password; + private final Secret password; private final String host; EmbeddedCredentials(String username, String password, String host) { this.username = username; - this.password = password; + this.password = Secret.fromString(password); this.host = host; } @@ -780,7 +780,7 @@ public String getUsername() { @Override @NonNull public Secret getPassword() { - return Secret.fromString(password); + return password; } } From 09ad24348661c2065a3ad1e74a6630262ae73832 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Fri, 16 Jan 2026 02:19:53 +0530 Subject: [PATCH 5/8] remove useless comments --- .../plugins/gitclient/JGitAPIImpl.java | 14 +++++++------- .../gitclient/JGitEmbeddedCredentialsTest.java | 18 ++---------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 7d86a2b1a0..2e2b67b5d2 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -799,12 +799,10 @@ private void extractAndAddEmbeddedCredentials(URIish url) { String user = url.getUser(); String pass = url.getPass(); - // Only add credentials if both username and password are present in the URL if (user != null && !user.isEmpty() && pass != null && !pass.isEmpty()) { StandardUsernamePasswordCredentials embeddedCredentials = new EmbeddedCredentials(user, pass, url.getHost()); - // Add the credentials to the provider so they can be used for authentication addCredentials(url.toString(), embeddedCredentials); } } @@ -899,8 +897,9 @@ public void execute() throws GitException { throw new GitException("unsupported protocol in URL " + url); } - // JENKINS-69507: Handle embedded credentials in URLs - // If the URL looks like a remote name (not a full URL), resolve it from git config first + /* JENKINS-69507: Handle embedded credentials in URLs + * If the URL looks like a remote name (not a full URL), resolve it from git config first + */ URIish urlForCredentials = url; if (!url.isRemote() && !org.apache.commons.lang3.StringUtils.containsAny(url.toString(), ":@/\\")) { try { @@ -913,9 +912,10 @@ public void execute() throws GitException { } } - // Extract and add credentials from the resolved URL if embedded - // This handles the case where a URL with embedded credentials is stored in git config - // and used in subsequent fetches + /* Extract and add credentials from the resolved URL if embedded + * This handles the case where a URL with embedded credentials is stored in git config + * and used in subsequent fetches + */ extractAndAddEmbeddedCredentials(urlForCredentials); fetch.setRemote(url.toString()); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java index 997185c7f5..a80f0a7729 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java @@ -15,6 +15,8 @@ * Test that verifies JENKINS-69507 fix: JGit should extract and use * credentials embedded in URLs (like https://user:pass@host/repo.git) * for subsequent fetch operations. + * + * @author Akash Manna */ class JGitEmbeddedCredentialsTest { @@ -30,24 +32,18 @@ void testExtractEmbeddedCredentials() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - // Create a URL with embedded credentials URIish urlWithCredentials = new URIish("https://testuser:testpass@example.com/repo.git"); - // Get the credentials provider before extraction SmartCredentialsProvider provider = gitClient.getProvider(); - // Verify credentials map is initially empty or doesn't have our URL var credsBefore = provider.getCredentials(); assertFalse( credsBefore.containsKey("https://testuser:testpass@example.com/repo.git") || credsBefore.containsKey("https://example.com/repo.git"), "Credentials should not exist before extraction"); - // Trigger the credential extraction by calling addCredentials - // (the fix calls extractAndAddEmbeddedCredentials before fetch) gitClient.addCredentials(urlWithCredentials.toString(), createTestCredentials("testuser", "testpass")); - // Verify credentials were added var credsAfter = provider.getCredentials(); assertTrue(credsAfter.size() > credsBefore.size(), "Credentials should be added"); } @@ -60,10 +56,8 @@ void testUrlWithoutCredentials() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - // Create a URL without embedded credentials URIish urlWithoutCredentials = new URIish("https://example.com/repo.git"); - // This should not throw an exception assertDoesNotThrow(() -> { gitClient.addCredentials(urlWithoutCredentials.toString(), createTestCredentials("user", "pass")); }); @@ -77,10 +71,8 @@ void testUrlWithOnlyUsername() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - // Create a URL with only username URIish urlWithOnlyUser = new URIish("https://testuser@example.com/repo.git"); - // This should not throw an exception (credentials won't be extracted as there's no password) assertDoesNotThrow(() -> { gitClient.addCredentials(urlWithOnlyUser.toString(), createTestCredentials("testuser", "pass")); }); @@ -99,21 +91,15 @@ void testRemoteNameResolutionWithEmbeddedCredentials() throws Exception { TaskListener listener = StreamTaskListener.fromStdout(); JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); - // Initialize a git repository gitClient.init_().workspace(tempDir.getAbsolutePath()).execute(); - // Set a remote URL with embedded credentials (simulating what happens after first clone) String urlWithCreds = "https://testuser:testpass@example.com/repo.git"; gitClient.setRemoteUrl("origin", urlWithCreds); - // Verify the remote URL was stored String storedUrl = gitClient.getRemoteUrl("origin"); assertNotNull(storedUrl, "Remote URL should be stored"); assertEquals(urlWithCreds, storedUrl, "Stored URL should match"); - // The fix should extract credentials from this URL when fetch is called with remote name - // Note: We can't actually test the fetch without a real repository, but we can verify - // that the URL resolution works correctly URIish resolvedUrl = new URIish(storedUrl); assertEquals("testuser", resolvedUrl.getUser(), "Username should be in URL"); assertEquals("testpass", resolvedUrl.getPass(), "Password should be in URL"); From 30266b39de2f63326796975d3cfe94629f32e356 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Fri, 16 Jan 2026 02:21:23 +0530 Subject: [PATCH 6/8] apply spotless command --- .../plugins/gitclient/JGitEmbeddedCredentialsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java index a80f0a7729..a07b3f3e5a 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java @@ -15,7 +15,7 @@ * Test that verifies JENKINS-69507 fix: JGit should extract and use * credentials embedded in URLs (like https://user:pass@host/repo.git) * for subsequent fetch operations. - * + * * @author Akash Manna */ class JGitEmbeddedCredentialsTest { From 6a985b800dff202da611a608f8f3d5767aaac058 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Sun, 1 Feb 2026 12:05:08 +0530 Subject: [PATCH 7/8] enhance credential handling in JGitAPIImpl --- .../jenkinsci/plugins/gitclient/JGitAPIImpl.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 2e2b67b5d2..2ca58ed075 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -800,10 +800,21 @@ private void extractAndAddEmbeddedCredentials(URIish url) { String pass = url.getPass(); if (user != null && !user.isEmpty() && pass != null && !pass.isEmpty()) { + String host = url.getHost(); + if (host == null || host.isEmpty()) { + host = "unknown-host"; + } + StandardUsernamePasswordCredentials embeddedCredentials = - new EmbeddedCredentials(user, pass, url.getHost()); + new EmbeddedCredentials(user, pass, host); + // Add credentials keyed by the full URL (including embedded credentials) addCredentials(url.toString(), embeddedCredentials); + + // Also add credentials keyed by the URL without embedded credentials, + // since JGit's TransportHttp may query using a stripped URL. + String urlWithoutCredentials = url.toASCIIString().replaceFirst("://[^@]+@", "://"); + addCredentials(urlWithoutCredentials, embeddedCredentials); } } @@ -901,7 +912,7 @@ public void execute() throws GitException { * If the URL looks like a remote name (not a full URL), resolve it from git config first */ URIish urlForCredentials = url; - if (!url.isRemote() && !org.apache.commons.lang3.StringUtils.containsAny(url.toString(), ":@/\\")) { + if (!url.isRemote()) { try { String resolvedUrl = getRemoteUrl(url.toString()); if (resolvedUrl != null) { From f2267368cf9bd61fadb5f89c975a841a746c8784 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Sat, 15 Aug 2026 01:16:57 +0530 Subject: [PATCH 8/8] Enhance embedded credentials handling in JGitAPIImpl and add comprehensive tests - Introduced EmbeddedCredentialsDescriptor for safe descriptor access on agents. - Updated embedded credentials extraction logic to handle various URL formats. - Improved test coverage for embedded credentials, including edge cases and protocol handling. - Ensured proper registration and authentication of credentials in different scenarios. --- .../plugins/gitclient/JGitAPIImpl.java | 111 +++++-- .../plugins/gitclient/CredentialsTest.java | 2 +- .../JGitEmbeddedCredentialsTest.java | 313 ++++++++++++------ 3 files changed, 297 insertions(+), 129 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 2ca58ed075..7e9d6497ce 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -56,6 +56,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -730,6 +731,23 @@ private boolean unsupportedProtocol(URIish url) { return url != null && unsupportedProtocol(url.toString()); } + /** + * Self contained descriptor so that {@code getDescriptor()} is safe to call on agents, where no + * Jenkins instance is available. + */ + private static class EmbeddedCredentialsDescriptor extends CredentialsDescriptor { + + EmbeddedCredentialsDescriptor() { + super(EmbeddedCredentials.class); + } + + @Override + @NonNull + public String getDisplayName() { + return "Embedded URL credentials"; + } + } + /** * Static inner class to hold embedded credentials extracted from URLs. * This avoids SpotBugs warnings about serializable inner classes. @@ -738,6 +756,8 @@ private static class EmbeddedCredentials implements StandardUsernamePasswordCred @Serial private static final long serialVersionUID = 1L; + private static final CredentialsDescriptor DESCRIPTOR = new EmbeddedCredentialsDescriptor(); + private final String username; private final Secret password; private final String host; @@ -757,7 +777,7 @@ public String getDescription() { @Override @NonNull public String getId() { - return "embedded-url-credentials-" + host; + return "embedded-url-credentials-" + username + "@" + host; } @Override @@ -768,7 +788,7 @@ public CredentialsScope getScope() { @Override @NonNull public CredentialsDescriptor getDescriptor() { - throw new UnsupportedOperationException("Descriptor not available for embedded credentials"); + return DESCRIPTOR; } @Override @@ -782,12 +802,42 @@ public String getUsername() { public Secret getPassword() { return password; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof EmbeddedCredentials other)) { + return false; + } + return Objects.equals(username, other.username) + && Objects.equals(password, other.password) + && Objects.equals(host, other.host); + } + + @Override + public int hashCode() { + return Objects.hash(username, password, host); + } } /** - * Extracts embedded credentials from a URL and adds them to the credentials provider. - * This is necessary because JGit doesn't automatically use credentials embedded in URLs - * stored in git config (JENKINS-69507). + * Returns true if the value is a bare remote name such as {@code origin} rather than a + * repository location. + */ + private static boolean isRemoteName(URIish url) { + if (url.getScheme() != null || url.getHost() != null) { + return false; + } + String value = url.toString(); + return !value.isEmpty() && !value.contains("/") && !value.contains("\\"); + } + + /** + * Adds credentials embedded in an http or https URL to the credentials provider, since JGit does + * not use credentials embedded in a URL resolved from git config (JENKINS-69507). Other + * protocols are ignored. * * @param url the URL which may contain embedded credentials */ @@ -796,26 +846,26 @@ private void extractAndAddEmbeddedCredentials(URIish url) { return; } + String scheme = url.getScheme(); + if (scheme == null || !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) { + return; + } + String user = url.getUser(); String pass = url.getPass(); + if (user == null || user.isEmpty() || pass == null || pass.isEmpty()) { + return; + } - if (user != null && !user.isEmpty() && pass != null && !pass.isEmpty()) { - String host = url.getHost(); - if (host == null || host.isEmpty()) { - host = "unknown-host"; - } - - StandardUsernamePasswordCredentials embeddedCredentials = - new EmbeddedCredentials(user, pass, host); + String host = url.getHost(); + if (host == null || host.isEmpty()) { + host = "unknown-host"; + } - // Add credentials keyed by the full URL (including embedded credentials) - addCredentials(url.toString(), embeddedCredentials); + StandardUsernamePasswordCredentials embeddedCredentials = new EmbeddedCredentials(user, pass, host); - // Also add credentials keyed by the URL without embedded credentials, - // since JGit's TransportHttp may query using a stripped URL. - String urlWithoutCredentials = url.toASCIIString().replaceFirst("://[^@]+@", "://"); - addCredentials(urlWithoutCredentials, embeddedCredentials); - } + addCredentials(url.toString(), embeddedCredentials); + addCredentials(url.setUser(null).setPass(null).toString(), embeddedCredentials); } /** @@ -908,25 +958,18 @@ public void execute() throws GitException { throw new GitException("unsupported protocol in URL " + url); } - /* JENKINS-69507: Handle embedded credentials in URLs - * If the URL looks like a remote name (not a full URL), resolve it from git config first - */ + /* JENKINS-69507 */ URIish urlForCredentials = url; - if (!url.isRemote()) { - try { - String resolvedUrl = getRemoteUrl(url.toString()); - if (resolvedUrl != null) { + if (isRemoteName(url)) { + String resolvedUrl = repo.getConfig().getString("remote", url.toString(), "url"); + if (resolvedUrl != null) { + try { urlForCredentials = new URIish(resolvedUrl); + } catch (URISyntaxException e) { + LOGGER.log(Level.FINE, e, () -> "Could not parse the URL configured for remote " + url); } - } catch (URISyntaxException e) { - // If resolution fails, continue with original URL } } - - /* Extract and add credentials from the resolved URL if embedded - * This handles the case where a URL with embedded credentials is stored in git config - * and used in subsequent fetches - */ extractAndAddEmbeddedCredentials(urlForCredentials); fetch.setRemote(url.toString()); diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java index b511f9c82f..bb058ae900 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java @@ -334,7 +334,7 @@ static List gitRepoUrls() throws Exception { false, lfsSpecificTest); repos.add(repo); - /* Add embedded credentials test case if valid username, valid password, CLI git, and http protocol */ + /* Add embedded credentials test case if valid username, valid password, CLI git or JGit, and http protocol */ if (username != null && !username.matches(".*[@:].*") && // Skip special cases of username diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java index a07b3f3e5a..6d58f51231 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/JGitEmbeddedCredentialsTest.java @@ -2,14 +2,24 @@ import static org.junit.jupiter.api.Assertions.*; +import com.cloudbees.plugins.credentials.CredentialsDescriptor; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.model.TaskListener; +import hudson.plugins.git.GitException; import hudson.util.StreamTaskListener; import java.io.File; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.URIish; import org.jenkinsci.plugins.gitclient.jgit.SmartCredentialsProvider; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.jvnet.hudson.test.Issue; /** * Test that verifies JENKINS-69507 fix: JGit should extract and use @@ -18,140 +28,255 @@ * * @author Akash Manna */ +@Issue("JENKINS-69507") class JGitEmbeddedCredentialsTest { + /** Refuses connections immediately, so a fetch fails without reaching the network. */ + private static final String UNREACHABLE_AUTHORITY = "localhost:1"; + @TempDir - File tempDir; + private File tempDir; + + private JGitAPIImpl newClient(String name) throws Exception { + TaskListener listener = StreamTaskListener.fromStdout(); + File workspace = new File(tempDir, name); + assertTrue(workspace.mkdirs(), "Failed to create " + workspace); + return new JGitAPIImpl(workspace, listener); + } + + private JGitAPIImpl newInitializedClient(String name) throws Exception { + JGitAPIImpl gitClient = newClient(name); + gitClient.init_().workspace(gitClient.getWorkTree().getRemote()).execute(); + return gitClient; + } + + private static void extractCredentials(JGitAPIImpl gitClient, URIish url) throws Exception { + Method method = JGitAPIImpl.class.getDeclaredMethod("extractAndAddEmbeddedCredentials", URIish.class); + method.setAccessible(true); + method.invoke(gitClient, new Object[] {url}); + } + + private static Map registeredCredentials(JGitAPIImpl gitClient) { + Map credentials = + new HashMap<>(gitClient.getProvider().getCredentials()); + credentials.remove(""); + return credentials; + } + + private static StandardUsernamePasswordCredentials assertRegistered( + JGitAPIImpl gitClient, String key, String expectedUsername, String expectedPassword) { + Map credentials = registeredCredentials(gitClient); + StandardCredentials registered = credentials.get(key); + assertNotNull(registered, "No credentials registered for " + key + ", registered " + credentials.keySet()); + StandardUsernamePasswordCredentials usernamePassword = + assertInstanceOf(StandardUsernamePasswordCredentials.class, registered); + assertEquals(expectedUsername, usernamePassword.getUsername()); + assertEquals(expectedPassword, usernamePassword.getPassword().getPlainText()); + return usernamePassword; + } + + private static void assertProviderAuthenticates( + SmartCredentialsProvider provider, String url, String expectedUsername, String expectedPassword) + throws Exception { + CredentialItem.Username username = new CredentialItem.Username(); + CredentialItem.Password password = new CredentialItem.Password(); + assertTrue(provider.get(new URIish(url), username, password), "No credentials provided for " + url); + assertEquals(expectedUsername, username.getValue()); + assertEquals(expectedPassword, new String(password.getValue())); + } - /** - * Test that extractAndAddEmbeddedCredentials properly extracts username and password - * from a URL and adds them to the credentials provider. - */ @Test void testExtractEmbeddedCredentials() throws Exception { - TaskListener listener = StreamTaskListener.fromStdout(); - JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + JGitAPIImpl gitClient = newClient("extract"); - URIish urlWithCredentials = new URIish("https://testuser:testpass@example.com/repo.git"); + extractCredentials(gitClient, new URIish("https://testuser:testpass@example.com/repo.git")); - SmartCredentialsProvider provider = gitClient.getProvider(); + assertEquals(2, registeredCredentials(gitClient).size()); + assertRegistered(gitClient, "https://testuser@example.com/repo", "testuser", "testpass"); + assertRegistered(gitClient, "https://example.com/repo", "testuser", "testpass"); + assertProviderAuthenticates(gitClient.getProvider(), "https://example.com/repo.git", "testuser", "testpass"); + assertProviderAuthenticates( + gitClient.getProvider(), "https://testuser@example.com/repo.git", "testuser", "testpass"); + } - var credsBefore = provider.getCredentials(); - assertFalse( - credsBefore.containsKey("https://testuser:testpass@example.com/repo.git") - || credsBefore.containsKey("https://example.com/repo.git"), - "Credentials should not exist before extraction"); + @Test + void testExtractPercentEncodedCredentials() throws Exception { + JGitAPIImpl gitClient = newClient("encoded"); - gitClient.addCredentials(urlWithCredentials.toString(), createTestCredentials("testuser", "testpass")); + extractCredentials( + gitClient, new URIish("https://user%40domain.com:p%40ss%3Aword@example.com:8443/team/repo.git")); - var credsAfter = provider.getCredentials(); - assertTrue(credsAfter.size() > credsBefore.size(), "Credentials should be added"); + assertEquals(2, registeredCredentials(gitClient).size()); + assertRegistered( + gitClient, "https://user%40domain.com@example.com:8443/team/repo", "user@domain.com", "p@ss:word"); + assertRegistered(gitClient, "https://example.com:8443/team/repo", "user@domain.com", "p@ss:word"); } - /** - * Test that URLs without credentials don't cause issues - */ @Test - void testUrlWithoutCredentials() throws Exception { - TaskListener listener = StreamTaskListener.fromStdout(); - JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + void testExtractCredentialsFromIPv6UrlWithPort() throws Exception { + JGitAPIImpl gitClient = newClient("ipv6"); - URIish urlWithoutCredentials = new URIish("https://example.com/repo.git"); + extractCredentials(gitClient, new URIish("https://ipv6user:ipv6pass@[fe80::1]:8443/repo.git")); - assertDoesNotThrow(() -> { - gitClient.addCredentials(urlWithoutCredentials.toString(), createTestCredentials("user", "pass")); - }); + assertEquals(2, registeredCredentials(gitClient).size()); + assertRegistered(gitClient, "https://ipv6user@[fe80::1]:8443/repo", "ipv6user", "ipv6pass"); + assertRegistered(gitClient, "https://[fe80::1]:8443/repo", "ipv6user", "ipv6pass"); } - /** - * Test that URLs with only username (no password) are handled correctly - */ @Test void testUrlWithOnlyUsername() throws Exception { - TaskListener listener = StreamTaskListener.fromStdout(); - JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + JGitAPIImpl gitClient = newClient("user-only"); - URIish urlWithOnlyUser = new URIish("https://testuser@example.com/repo.git"); + extractCredentials(gitClient, new URIish("https://testuser@example.com/repo.git")); - assertDoesNotThrow(() -> { - gitClient.addCredentials(urlWithOnlyUser.toString(), createTestCredentials("testuser", "pass")); - }); + assertTrue(registeredCredentials(gitClient).isEmpty(), "Credentials registered without a password"); } - /** - * Test the scenario described in JENKINS-69507: credentials should be extracted - * when a remote name is resolved to a URL with embedded credentials. - * This simulates the case where: - * 1. First fetch works with URL containing credentials - * 2. URL is stored in git config as remote "origin" - * 3. Second fetch using remote name "origin" should extract credentials from the stored URL - */ @Test - void testRemoteNameResolutionWithEmbeddedCredentials() throws Exception { - TaskListener listener = StreamTaskListener.fromStdout(); - JGitAPIImpl gitClient = new JGitAPIImpl(tempDir, listener); + void testUrlWithoutCredentials() throws Exception { + JGitAPIImpl gitClient = newClient("no-credentials"); - gitClient.init_().workspace(tempDir.getAbsolutePath()).execute(); + extractCredentials(gitClient, new URIish("https://example.com/repo.git")); - String urlWithCreds = "https://testuser:testpass@example.com/repo.git"; - gitClient.setRemoteUrl("origin", urlWithCreds); + assertTrue(registeredCredentials(gitClient).isEmpty(), "Credentials registered for an anonymous URL"); + } + + @Test + void testNonHttpUrlsAreIgnored() throws Exception { + JGitAPIImpl gitClient = newClient("other-protocols"); - String storedUrl = gitClient.getRemoteUrl("origin"); - assertNotNull(storedUrl, "Remote URL should be stored"); - assertEquals(urlWithCreds, storedUrl, "Stored URL should match"); + extractCredentials(gitClient, new URIish("ssh://sshuser:sshpass@example.com/repo.git")); + extractCredentials(gitClient, new URIish("git@example.com:jenkinsci/git-client-plugin.git")); + extractCredentials( + gitClient, + new URIish(new File(tempDir, "other-protocols").toURI().toString())); + extractCredentials(gitClient, null); - URIish resolvedUrl = new URIish(storedUrl); - assertEquals("testuser", resolvedUrl.getUser(), "Username should be in URL"); - assertEquals("testpass", resolvedUrl.getPass(), "Password should be in URL"); + assertTrue(registeredCredentials(gitClient).isEmpty(), "Credentials registered for a non http URL"); } - /** - * Static inner class for test credentials to avoid SpotBugs warnings. - */ - private static class TestCredentials implements StandardUsernamePasswordCredentials { - private final String username; - private final String password; + @Test + void testRepeatedExtractionDoesNotGrowProvider() throws Exception { + JGitAPIImpl gitClient = newClient("repeated"); + URIish url = new URIish("https://testuser:testpass@example.com/repo.git"); - TestCredentials(String username, String password) { - this.username = username; - this.password = password; + for (int i = 0; i < 5; i++) { + extractCredentials(gitClient, url); } - @Override - public String getDescription() { - return "Test credentials"; - } + assertEquals(2, registeredCredentials(gitClient).size()); + } - @Override - public String getId() { - return "test-id"; - } + @Test + void testEmbeddedCredentialsProvideDescriptor() throws Exception { + JGitAPIImpl gitClient = newClient("descriptor"); - @Override - public com.cloudbees.plugins.credentials.CredentialsScope getScope() { - return com.cloudbees.plugins.credentials.CredentialsScope.GLOBAL; - } + extractCredentials(gitClient, new URIish("https://testuser:testpass@example.com/repo.git")); - @Override - public com.cloudbees.plugins.credentials.CredentialsDescriptor getDescriptor() { - throw new UnsupportedOperationException(); - } + StandardUsernamePasswordCredentials credentials = + assertRegistered(gitClient, "https://example.com/repo", "testuser", "testpass"); + CredentialsDescriptor descriptor = credentials.getDescriptor(); + assertNotNull(descriptor, "Embedded credentials have no descriptor"); + assertEquals("Embedded URL credentials", descriptor.getDisplayName()); + } - @Override - public String getUsername() { - return username; - } + @Test + void testEmbeddedCredentialsEqualsAndHashCode() throws Exception { + JGitAPIImpl first = newClient("equals-first"); + JGitAPIImpl second = newClient("equals-second"); + JGitAPIImpl third = newClient("equals-third"); - @Override - public hudson.util.Secret getPassword() { - return hudson.util.Secret.fromString(password); - } + extractCredentials(first, new URIish("https://testuser:testpass@example.com/repo.git")); + extractCredentials(second, new URIish("https://testuser:testpass@example.com/other.git")); + extractCredentials(third, new URIish("https://testuser:different@example.com/repo.git")); + + StandardCredentials firstCredentials = registeredCredentials(first).get("https://example.com/repo"); + StandardCredentials secondCredentials = registeredCredentials(second).get("https://example.com/other"); + StandardCredentials thirdCredentials = registeredCredentials(third).get("https://example.com/repo"); + + assertEquals(firstCredentials, secondCredentials); + assertEquals(firstCredentials.hashCode(), secondCredentials.hashCode()); + assertNotEquals(firstCredentials, thirdCredentials); + assertNotEquals(firstCredentials, null); + assertNotEquals(firstCredentials, "not a credential"); } - /** - * Helper method to create test credentials - */ - private StandardUsernamePasswordCredentials createTestCredentials(String username, String password) { - return new TestCredentials(username, password); + @Test + void testFetchWithEmbeddedCredentialsPopulatesProvider() throws Exception { + JGitAPIImpl gitClient = newInitializedClient("fetch-url"); + URIish url = new URIish("https://fetchuser:fetchpass@" + UNREACHABLE_AUTHORITY + "/repo.git"); + List refSpecs = List.of(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + + assertTrue(registeredCredentials(gitClient).isEmpty()); + assertThrows( + GitException.class, + () -> gitClient + .fetch_() + .from(url, refSpecs) + .tags(false) + .timeout(1) + .execute()); + + assertRegistered(gitClient, "https://fetchuser@" + UNREACHABLE_AUTHORITY + "/repo", "fetchuser", "fetchpass"); + assertRegistered(gitClient, "https://" + UNREACHABLE_AUTHORITY + "/repo", "fetchuser", "fetchpass"); + assertProviderAuthenticates( + gitClient.getProvider(), "https://" + UNREACHABLE_AUTHORITY + "/repo.git", "fetchuser", "fetchpass"); + } + + @Test + void testFetchByRemoteNameResolvesEmbeddedCredentials() throws Exception { + JGitAPIImpl gitClient = newInitializedClient("fetch-remote-name"); + String urlWithCredentials = "https://remoteuser:remotepass@" + UNREACHABLE_AUTHORITY + "/repo.git"; + gitClient.setRemoteUrl("origin", urlWithCredentials); + assertEquals(urlWithCredentials, gitClient.getRemoteUrl("origin")); + + List refSpecs = List.of(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + URIish remoteName = new URIish("origin"); + + assertTrue(registeredCredentials(gitClient).isEmpty()); + assertThrows( + GitException.class, + () -> gitClient + .fetch_() + .from(remoteName, refSpecs) + .tags(false) + .timeout(1) + .execute()); + + assertRegistered( + gitClient, "https://remoteuser@" + UNREACHABLE_AUTHORITY + "/repo", "remoteuser", "remotepass"); + assertRegistered(gitClient, "https://" + UNREACHABLE_AUTHORITY + "/repo", "remoteuser", "remotepass"); + } + + @Test + void testFetchByRemoteNameWithoutEmbeddedCredentials() throws Exception { + JGitAPIImpl gitClient = newInitializedClient("fetch-remote-name-anonymous"); + gitClient.setRemoteUrl("origin", "https://" + UNREACHABLE_AUTHORITY + "/repo.git"); + + List refSpecs = List.of(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + URIish remoteName = new URIish("origin"); + + assertThrows( + GitException.class, + () -> gitClient + .fetch_() + .from(remoteName, refSpecs) + .tags(false) + .timeout(1) + .execute()); + + assertTrue(registeredCredentials(gitClient).isEmpty(), "Credentials registered for an anonymous remote"); + } + + @Test + void testFetchFromLocalRepositoryRegistersNoCredentials() throws Exception { + JGitAPIImpl bare = newInitializedClient("local-source"); + JGitAPIImpl gitClient = newInitializedClient("local-destination"); + + URIish source = new URIish(bare.getWorkTree().getRemote()); + List refSpecs = List.of(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + gitClient.fetch_().from(source, refSpecs).tags(false).execute(); + + assertTrue(registeredCredentials(gitClient).isEmpty(), "Credentials registered for a local fetch"); } }