From 58d5501a0ec7d9eca8fbc8bc21a0ce5cb6da116f Mon Sep 17 00:00:00 2001 From: Daniel Kleveros Date: Mon, 8 Sep 2025 15:21:07 -0700 Subject: [PATCH 1/3] adding git.systemGitAuthProviders option to git.oauth --- .../concord/agent/RepositoryManager.java | 1 + .../concord/agent/cfg/GitConfiguration.java | 53 ++++++++++++++++ common/pom.xml | 19 +++++- .../concord/common/secret/SecretUtils.java | 62 +++++++++++++++++-- .../concord/repository/AuthType.java | 41 ++++++++++++ .../concord/repository/GitAuthProvider.java | 50 +++++++++++++++ .../repository/GitClientConfiguration.java | 14 ++--- .../concord/server/cfg/GitConfiguration.java | 11 ++++ .../server/repository/RepositoryManager.java | 1 + 9 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 repository/src/main/java/com/walmartlabs/concord/repository/AuthType.java create mode 100644 repository/src/main/java/com/walmartlabs/concord/repository/GitAuthProvider.java diff --git a/agent/src/main/java/com/walmartlabs/concord/agent/RepositoryManager.java b/agent/src/main/java/com/walmartlabs/concord/agent/RepositoryManager.java index e743d4203d..ccf723dd36 100644 --- a/agent/src/main/java/com/walmartlabs/concord/agent/RepositoryManager.java +++ b/agent/src/main/java/com/walmartlabs/concord/agent/RepositoryManager.java @@ -58,6 +58,7 @@ public RepositoryManager(SecretClient secretClient, GitClientConfiguration clientCfg = GitClientConfiguration.builder() .oauthToken(gitCfg.getToken()) + .systemGitAuthProviders(gitCfg.getSystemGitAuthProviders()) .defaultOperationTimeout(gitCfg.getDefaultOperationTimeout()) .fetchTimeout(gitCfg.getFetchTimeout()) .httpLowSpeedLimit(gitCfg.getHttpLowSpeedLimit()) diff --git a/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java b/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java index 19d63aa9a7..549cce1f8d 100644 --- a/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java +++ b/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java @@ -21,14 +21,21 @@ */ import com.typesafe.config.Config; +import com.walmartlabs.concord.repository.AuthType; +import com.walmartlabs.concord.repository.GitAuthProvider; +import com.walmartlabs.concord.repository.ImmutableGitAuthProvider; import javax.inject.Inject; import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault; public class GitConfiguration { + private final List systemGitAuthProviders; private final String token; private final boolean shallowClone; private final boolean checkAlreadyFetched; @@ -43,6 +50,11 @@ public class GitConfiguration { @Inject public GitConfiguration(Config cfg) { this.token = getStringOrDefault(cfg, "git.oauth", () -> null); + this.systemGitAuthProviders = cfg.hasPath("systemGitAuthProviders") + ? cfg.getConfigList("systemGitAuthProviders").stream() + .map(GitConfiguration::buildAuthProvider) + .collect(Collectors.toList()) + : null; this.shallowClone = cfg.getBoolean("git.shallowClone"); this.checkAlreadyFetched = cfg.getBoolean("git.checkAlreadyFetched"); this.defaultOperationTimeout = cfg.getDuration("git.defaultOperationTimeout"); @@ -54,6 +66,47 @@ public GitConfiguration(Config cfg) { this.skip = cfg.getBoolean("git.skip"); } + private static GitAuthProvider buildAuthProvider(Config c) { + ImmutableGitAuthProvider.Builder b = ImmutableGitAuthProvider.builder() + .authType(AuthType.valueOf(c.getString("type"))) + .baseUrl(getOptString(c, "baseUrl")); + + // Optional fields depending on type + if (c.hasPath("oauthToken")) { + b.oauthToken(c.getString("oauthToken")); + } + if (c.hasPath("clientId")) { + b.clientId(c.getString("clientId")); + } + if (c.hasPath("privateKey")) { + b.privateKey(c.getString("privateKey")); + } + if (c.hasPath("installationId")) { + b.installationId(c.getString("installationId")); + } + return b.build(); + } + + private static String getOptString(Config c, String k) { + return c.hasPath(k) ? c.getString(k) : null; + } + + private static boolean getBoolean(Config c, String k, boolean def) { + return c.hasPath(k) ? c.getBoolean(k) : def; + } + + private static int getInt(Config c, String k, int def) { + return c.hasPath(k) ? c.getInt(k) : def; + } + + private static Duration getDuration(Config c, String k, Duration def) { + return c.hasPath(k) ? c.getDuration(k) : def; + } + + public List getSystemGitAuthProviders() { + return systemGitAuthProviders; + } + public String getToken() { return token; } diff --git a/common/pom.xml b/common/pom.xml index 196c536d2a..1f7836c8f2 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -69,11 +69,28 @@ jackson-datatype-guava provided - + com.fasterxml.jackson.datatype jackson-datatype-jsr310 provided + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + diff --git a/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java b/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java index 9e9e6d5bd3..15195e6f78 100644 --- a/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java +++ b/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java @@ -27,10 +27,20 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Jwts; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.PrivateKey; +import java.security.*; +import java.time.Duration; +import java.util.Date; +import java.util.UUID; public final class SecretUtils { @@ -116,4 +126,48 @@ public static byte[] generateSalt(int size) { private SecretUtils() { } + + private static String generateJwtToken(String clientId, PrivateKey privateKey, long expirationTimeInSeconds) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + (expirationTimeInSeconds * 1000)); + + return Jwts.builder() + .issuer(clientId) + .subject(clientId) + .audience().add("concord").and() + .issuedAt(now) + .expiration(expiration) + .id(UUID.randomUUID().toString()) + .signWith(privateKey) + .compact(); + } + + public static String generateGitHubInstallationToken(String appId, PrivateKey privateKey, String installationId) throws IOException, InterruptedException { + // Generate JWT token for GitHub App authentication (expires in 10 minutes) + String jwtToken = generateJwtToken(appId, privateKey, 3600); + + // Make API call to generate installation access token + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://api.github.com/app/installations/" + installationId + "/access_tokens")) + .header("Authorization", "Bearer " + jwtToken) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 201) { + throw new IOException("Failed to generate installation token: " + response.statusCode() + " " + response.body()); + } + + // Parse the access token from JSON response + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(response.body()); + return jsonNode.get("token").asText(); + } } diff --git a/repository/src/main/java/com/walmartlabs/concord/repository/AuthType.java b/repository/src/main/java/com/walmartlabs/concord/repository/AuthType.java new file mode 100644 index 0000000000..a0cbda8d0d --- /dev/null +++ b/repository/src/main/java/com/walmartlabs/concord/repository/AuthType.java @@ -0,0 +1,41 @@ +package com.walmartlabs.concord.repository; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2025 Walmart Inc. + * ----- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ===== + */ + +public enum AuthType { + OAUTH_TOKEN("oauthToken"), + GITHUB_APP_INSTALLATION("githubAppInstallation"); + + private final String value; + + AuthType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } +} \ No newline at end of file diff --git a/repository/src/main/java/com/walmartlabs/concord/repository/GitAuthProvider.java b/repository/src/main/java/com/walmartlabs/concord/repository/GitAuthProvider.java new file mode 100644 index 0000000000..5a2659be3a --- /dev/null +++ b/repository/src/main/java/com/walmartlabs/concord/repository/GitAuthProvider.java @@ -0,0 +1,50 @@ +package com.walmartlabs.concord.repository; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2025 Walmart Inc. + * ----- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ===== + */ + +import org.immutables.value.Value; +import javax.annotation.Nullable; + +@Value.Immutable +@Value.Style(jdkOnly = true) +public interface GitAuthProvider { + + AuthType authType(); + + @Nullable + String baseUrl(); + + @Nullable + String oauthToken(); + + @Nullable + String installationId(); + + @Nullable + String clientId(); + + @Nullable + String privateKey(); + + static ImmutableGitAuthProvider.Builder builder() { + return ImmutableGitAuthProvider.builder(); + } +} diff --git a/repository/src/main/java/com/walmartlabs/concord/repository/GitClientConfiguration.java b/repository/src/main/java/com/walmartlabs/concord/repository/GitClientConfiguration.java index 523f7494a9..17d68ce426 100644 --- a/repository/src/main/java/com/walmartlabs/concord/repository/GitClientConfiguration.java +++ b/repository/src/main/java/com/walmartlabs/concord/repository/GitClientConfiguration.java @@ -24,6 +24,7 @@ import javax.annotation.Nullable; import java.time.Duration; +import java.util.Collections; import java.util.List; @Value.Immutable @@ -32,7 +33,7 @@ public interface GitClientConfiguration { @Nullable String oauthToken(); - + @Nullable List authorizedGitHosts(); @@ -40,33 +41,32 @@ public interface GitClientConfiguration { default Duration defaultOperationTimeout() { return Duration.ofMinutes(10L); } - @Value.Default default Duration fetchTimeout() { return Duration.ofMinutes(10L); } - @Value.Default default int httpLowSpeedLimit() { return 0; } - @Value.Default default Duration httpLowSpeedTime() { return Duration.ofMinutes(0L); } - @Value.Default default Duration sshTimeout() { return Duration.ofMinutes(10); } - @Value.Default default int sshTimeoutRetryCount() { return 1; } + @Value.Default + default List systemGitAuthProviders() { + return Collections.emptyList(); + } static ImmutableGitClientConfiguration.Builder builder() { return ImmutableGitClientConfiguration.builder(); } -} +} \ No newline at end of file diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/cfg/GitConfiguration.java b/server/impl/src/main/java/com/walmartlabs/concord/server/cfg/GitConfiguration.java index 07bfd08971..1ecf88ba2e 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/cfg/GitConfiguration.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/cfg/GitConfiguration.java @@ -21,6 +21,8 @@ */ import com.walmartlabs.concord.config.Config; +import com.walmartlabs.concord.repository.GitAuthProvider; + import org.eclipse.sisu.Nullable; import javax.inject.Inject; @@ -37,6 +39,11 @@ public class GitConfiguration implements Serializable { @Nullable private String oauthToken; + @Inject + @Config("git.systemGitAuthProviders") + @Nullable + private List systemGitAuthProviders; + @Inject @Config("git.authorizedGitHosts") @Nullable @@ -94,6 +101,10 @@ public String getOauthToken() { return oauthToken; } + public List getSystemGitAuthProviders() { + return systemGitAuthProviders; + } + public List getAuthorizedGitHosts() { return authorizedGitHosts; } diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryManager.java b/server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryManager.java index c50f8c5145..df9c33067b 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryManager.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryManager.java @@ -67,6 +67,7 @@ public RepositoryManager(ObjectMapper objectMapper, GitClientConfiguration gitCliCfg = GitClientConfiguration.builder() .oauthToken(gitCfg.getOauthToken()) + .systemGitAuthProviders(gitCfg.getSystemGitAuthProviders()) .authorizedGitHosts(gitCfg.getAuthorizedGitHosts()) .defaultOperationTimeout(gitCfg.getDefaultOperationTimeout()) .fetchTimeout(gitCfg.getFetchTimeout()) From 0004d2e2463c59d0503b09cfd79cdc9e7a204ea7 Mon Sep 17 00:00:00 2001 From: Daniel Kleveros Date: Tue, 9 Sep 2025 00:14:08 -0700 Subject: [PATCH 2/3] Adding resolver for GitAuthProviders and tests --- .../concord/agent/cfg/GitConfiguration.java | 1 - .../concord/common/secret/SecretUtils.java | 12 +- repository/pom.xml | 16 +++ .../concord/repository/GitAuthResolver.java | 78 ++++++++++ .../concord/repository/GitClient.java | 10 +- .../repository/GitClientUpdateUrlTest.java | 135 ++++++++++++++++++ 6 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 repository/src/main/java/com/walmartlabs/concord/repository/GitAuthResolver.java create mode 100644 repository/src/test/java/com/walmartlabs/concord/repository/GitClientUpdateUrlTest.java diff --git a/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java b/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java index 549cce1f8d..ea6f965a6f 100644 --- a/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java +++ b/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java @@ -27,7 +27,6 @@ import javax.inject.Inject; import java.time.Duration; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; diff --git a/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java b/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java index 15195e6f78..d8ee071491 100644 --- a/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java +++ b/common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java @@ -127,11 +127,14 @@ public static byte[] generateSalt(int size) { private SecretUtils() { } - private static String generateJwtToken(String clientId, PrivateKey privateKey, long expirationTimeInSeconds) { + public static String generateGitHubInstallationToken(String clientId, PrivateKey privateKey, String installationId) throws IOException, InterruptedException { + + // Generate JWT token for GitHub App authentication (expires in 10 minutes) Date now = new Date(); + long expirationTimeInSeconds = 3600; Date expiration = new Date(now.getTime() + (expirationTimeInSeconds * 1000)); - return Jwts.builder() + String jwtToken = Jwts.builder() .issuer(clientId) .subject(clientId) .audience().add("concord").and() @@ -140,11 +143,6 @@ private static String generateJwtToken(String clientId, PrivateKey privateKey, l .id(UUID.randomUUID().toString()) .signWith(privateKey) .compact(); - } - - public static String generateGitHubInstallationToken(String appId, PrivateKey privateKey, String installationId) throws IOException, InterruptedException { - // Generate JWT token for GitHub App authentication (expires in 10 minutes) - String jwtToken = generateJwtToken(appId, privateKey, 3600); // Make API call to generate installation access token HttpClient client = HttpClient.newBuilder() diff --git a/repository/pom.xml b/repository/pom.xml index a887559fd3..58db5b7b75 100644 --- a/repository/pom.xml +++ b/repository/pom.xml @@ -85,5 +85,21 @@ org.eclipse.jgit test + + org.mockito + mockito-core + test + + + org.mockito + mockito-inline + 5.2.0 + test + + + org.mockito + mockito-junit-jupiter + test + diff --git a/repository/src/main/java/com/walmartlabs/concord/repository/GitAuthResolver.java b/repository/src/main/java/com/walmartlabs/concord/repository/GitAuthResolver.java new file mode 100644 index 0000000000..bed9d0b860 --- /dev/null +++ b/repository/src/main/java/com/walmartlabs/concord/repository/GitAuthResolver.java @@ -0,0 +1,78 @@ +package com.walmartlabs.concord.repository; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2025 Walmart Inc. + * ----- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ===== + */ + +import com.walmartlabs.concord.common.secret.SecretUtils; + +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.List; + +public class GitAuthResolver { + + public static String resolveOAuthToken(String repoUrl, GitClientConfiguration cfg) { + List providers = cfg.systemGitAuthProviders(); + if (providers == null) { + return null; + } + + for (GitAuthProvider provider : providers) { + String baseUrl = provider.baseUrl(); + if (baseUrl == null) { + continue; + } + if (!repoUrl.startsWith(baseUrl)) { + continue; + } + + if (provider.authType() == AuthType.OAUTH_TOKEN) { + return provider.oauthToken(); + } else if (provider.authType() == AuthType.GITHUB_APP_INSTALLATION) { + String installationId = provider.installationId(); + String clientId = provider.clientId(); + String privateKeyPem = provider.privateKey(); + if (installationId == null || clientId == null || privateKeyPem == null) { + continue; // incomplete config, try next + } + try { + PrivateKey privateKey = toPrivateKey(privateKeyPem); + // Order: installationId, clientId, privateKey (per request) + return SecretUtils.generateGitHubInstallationToken(provider.clientId(), privateKey, installationId); + } catch (Exception e) { + throw new RuntimeException("Failed to generate GitHub installation token", e); + } + } + } + return null; + } + + private static PrivateKey toPrivateKey(String pem) throws Exception { + String normalized = pem + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + byte[] der = Base64.getDecoder().decode(normalized); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(der); + return KeyFactory.getInstance("RSA").generatePrivate(spec); + } +} diff --git a/repository/src/main/java/com/walmartlabs/concord/repository/GitClient.java b/repository/src/main/java/com/walmartlabs/concord/repository/GitClient.java index 5e2d79fbcd..2928e61af5 100644 --- a/repository/src/main/java/com/walmartlabs/concord/repository/GitClient.java +++ b/repository/src/main/java/com/walmartlabs/concord/repository/GitClient.java @@ -314,14 +314,16 @@ private String updateUrl(String url, Secret secret) { return url; } - if (secret != null || cfg.oauthToken() == null || url.contains("@") || !url.startsWith("https://")) { + if (secret != null || (cfg.oauthToken() == null && cfg.systemGitAuthProviders() == null) || url.contains("@") || !url.startsWith("https://")) { // provided url already has credentials OR there are no default credentials to use. // anonymous auth is the only viable option. return url; } - // using default credentials - return "https://" + cfg.oauthToken() + "@" + url.substring("https://".length()); + // SystemGitAuthProviders have precedence over git.oauth as default credentials. + String oauthToken = cfg.systemGitAuthProviders() != null ? GitAuthResolver.resolveOAuthToken(url, cfg) : cfg.oauthToken(); + + return "https://" + oauthToken + "@" + url.substring("https://".length()); } private void updateSubmodules(Path workDir, Secret secret) { @@ -576,6 +578,8 @@ private String hideSensitiveData(String s) { return s; } + + private Path createUnixGitSSH(Path key) throws IOException { Path ssh = PathUtils.createTempFile("ssh", ".sh"); diff --git a/repository/src/test/java/com/walmartlabs/concord/repository/GitClientUpdateUrlTest.java b/repository/src/test/java/com/walmartlabs/concord/repository/GitClientUpdateUrlTest.java new file mode 100644 index 0000000000..12d03690f3 --- /dev/null +++ b/repository/src/test/java/com/walmartlabs/concord/repository/GitClientUpdateUrlTest.java @@ -0,0 +1,135 @@ +package com.walmartlabs.concord.repository; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2025 Walmart Inc. + * ----- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ===== + */ + +import com.walmartlabs.concord.common.secret.SecretUtils; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +public class GitClientUpdateUrlTest { + + @Test + public void testTwoSystemProvidersBothAuthTypes() throws Exception { + GitAuthProvider oauth = Mockito.mock(GitAuthProvider.class); + when(oauth.baseUrl()).thenReturn("https://example.com"); + when(oauth.authType()).thenReturn(AuthType.OAUTH_TOKEN); + when(oauth.oauthToken()).thenReturn("oauthProvTok"); + + GitAuthProvider gha = Mockito.mock(GitAuthProvider.class); + when(gha.baseUrl()).thenReturn("https://gh.example.com"); + when(gha.authType()).thenReturn(AuthType.GITHUB_APP_INSTALLATION); + when(gha.installationId()).thenReturn("12345"); + when(gha.clientId()).thenReturn("clientId123"); + when(gha.privateKey()).thenReturn(dummyPrivateKeyPem()); + + GitClientConfiguration cfg = Mockito.mock(GitClientConfiguration.class); + when(cfg.oauthToken()).thenReturn(null); // no global token + when(cfg.authorizedGitHosts()).thenReturn(List.of("example.com", "gh.example.com")); + when(cfg.systemGitAuthProviders()).thenReturn(List.of(oauth, gha)); + mockTiming(cfg); + + try (MockedStatic ms = Mockito.mockStatic(SecretUtils.class)) { + ms.when(() -> SecretUtils.generateGitHubInstallationToken(eq("clientId123"), any(), eq("12345"))) + .thenReturn("appTok"); + GitClient client = new GitClient(cfg); + + String r1 = invokeUpdateUrl(client, "https://example.com/repo.git", null); + assertEquals("https://oauthProvTok@example.com/repo.git", r1); + + String r2 = invokeUpdateUrl(client, "https://gh.example.com/owner/repo.git", null); + assertEquals("https://appTok@gh.example.com/owner/repo.git", r2); + } + } + + @Test + public void testSingleGitHubAppProvider() throws Exception { + GitClientConfiguration cfg = Mockito.mock(GitClientConfiguration.class); + when(cfg.oauthToken()).thenReturn("defaultGitAuth"); // no global token + when(cfg.authorizedGitHosts()).thenReturn(List.of("example.com", "gh.example.com")); + when(cfg.systemGitAuthProviders()).thenReturn(null); + mockTiming(cfg); + + try (MockedStatic ms = Mockito.mockStatic(SecretUtils.class)) { + ms.when(() -> SecretUtils.generateGitHubInstallationToken(eq("clientId123"), any(), eq("12345"))) + .thenReturn("appTok"); + GitClient client = new GitClient(cfg); + + // default url should be returned as-is + String r1 = invokeUpdateUrl(client, "https://example.com/repo.git", null); + assertEquals("https://defaultGitAuth@example.com/repo.git", r1); + } + } + + private static void mockTiming(GitClientConfiguration cfg) { + when(cfg.fetchTimeout()).thenReturn(Duration.ofSeconds(30)); + when(cfg.defaultOperationTimeout()).thenReturn(Duration.ofSeconds(30)); + when(cfg.httpLowSpeedLimit()).thenReturn(0); + when(cfg.httpLowSpeedTime()).thenReturn(Duration.ofSeconds(0)); + when(cfg.sshTimeout()).thenReturn(Duration.ofSeconds(30)); + when(cfg.sshTimeoutRetryCount()).thenReturn(1); + } + + private static String invokeUpdateUrl(GitClient client, String url, com.walmartlabs.concord.sdk.Secret secret) throws Exception { + Method m = GitClient.class.getDeclaredMethod("updateUrl", String.class, com.walmartlabs.concord.sdk.Secret.class); + m.setAccessible(true); + return (String) m.invoke(client, url, secret); + } + + private static String dummyPrivateKeyPem() { + return "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDObzvB6PvW6QEz\n" + + "fVkR6Tn6kG6DgGQ2wxYMX+0X2PkaIu+lX2ftheszl1St7uw0lP0k6j9jDF6v74mt\n" + + "mCjJ3Y1KzXoNTcCaqmReyFm2dwAWyMNhg5FrCce8K9ID1NDuKCYN6VRPvz9lcJ4Q\n" + + "lQ01vPSg6gLhGv+RtHnF9R9RC1K0M6EKi0mD1kZPRBuZJr4O7YCO00zXGbWJ3QMi\n" + + "7R3p5ghPT23HB8s1dBZU9fairyGbQHcBjGAslv+YfyKxo6LnV+JZK8qnDwP0oDjg\n" + + "GgL3wfr8PqXQNe7y6ojVYhXG1ob6gwpPb4gZ/QgkXVk9M879b8Sly/GbN6bIM1KX\n" + + "PPcL7Dc3AgMBAAECggEAGy6zhi1f2cW7lCGS3BSB1iZpAnLT2xx9UpRZVc4hPPES\n" + + "CtaQoJDX9i8Aa/vuHXe3UioYTFnTt98BFzq7J7ZP5m4O79lyULqZKXdyC1VVn21P\n" + + "AN2eRwyMi9CuavHwy3WvZbQXz3QNrmYJqZBN1uJGiH8K42Z6mV4Ut1irl65fPHXC\n" + + "o9gpq5JaGoMfjd+ZfA3c/Rr9p2MqV1WsTb96bWYxHkEAr7InB/vBY8dMus5msmqs\n" + + "1b5UmJce2W11MDUzmv23whumFcuVQH3Sbeocv1suZ/f1mE2roGwexkR6A4olh+3c\n" + + "0JY3X08rNOQ9SWulzAsDUd3o9sZIlTmvb2EvfRtciQKBgQDqS0X5qL0PTK5ZfphQ\n" + + "xHATXSZqOig6GaxUc6kB0iOMlIkdT6DAxF/Efj6cv9h/k0GwZC3XVUZBBgMscc4c\n" + + "IfpqADSxtARHbN2Ac0PlhwX8mXawrw/yzbH6M64yDkPOblRKNp4ZgK5bPwkSc3ED\n" + + "0Ep0Nv7J6PXH+o/mZwGphRjydQKBgQDk2ujy4F1k6OD4+DfZKsYF9QfiIpNnBomN\n" + + "qj5TrnmXChbXTJaGQPHOQlOmTWlYv5FSVNMPjsflIH/t5WEbVPwIWZVWToyR3n0C\n" + + "xCh1GfjLvaE4T9MDI4MFrnef0/wsWegxeo/76ZUqJFZPvq3wEnD4jyC1u3whd0/M\n" + + "JFrHC2RDhwKBgGnu7CVXnA1Ck3KBIg+zc1k1exogTD1+n/oPfsfv/llJUAUSyjqw\n" + + "v6zwAxCjF6QT8PXRGgaHwE7Ew2r0tSCDduv2aUM7E+GpSz9fxCUja2VfQ3x0Ikcg\n" + + "+yRiqrNoVarBz6BhRG6hSOBLD/lCQ4K2P+AaNboKmtQspLlf190bD7gtAoGAF4Ch\n" + + "J4cmEh2VEkDMS+Wk6ya9ygPp4YlzO8bT1QaxveznXyjcQl5HLwrgBzcZxbTlH1DB\n" + + "s3wSyK+jVbL5mwH8bh0l53sZr4agvCi7ISPZhPR8VYmn9MWbVdb+bZCgM3Vd+tL7\n" + + "Ai3G6yprJ1SeB4n4xeFCABwwvPfylGzPCf4XDTkCgYEAiksAv52h244hCgZynxCm\n" + + "90LIZpw3PqonW6gUblWGHXYgeBCfCwS8G7i3mluh9Iv/zVt8y7RnwNTAVXkZLo+s\n" + + "3WWzSurZUixqaOHJ3RmLO0jB1MkQnuwyEb5LHb3+CrbePxrLg00yWiLRgqd5joOR\n" + + "0pqiM3aXGjT8LWXSl6qOeUE=\n" + + "-----END PRIVATE KEY-----"; + } +} From 0326b12e0a1ec7ba6709d0d5bcf8339724adc16b Mon Sep 17 00:00:00 2001 From: Daniel Kleveros Date: Tue, 9 Sep 2025 00:44:49 -0700 Subject: [PATCH 3/3] fixing config path for git.systemGitAuthProviders --- .../com/walmartlabs/concord/agent/cfg/GitConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java b/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java index ea6f965a6f..923f6e34b4 100644 --- a/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java +++ b/agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java @@ -49,8 +49,8 @@ public class GitConfiguration { @Inject public GitConfiguration(Config cfg) { this.token = getStringOrDefault(cfg, "git.oauth", () -> null); - this.systemGitAuthProviders = cfg.hasPath("systemGitAuthProviders") - ? cfg.getConfigList("systemGitAuthProviders").stream() + this.systemGitAuthProviders = cfg.hasPath("git.systemGitAuthProviders") + ? cfg.getConfigList("git.systemGitAuthProviders").stream() .map(GitConfiguration::buildAuthProvider) .collect(Collectors.toList()) : null;