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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@
*/

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.List;
import java.util.stream.Collectors;

import static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault;

public class GitConfiguration {

private final List<GitAuthProvider> systemGitAuthProviders;
private final String token;
private final boolean shallowClone;
private final boolean checkAlreadyFetched;
Expand All @@ -43,6 +49,11 @@ public class GitConfiguration {
@Inject
public GitConfiguration(Config cfg) {
this.token = getStringOrDefault(cfg, "git.oauth", () -> null);
this.systemGitAuthProviders = cfg.hasPath("git.systemGitAuthProviders")
? cfg.getConfigList("git.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");
Expand All @@ -54,6 +65,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<GitAuthProvider> getSystemGitAuthProviders() {
return systemGitAuthProviders;
}

public String getToken() {
return token;
}
Expand Down
19 changes: 18 additions & 1 deletion common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,28 @@
<artifactId>jackson-datatype-guava</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>

<!-- JDK9+ compatibility -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -116,4 +126,46 @@ public static byte[] generateSalt(int size) {

private SecretUtils() {
}

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));

String jwtToken = Jwts.builder()
.issuer(clientId)
.subject(clientId)
.audience().add("concord").and()
.issuedAt(now)
.expiration(expiration)
.id(UUID.randomUUID().toString())
.signWith(privateKey)
.compact();

// 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<String> 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();
}
}
16 changes: 16 additions & 0 deletions repository/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,21 @@
<artifactId>org.eclipse.jgit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<GitAuthProvider> 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);
}
}
Loading
Loading