Skip to content

Commit 3307348

Browse files
committed
feat: allow one app to authenticate
against multiple app installations and orgs
1 parent bd50907 commit 3307348

File tree

10 files changed

+81
-25
lines changed

10 files changed

+81
-25
lines changed

src/main/java/org/kohsuke/github/GitHubClient.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ protected String getEncodedAuthorization() throws IOException {
219219
return authorizationProvider.getEncodedAuthorization();
220220
}
221221

222+
@CheckForNull
223+
protected String getEncodedAuthorization(URL url) throws IOException {
224+
return authorizationProvider.getEncodedAuthorization(url);
225+
}
226+
222227
@Nonnull
223228
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
224229
GHRateLimit result;

src/main/java/org/kohsuke/github/GitHubHttpUrlConnectionClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ static HttpURLConnection setupConnection(@Nonnull GitHubClient client, @Nonnull
110110
// if the authentication is needed but no credential is given, try it anyway (so that some calls
111111
// that do work with anonymous access in the reduced form should still work.)
112112
if (!request.headers().containsKey("Authorization")) {
113-
String authorization = client.getEncodedAuthorization();
113+
String authorization = client.getEncodedAuthorization(request.url());
114114
if (authorization != null) {
115115
connection.setRequestProperty("Authorization", client.getEncodedAuthorization());
116116
}

src/main/java/org/kohsuke/github/Requester.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import edu.umd.cs.findbugs.annotations.NonNull;
2727
import org.apache.commons.io.IOUtils;
28+
import org.kohsuke.github.authorization.AuthorizationProvider;
2829
import org.kohsuke.github.function.InputStreamFunction;
2930

3031
import java.io.ByteArrayInputStream;
@@ -158,4 +159,5 @@ public <R> PagedIterable<R> toIterable(Class<R[]> type, Consumer<R> itemInitiali
158159
}
159160

160161
}
162+
161163
}

src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.kohsuke.github.authorization;
22

33
import java.io.IOException;
4+
import java.net.URL;
45

56
/**
67
* Provides a functional interface that returns a valid encodedAuthorization. This strategy allows for a provider that
@@ -30,13 +31,18 @@ public interface AuthorizationProvider {
3031
* on any error that prevents the provider from getting a valid authorization
3132
*/
3233
String getEncodedAuthorization() throws IOException;
34+
String getEncodedAuthorization(URL url) throws IOException;
3335

3436
/**
3537
* A {@link AuthorizationProvider} that ensures that no credentials are returned
3638
*/
3739
class AnonymousAuthorizationProvider implements AuthorizationProvider {
3840
@Override
3941
public String getEncodedAuthorization() throws IOException {
42+
return getEncodedAuthorization(null);
43+
}
44+
@Override
45+
public String getEncodedAuthorization(URL url) throws IOException {
4046
return null;
4147
}
4248
}

src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.kohsuke.github.authorization;
22

33
import java.io.UnsupportedEncodingException;
4+
import java.net.URL;
45
import java.nio.charset.StandardCharsets;
56
import java.util.Base64;
67

@@ -92,10 +93,15 @@ public static AuthorizationProvider fromLoginAndPassword(String login, String pa
9293
}
9394

