Skip to content

Commit 83557c2

Browse files
Remove GitHub library and just use GitHub API calls via WebClient. Use app installation tokens for API calls.
1 parent 9a86cd5 commit 83557c2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+874
-1165
lines changed

pom.xml

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,31 @@
7676
<version>1.19.8</version>
7777
<scope>test</scope>
7878
</dependency>
79+
80+
<dependency>
81+
<groupId>io.jsonwebtoken</groupId>
82+
<artifactId>jjwt-api</artifactId>
83+
<version>0.12.6</version>
84+
</dependency>
85+
<dependency>
86+
<groupId>io.jsonwebtoken</groupId>
87+
<artifactId>jjwt-impl</artifactId>
88+
<version>0.12.6</version>
89+
<scope>runtime</scope>
90+
</dependency>
91+
<dependency>
92+
<groupId>io.jsonwebtoken</groupId>
93+
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
94+
<version>0.12.6</version>
95+
<scope>runtime</scope>
96+
</dependency>
97+
<!-- BouncyCastle for PKCS#1 PEM parsing -->
98+
<dependency>
99+
<groupId>org.bouncycastle</groupId>
100+
<artifactId>bcprov-jdk15on</artifactId>
101+
<version>1.70</version>
102+
</dependency>
103+
79104
<dependency>
80105
<groupId>org.testcontainers</groupId>
81106
<artifactId>rabbitmq</artifactId>
@@ -89,13 +114,6 @@
89114
<scope>test</scope>
90115
</dependency>
91116

92-
<dependency>
93-
<groupId>org.kohsuke</groupId>
94-
<artifactId>github-api</artifactId>
95-
<version>1.316</version>
96-
</dependency>
97-
98-
99117
<dependency>
100118
<groupId>org.springframework.boot</groupId>
101119
<artifactId>spring-boot-starter-test</artifactId>

src/main/java/edu/stanford/webprotege/issues/WebprotegeGhIssuesServiceApplication.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
package edu.stanford.webprotege.issues;
22

3-
import edu.stanford.protege.github.GitHubRepositoryCoordinates;
4-
import edu.stanford.webprotege.issues.persistence.GitHubRepositoryLinkRecord;
5-
import edu.stanford.webprotege.issues.persistence.GitHubRepositoryLinkRecordStore;
6-
import edu.stanford.protege.webprotege.common.ProjectId;
73
import edu.stanford.protege.webprotege.ipc.WebProtegeIpcApplication;
84
import edu.stanford.protege.webprotege.jackson.WebProtegeJacksonApplication;
9-
import org.kohsuke.github.GitHub;
10-
import org.kohsuke.github.GitHubBuilder;
115
import org.slf4j.Logger;
126
import org.slf4j.LoggerFactory;
137
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,18 +24,10 @@ public static void main(String[] args) {
3024
SpringApplication.run(WebprotegeGhIssuesServiceApplication.class, args);
3125
}
3226

33-
@Bean
34-
public GitHub gitHub() throws IOException {
35-
return GitHubBuilder.fromEnvironment().build();
36-
}
37-
3827
@Autowired
3928
private ApplicationContext context;
4029

