diff --git a/src/main/java/org/kohsuke/github/GitHubClient.java b/src/main/java/org/kohsuke/github/GitHubClient.java index 9ba5b91026..c9c17378ab 100644 --- a/src/main/java/org/kohsuke/github/GitHubClient.java +++ b/src/main/java/org/kohsuke/github/GitHubClient.java @@ -219,6 +219,11 @@ protected String getEncodedAuthorization() throws IOException { return authorizationProvider.getEncodedAuthorization(); } + @CheckForNull + protected String getEncodedAuthorization(URL url) throws IOException { + return authorizationProvider.getEncodedAuthorization(url); + } + @Nonnull GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException { GHRateLimit result; diff --git a/src/main/java/org/kohsuke/github/GitHubHttpUrlConnectionClient.java b/src/main/java/org/kohsuke/github/GitHubHttpUrlConnectionClient.java index 0fcfc65769..05703a785c 100644 --- a/src/main/java/org/kohsuke/github/GitHubHttpUrlConnectionClient.java +++ b/src/main/java/org/kohsuke/github/GitHubHttpUrlConnectionClient.java @@ -110,7 +110,7 @@ static HttpURLConnection setupConnection(@Nonnull GitHubClient client, @Nonnull // if the authentication is needed but no credential is given, try it anyway (so that some calls // that do work with anonymous access in the reduced form should still work.) if (!request.headers().containsKey("Authorization")) { - String authorization = client.getEncodedAuthorization(); + String authorization = client.getEncodedAuthorization(request.url()); if (authorization != null) { connection.setRequestProperty("Authorization", client.getEncodedAuthorization()); } diff --git a/src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java b/src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java index 4dd615885d..cf373e9ca3 100644 --- a/src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java +++ b/src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java @@ -1,6 +1,7 @@ package org.kohsuke.github.authorization; import java.io.IOException; +import java.net.URL; /** * Provides a functional interface that returns a valid encodedAuthorization. This strategy allows for a provider that @@ -30,6 +31,7 @@ public interface AuthorizationProvider { * on any error that prevents the provider from getting a valid authorization */ String getEncodedAuthorization() throws IOException; + String getEncodedAuthorization(URL url) throws IOException; /** * A {@link AuthorizationProvider} that ensures that no credentials are returned @@ -37,6 +39,10 @@ public interface AuthorizationProvider { class AnonymousAuthorizationProvider implements AuthorizationProvider { @Override public String getEncodedAuthorization() throws IOException { + return getEncodedAuthorization(null); + } + @Override + public String getEncodedAuthorization(URL url) throws IOException { return null; } } diff --git a/src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java b/src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java index 41a113285a..dac9b43620 100644 --- a/src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java +++ b/src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java @@ -1,6 +1,7 @@ package org.kohsuke.github.authorization; import java.io.UnsupportedEncodingException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -92,10 +93,15 @@ public static AuthorizationProvider fromLoginAndPassword(String login, String pa } @Override - public String getEncodedAuthorization() { + public String getEncodedAuthorization(URL url) { return this.authorization; } + @Override + public String getEncodedAuthorization() { + return getEncodedAuthorization(null); + } + /** * An internal class representing all user-related credentials, which are credentials that have a login or should * query the user endpoint for the login matching this credential. diff --git a/src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java b/src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java index 020725fb41..851f7b347d 100644 --- a/src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java +++ b/src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java @@ -6,9 +6,15 @@ import org.kohsuke.github.GitHub; import java.io.IOException; +import java.net.URL; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nonnull; @@ -17,47 +23,75 @@ */ public class OrgAppInstallationAuthorizationProvider extends GitHub.DependentAuthorizationProvider { - private final String organizationName; + private static final Pattern pattern = Pattern.compile("/repos/(.*)/.*"); - private String latestToken; + private Map latestToken = new HashMap<>(); @Nonnull - private Instant validUntil = Instant.MIN; + private Map validUntil = new HashMap<>(); /** * Provides an AuthorizationProvider that performs automatic token refresh, based on an previously authenticated * github client. * - * @param organizationName - * The name of the organization where the application is installed * @param authorizationProvider * A authorization provider that returns a JWT token that can be used to refresh the App Installation * token from GitHub. */ @BetaApi @Deprecated - public OrgAppInstallationAuthorizationProvider(String organizationName, - AuthorizationProvider authorizationProvider) { + public OrgAppInstallationAuthorizationProvider(AuthorizationProvider authorizationProvider) { super(authorizationProvider); - this.organizationName = organizationName; } @Override - public String getEncodedAuthorization() throws IOException { + public String getEncodedAuthorization(URL url) throws IOException { synchronized (this) { - if (latestToken == null || Instant.now().isAfter(this.validUntil)) { - refreshToken(); + String org = getOrgFromURL(url); + if (latestToken.get(org) == null || this.validUntil.get(org) == null + || Instant.now().isAfter(this.validUntil.get(org))) { + refreshToken(url); + } + return String.format("token %s", latestToken.get(org)); + } + } + + @Override + public String getEncodedAuthorization() throws IOException { + return getEncodedAuthorization(null); + } + + /** + * Try to figure out what org is this url trying to access so we can use the correct App installation for that org. + * + * @param url + * @return the organization or "" if it cannot be computed + */ + private String getOrgFromURL(URL url) { + if (url != null) { + Matcher matcher = pattern.matcher(url.getPath()); + if (matcher.matches()) { + return matcher.group(1); } - return String.format("token %s", latestToken); } + return ""; } - private void refreshToken() throws IOException { - GitHub gitHub = this.gitHub(); - GHAppInstallation installationByOrganization = gitHub.getApp() - .getInstallationByOrganization(this.organizationName); - GHAppInstallationToken ghAppInstallationToken = installationByOrganization.createToken().create(); - this.validUntil = ghAppInstallationToken.getExpiresAt().toInstant().minus(Duration.ofMinutes(5)); - this.latestToken = Objects.requireNonNull(ghAppInstallationToken.getToken()); + private void refreshToken(URL url) throws IOException { + List installations = this.gitHub().getApp().listInstallations().asList(); + // take the first one if no one matches + GHAppInstallation installation = installations.get(0); + String org = getOrgFromURL(url); + for (GHAppInstallation ghAppInstallation : installations) { + if (org.equals(installation.getAccount().getLogin())) { + System.out.println( + String.format("Found installation for path %s: %s", url.getPath(), installation.getHtmlUrl())); + installation = ghAppInstallation; + break; + } + } + GHAppInstallationToken ghAppInstallationToken = installation.createToken().create(); + this.validUntil.put(org, ghAppInstallationToken.getExpiresAt().toInstant().minus(Duration.ofMinutes(5))); + this.latestToken.put(org, Objects.requireNonNull(ghAppInstallationToken.getToken())); } } diff --git a/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java b/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java index 1643b65d7a..5bf4b5dd4a 100644 --- a/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java +++ b/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java @@ -7,6 +7,7 @@ import java.io.File; import java.io.IOException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -60,6 +61,11 @@ public JWTTokenProvider(String applicationId, PrivateKey privateKey) { @Override public String getEncodedAuthorization() throws IOException { + return getEncodedAuthorization(null); + } + + @Override + public String getEncodedAuthorization(URL url) throws IOException { synchronized (this) { if (Instant.now().isAfter(validUntil)) { token = refreshJWT(); diff --git a/src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java b/src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java index 987e9a64a2..84d954f0f0 100644 --- a/src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java +++ b/src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java @@ -5,6 +5,7 @@ import org.kohsuke.github.authorization.OrgAppInstallationAuthorizationProvider; import java.io.IOException; +import java.net.URL; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; @@ -18,7 +19,6 @@ public OrgAppInstallationAuthorizationProviderTest() { @Test(expected = HttpException.class) public void invalidJWTTokenRaisesException() throws IOException { OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider( - "testOrganization", ImmutableAuthorizationProvider.fromJwtToken("myToken")); gitHub = getGitHubBuilder().withAuthorizationProvider(provider) .withEndpoint(mockGitHub.apiServer().baseUrl()) @@ -29,12 +29,13 @@ public void invalidJWTTokenRaisesException() throws IOException { @Test public void validJWTTokenAllowsOauthTokenRequest() throws IOException { - OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider("hub4j-test-org", + OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider( ImmutableAuthorizationProvider.fromJwtToken("bogus-valid-token")); gitHub = getGitHubBuilder().withAuthorizationProvider(provider) .withEndpoint(mockGitHub.apiServer().baseUrl()) .build(); - String encodedAuthorization = provider.getEncodedAuthorization(); + String encodedAuthorization = provider + .getEncodedAuthorization(new URL("https://api.github.com/repos/hub4j-test-org/github-api")); assertThat(encodedAuthorization, notNullValue()); assertThat(encodedAuthorization, equalTo("token v1.9a12d913f980a45a16ac9c3a9d34d9b7sa314cb6")); diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json index 80a6c2db36..c444d41d8a 100644 --- a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json +++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json @@ -1,4 +1,4 @@ -{ +[{ "id": 11575015, "account": { "login": "hub4j-test-org", @@ -40,4 +40,4 @@ "single_file_name": null, "suspended_by": null, "suspended_at": null -} \ No newline at end of file +}] \ No newline at end of file diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json index 54e1ea9d4d..45edb21712 100644 --- a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json +++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json @@ -2,7 +2,7 @@ "id": "9ffe1e34-1d0e-495a-abdc-86fdf1d15334", "name": "orgs_hub4j-test-org_installation", "request": { - "url": "/orgs/hub4j-test-org/installation", + "url": "/app/installations", "method": "GET", "headers": { "Accept": {