9495
@Override
95-
public String getEncodedAuthorization() {
96+
public String getEncodedAuthorization(URL url) {
9697
return this.authorization;
9798
}
9899

100+
@Override
101+
public String getEncodedAuthorization() {
102+
return getEncodedAuthorization(null);
103+
}
104+
99105
/**
100106
* An internal class representing all user-related credentials, which are credentials that have a login or should
101107
* query the user endpoint for the login matching this credential.

src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@
44
import org.kohsuke.github.GHAppInstallation;
55
import org.kohsuke.github.GHAppInstallationToken;
66
import org.kohsuke.github.GitHub;
7+
import org.kohsuke.github.PagedIterable;
78

89
import java.io.IOException;
10+
import java.net.URL;
911
import java.time.Duration;
1012
import java.time.Instant;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
1116
import java.util.Objects;
17+
import java.util.regex.Matcher;
18+
import java.util.regex.Pattern;
1219

1320
import javax.annotation.Nonnull;
1421

@@ -17,12 +24,12 @@
1724
*/
1825
public class OrgAppInstallationAuthorizationProvider extends GitHub.DependentAuthorizationProvider {
1926

20-
private final String organizationName;
27+
private static final Pattern pattern = Pattern.compile("/repos/(.*)/.*");
2128

22-
private String latestToken;
29+
private Map<String, String> latestToken = new HashMap<>();
2330

2431
@Nonnull
25-
private Instant validUntil = Instant.MIN;
32+
private Map<String, Instant> validUntil = new HashMap<>();
2633

2734
/**
2835
* Provides an AuthorizationProvider that performs automatic token refresh, based on an previously authenticated
@@ -36,28 +43,52 @@ public class OrgAppInstallationAuthorizationProvider extends GitHub.DependentAut
3643
*/
3744
@BetaApi
3845
@Deprecated
39-
public OrgAppInstallationAuthorizationProvider(String organizationName,
40-
AuthorizationProvider authorizationProvider) {
46+
public OrgAppInstallationAuthorizationProvider(AuthorizationProvider authorizationProvider) {
4147
super(authorizationProvider);
42-
this.organizationName = organizationName;
4348
}
4449

4550
@Override
46-
public String getEncodedAuthorization() throws IOException {
51+
public String getEncodedAuthorization(URL url) throws IOException {
4752
synchronized (this) {
48-
if (latestToken == null || Instant.now().isAfter(this.validUntil)) {
49-
refreshToken();
53+
String org = getOrgFromURL(url);
54+
if (latestToken.get(org) == null || this.validUntil.get(org) == null
55+
|| Instant.now().isAfter(this.validUntil.get(org))) {
56+
refreshToken(url);
5057
}
51-
return String.format("token %s", latestToken);
58+
return String.format("token %s", latestToken.get(org));
5259
}
5360
}
5461

55-
private void refreshToken() throws IOException {
56-
GitHub gitHub = this.gitHub();
57-
GHAppInstallation installationByOrganization = gitHub.getApp()
58-
.getInstallationByOrganization(this.organizationName);
59-
GHAppInstallationToken ghAppInstallationToken = installationByOrganization.createToken().create();
60-
this.validUntil = ghAppInstallationToken.getExpiresAt().toInstant().minus(Duration.ofMinutes(5));
61-
this.latestToken = Objects.requireNonNull(ghAppInstallationToken.getToken());
62+
@Override
63+
public String getEncodedAuthorization() throws IOException {
64+
return getEncodedAuthorization(null);
65+
}
66+
67+
private String getOrgFromURL(URL url) {
68+
if (url != null) {
69+
Matcher matcher = pattern.matcher(url.getPath());
70+
if (matcher.matches()) {
71+
return matcher.group(1);
72+
}
73+
}
74+
return "";
75+
}
76+
77+
private void refreshToken(URL url) throws IOException {
78+
List<GHAppInstallation> installations = this.gitHub().getApp().listInstallations().asList();
79+
// take the first one if no one matches
80+
GHAppInstallation installation = installations.get(0);
81+
String org = getOrgFromURL(url);
82+
for (GHAppInstallation ghAppInstallation : installations) {
83+
if (org.equals(installation.getAccount().getLogin())) {
84+
System.out.println(
85+
String.format("Found installation for path %s: %s", url.getPath(), installation.getHtmlUrl()));
86+
installation = ghAppInstallation;
87+
break;
88+
}
89+
}
90+
GHAppInstallationToken ghAppInstallationToken = installation.createToken().create();
91+
this.validUntil.put(org, ghAppInstallationToken.getExpiresAt().toInstant().minus(Duration.ofMinutes(5)));
92+
this.latestToken.put(org, Objects.requireNonNull(ghAppInstallationToken.getToken()));
6293
}
6394
}

src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import java.io.File;
99
import java.io.IOException;
10+
import java.net.URL;
1011
import java.nio.charset.StandardCharsets;
1112
import java.nio.file.Files;
1213
import java.nio.file.Path;
@@ -60,6 +61,11 @@ public JWTTokenProvider(String applicationId, PrivateKey privateKey) {
6061

6162
@Override
6263
public String getEncodedAuthorization() throws IOException {
64+
return getEncodedAuthorization(null);
65+
}
66+
67+
@Override
68+
public String getEncodedAuthorization(URL url) throws IOException {
6369
synchronized (this) {
6470
if (Instant.now().isAfter(validUntil)) {
6571
token = refreshJWT();

src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.kohsuke.github.authorization.OrgAppInstallationAuthorizationProvider;
66

77
import java.io.IOException;
8+
import java.net.URL;
89

910
import static org.hamcrest.CoreMatchers.equalTo;
1011
import static org.hamcrest.CoreMatchers.notNullValue;
@@ -18,7 +19,6 @@ public OrgAppInstallationAuthorizationProviderTest() {
1819
@Test(expected = HttpException.class)
1920
public void invalidJWTTokenRaisesException() throws IOException {
2021
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider(
21-
"testOrganization",
2222
ImmutableAuthorizationProvider.fromJwtToken("myToken"));
2323
gitHub = getGitHubBuilder().withAuthorizationProvider(provider)
2424
.withEndpoint(mockGitHub.apiServer().baseUrl())
@@ -29,12 +29,12 @@ public void invalidJWTTokenRaisesException() throws IOException {
2929

3030
@Test
3131
public void validJWTTokenAllowsOauthTokenRequest() throws IOException {
32-
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider("hub4j-test-org",
32+
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider(
3333
ImmutableAuthorizationProvider.fromJwtToken("bogus-valid-token"));
3434
gitHub = getGitHubBuilder().withAuthorizationProvider(provider)
3535
.withEndpoint(mockGitHub.apiServer().baseUrl())
3636
.build();
37-
String encodedAuthorization = provider.getEncodedAuthorization();
37+
String encodedAuthorization = provider.getEncodedAuthorization(new URL("https://api.github.com/repos/hub4j-test-org/github-api"));
3838

3939
assertThat(encodedAuthorization, notNullValue());
4040
assertThat(encodedAuthorization, equalTo("token v1.9a12d913f980a45a16ac9c3a9d34d9b7sa314cb6"));

src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
[{
22
"id": 11575015,
33
"account": {
44
"login": "hub4j-test-org",
@@ -40,4 +40,4 @@
4040
"single_file_name": null,
4141
"suspended_by": null,
4242
"suspended_at": null
43-
}
43+
}]

src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "9ffe1e34-1d0e-495a-abdc-86fdf1d15334",
33
"name": "orgs_hub4j-test-org_installation",
44
"request": {
5-
"url": "/orgs/hub4j-test-org/installation",
5+
"url": "/app/installations",
66
"method": "GET",
77
"headers": {
88
"Accept": {

0 commit comments

Comments
 (0)