4130
@Override
4231
public void run(String... args) throws Exception {
43-
logger.warn("Forcing linked repo");
44-
context.getBean(GitHubRepositoryLinkRecordStore.class)
45-
.save(new GitHubRepositoryLinkRecord(ProjectId.valueOf("c6a5fed1-47eb-4be1-9570-7d3eefd9b579"), new GitHubRepositoryCoordinates("matthewhorridge", "testrepo"), null, true));
4632
}
4733
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.web.reactive.function.client.WebClient;
8+
import edu.stanford.webprotege.issues.model.GitHubInstallationId;
9+
10+
@Configuration
11+
public class GitHubAuthConfig {
12+
13+
@Bean
14+
public WebClient githubWebClient() {
15+
return WebClient.builder()
16+
.baseUrl("https://api.github.com" )
17+
.defaultHeader(HttpHeaders.ACCEPT, "application/vnd.github+json" )
18+
.build();
19+
}
20+
21+
@Bean
22+
GitHubInstallationTokenCacheProvider gitHubInstallationTokenCacheProvider() {
23+
return new GitHubInstallationTokenCacheProvider();
24+
}
25+
26+
@Bean
27+
public Cache<GitHubInstallationId, GitHubInstallationToken> tokenCache(GitHubInstallationTokenCacheProvider tokenCacheProvider) {
28+
return tokenCacheProvider.createTokenCache();
29+
}
30+
31+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import edu.stanford.webprotege.issues.model.GitHubPermissions;
6+
7+
import java.time.Instant;
8+
9+
@JsonIgnoreProperties(ignoreUnknown = true)
10+
public record GitHubInstallationToken(@JsonProperty("token") String token,
11+
@JsonProperty("expires_at") Instant expiresAt,
12+
@JsonProperty("permissions") GitHubPermissions permissions) {
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
import com.github.benmanes.caffeine.cache.Cache;
4+
import com.github.benmanes.caffeine.cache.Caffeine;
5+
import edu.stanford.webprotege.issues.model.GitHubInstallationId;
6+
7+
public class GitHubInstallationTokenCacheProvider {
8+
9+
public Cache<GitHubInstallationId, GitHubInstallationToken> createTokenCache() {
10+
return Caffeine.newBuilder()
11+
.expireAfter(new GitHubInstallationTokenExpiry())
12+
.build();
13+
}
14+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
import com.github.benmanes.caffeine.cache.Expiry;
4+
import edu.stanford.webprotege.issues.model.GitHubInstallationId;
5+
6+
import java.time.Duration;
7+
import java.time.Instant;
8+
9+
public class GitHubInstallationTokenExpiry implements Expiry<GitHubInstallationId, GitHubInstallationToken> {
10+
11+
@Override
12+
public long expireAfterCreate(GitHubInstallationId key, GitHubInstallationToken token, long currentTimeNanos) {
13+
Instant now = Instant.now();
14+
Duration untilExpiry = Duration.between(now, token.expiresAt().minusSeconds(5));
15+
long durationNanos = untilExpiry.toNanos();
16+
return Math.max(durationNanos, 0);
17+
}
18+
19+
@Override
20+
public long expireAfterUpdate(GitHubInstallationId key, GitHubInstallationToken token, long currentTimeNanos, long currentDurationNanos) {
21+
return currentDurationNanos;
22+
}
23+
24+
@Override
25+
public long expireAfterRead(GitHubInstallationId key, GitHubInstallationToken token, long currentTimeNanos, long currentDurationNanos) {
26+
return currentDurationNanos;
27+
}
28+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
public record GitHubJwt(String token) {
4+
5+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
4+
import edu.stanford.webprotege.issues.model.GitHubAppId;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
@Configuration
12+
public class GitHubJwtConfig {
13+
14+
public static final Logger logger = LoggerFactory.getLogger(GitHubJwtConfig.class);
15+
16+
@Bean
17+
public GitHubPrivateKeyLoader gitHubPrivateKeyLoader(@Value("${webprotege.github.pem-path:}") String pemPath) throws Exception {
18+
if(pemPath.isEmpty()) {
19+
logger.warn("Missing required property: webprotege.github.pem-path");
20+
}
21+
return new GitHubPrivateKeyLoader(pemPath);
22+
}
23+
24+
/**
25+
* Supplies a new short-lived JWT token (valid for 9 minutes) signed with GitHub App private key.
26+
*/
27+
@Bean
28+
public GitHubJwtFactory githubJwtSupplier(GitHubAppId appId,
29+
GitHubPrivateKeyLoader privateKeyLoader) {
30+
return new GitHubJwtFactory(appId, privateKeyLoader);
31+
}
32+
33+
@Bean
34+
GitHubAppId gitHubAppId(@Value("${webprotege.github.app-id}") String appId) {
35+
return new GitHubAppId(appId);
36+
}
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
import edu.stanford.webprotege.issues.model.GitHubAppId;
4+
import io.jsonwebtoken.Jwts;
5+
import io.jsonwebtoken.SignatureAlgorithm;
6+
7+
import java.time.Instant;
8+
import java.time.temporal.ChronoUnit;
9+
import java.util.Date;
10+
11+
public class GitHubJwtFactory {
12+
13+
private final GitHubAppId appId;
14+
15+
private final GitHubPrivateKeyLoader privateKeyLoader;
16+
17+
public GitHubJwtFactory(GitHubAppId appId,
18+
GitHubPrivateKeyLoader privateKeyLoader) {
19+
this.appId = appId;
20+
this.privateKeyLoader = privateKeyLoader;
21+
}
22+
23+
public GitHubJwt getJwt() {
24+
var now = Instant.now();
25+
var privateKey = privateKeyLoader.getPrivateKey();
26+
var token = Jwts.builder().issuer(appId.id())
27+
.issuedAt(Date.from(now))
28+
.expiration(Date.from(now.plus(10, ChronoUnit.MINUTES))) // GitHub max is 10 minutes
29+
.signWith(privateKey, SignatureAlgorithm.RS256)
30+
.compact();
31+
return new GitHubJwt(token);
32+
}
33+
34+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package edu.stanford.webprotege.issues.auth;
2+
3+
import org.bouncycastle.asn1.ASN1Sequence;
4+
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
5+
import org.bouncycastle.util.io.pem.PemReader;
6+
7+
import java.io.FileNotFoundException;
8+
import java.io.FileReader;
9+
import java.io.IOException;
10+
import java.security.KeyFactory;
11+
import java.security.NoSuchAlgorithmException;
12+
import java.security.PrivateKey;
13+
import java.security.spec.InvalidKeySpecException;
14+
import java.security.spec.RSAPrivateCrtKeySpec;
15+
16+
public class GitHubPrivateKeyLoader {
17+
18+
private final String pemPath;
19+
20+
public GitHubPrivateKeyLoader(String pemPath) {
21+
this.pemPath = pemPath;
22+
}
23+
24+
public PrivateKey getPrivateKey() {
25+
if(pemPath.isEmpty()) {
26+
throw new IllegalStateException("Path to .pem file is empty. Make sure that the path to .pem file is specified in the webprotege.github.pem-path property.");
27+
}
28+
try (var pemReader = new PemReader(new FileReader(pemPath))) {
29+
var content = pemReader.readPemObject().getContent();
30+
var rsa = RSAPrivateKey.getInstance(ASN1Sequence.getInstance(content));
31+
32+
var keySpec = new RSAPrivateCrtKeySpec(
33+
rsa.getModulus(),
34+
rsa.getPublicExponent(),
35+
rsa.getPrivateExponent(),
36+
rsa.getPrime1(),
37+
rsa.getPrime2(),
38+
rsa.getExponent1(),
39+
rsa.getExponent2(),
40+
rsa.getCoefficient()
41+
);
42+
var keyFactory = KeyFactory.getInstance("RSA");
43+
return keyFactory.generatePrivate(keySpec);
44+
45+
} catch (FileNotFoundException e) {
46+
throw new RuntimeException(e);
47+
} catch (IOException e) {
48+
throw new RuntimeException(e);
49+
} catch (NoSuchAlgorithmException e) {
50+
throw new RuntimeException(e);
51+
} catch (InvalidKeySpecException e) {
52+
throw new RuntimeException(e);
53+
}
54+
}
55+
}
56+

0 commit comments

Comments
 (0)