Skip to content

Commit

Permalink
Clean-up retrieval of Github authentication tokens
Browse files Browse the repository at this point in the history
The current implementation of the RestApplicationAuthenticationProvider
contains an instance-level Object Mapper but doesn't use it instead
constructing a new Object Mapper on each invocation and eagerly loads
all pages of App Installations even though the first page may contain a
token with access to the required repository. The implementation has
therefore been altered to use the constructed object mapper for all
operations and to work through the full contents of a page
of AppInstallations before moving on to the next page of installations.
  • Loading branch information
mc1arke committed Sep 16, 2023
1 parent 78d7725 commit 808604b
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 234 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2022 Michael Clarke
* Copyright (C) 2020-2023 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand Down Expand Up @@ -28,9 +28,14 @@
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.AppToken;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.InstallationRepositories;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.Repository;
import com.google.common.annotations.VisibleForTesting;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultJwtBuilder;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
Expand All @@ -41,16 +46,8 @@
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;

@ServerSide
@ComputeEngineSide
Expand All @@ -75,68 +72,70 @@ public RestApplicationAuthenticationProvider(Clock clock, LinkHeaderReader linkH
this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}

@VisibleForTesting
protected List<AppInstallation> getAppInstallations(ObjectMapper objectMapper, String apiUrl, String jwtToken) throws IOException {
@Override
public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String appId, String apiPrivateKey,
String projectPath) throws IOException {

Instant issued = clock.instant().minus(10, ChronoUnit.SECONDS);
Instant expiry = issued.plus(2, ChronoUnit.MINUTES);
String jwtToken = new DefaultJwtBuilder().setIssuedAt(Date.from(issued)).setExpiration(Date.from(expiry))
.claim("iss", appId).signWith(createPrivateKey(apiPrivateKey), SignatureAlgorithm.RS256).compact();

List<AppInstallation> appInstallations = new ArrayList<>();
Optional<RepositoryAuthenticationToken> repositoryAuthenticationToken = findTokenFromAppInstallationList(getV3Url(apiUrl) + "/app/installations", jwtToken, projectPath);

return repositoryAuthenticationToken.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT,
"No token could be found with access to the requested repository using the given application ID and key"));
}

private Optional<RepositoryAuthenticationToken> findTokenFromAppInstallationList(String apiUrl, String jwtToken, String projectPath) throws IOException {
URLConnection appConnection = urlProvider.createUrlConnection(apiUrl);
appConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER);
appConnection.setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken);

try (Reader reader = new InputStreamReader(appConnection.getInputStream())) {
appInstallations.addAll(Arrays.asList(objectMapper.readerFor(AppInstallation[].class).readValue(reader)));
AppInstallation[] appInstallations = objectMapper.readerFor(AppInstallation[].class).readValue(reader);
for (AppInstallation appInstallation : appInstallations) {
Optional<RepositoryAuthenticationToken> repositoryAuthenticationToken = findAppTokenFromAppInstallation(appInstallation, jwtToken, projectPath);

if (repositoryAuthenticationToken.isPresent()) {
return repositoryAuthenticationToken;
}
}
}

Optional<String> nextLink = linkHeaderReader.findNextLink(appConnection.getHeaderField("Link"));
if (nextLink.isPresent()) {
appInstallations.addAll(getAppInstallations(objectMapper, nextLink.get(), jwtToken));
if (nextLink.isEmpty()) {
return Optional.empty();
}

return appInstallations;
return findTokenFromAppInstallationList(nextLink.get(), jwtToken, projectPath);
}

@Override
public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String appId, String apiPrivateKey,
String projectPath) throws IOException {

Instant issued = clock.instant().minus(10, ChronoUnit.SECONDS);
Instant expiry = issued.plus(2, ChronoUnit.MINUTES);
String jwtToken = new DefaultJwtBuilder().setIssuedAt(Date.from(issued)).setExpiration(Date.from(expiry))
.claim("iss", appId).signWith(createPrivateKey(apiPrivateKey), SignatureAlgorithm.RS256).compact();

ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

List<AppInstallation> appInstallations = getAppInstallations(objectMapper, getV3Url(apiUrl) + "/app/installations", jwtToken);
private Optional<RepositoryAuthenticationToken> findAppTokenFromAppInstallation(AppInstallation installation, String jwtToken, String projectPath) throws IOException {
URLConnection accessTokenConnection = urlProvider.createUrlConnection(installation.getAccessTokensUrl());
((HttpURLConnection) accessTokenConnection).setRequestMethod("POST");
accessTokenConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER);
accessTokenConnection
.setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken);

for (AppInstallation installation : appInstallations) {
URLConnection accessTokenConnection = urlProvider.createUrlConnection(installation.getAccessTokensUrl());
((HttpURLConnection) accessTokenConnection).setRequestMethod("POST");
accessTokenConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER);
accessTokenConnection
.setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken);
try (Reader reader = new InputStreamReader(accessTokenConnection.getInputStream())) {
AppToken appToken = objectMapper.readerFor(AppToken.class).readValue(reader);

String targetUrl = installation.getRepositoriesUrl();

try (Reader reader = new InputStreamReader(accessTokenConnection.getInputStream())) {
AppToken appToken = objectMapper.readerFor(AppToken.class).readValue(reader);

String targetUrl = installation.getRepositoriesUrl();

Optional<RepositoryAuthenticationToken> potentialRepositoryAuthenticationToken = findRepositoryAuthenticationToken(appToken, targetUrl, projectPath, objectMapper);

if (potentialRepositoryAuthenticationToken.isPresent()) {
return potentialRepositoryAuthenticationToken.get();
}
Optional<RepositoryAuthenticationToken> potentialRepositoryAuthenticationToken = findRepositoryAuthenticationToken(appToken, targetUrl, projectPath);

if (potentialRepositoryAuthenticationToken.isPresent()) {
return potentialRepositoryAuthenticationToken;
}

}

throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT,
"No token could be found with access to the requested repository using the given application ID and key");
return Optional.empty();
}

private Optional<RepositoryAuthenticationToken> findRepositoryAuthenticationToken(AppToken appToken, String targetUrl,
String projectPath, ObjectMapper objectMapper) throws IOException {
String projectPath) throws IOException {
URLConnection installationRepositoriesConnection = urlProvider.createUrlConnection(targetUrl);
((HttpURLConnection) installationRepositoriesConnection).setRequestMethod("GET");
installationRepositoriesConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER);
Expand All @@ -161,7 +160,7 @@ private Optional<RepositoryAuthenticationToken> findRepositoryAuthenticationToke
return Optional.empty();
}

return findRepositoryAuthenticationToken(appToken, nextLink.get(), projectPath, objectMapper);
return findRepositoryAuthenticationToken(appToken, nextLink.get(), projectPath);
}

private static String getV3Url(String apiUrl) {
Expand Down
Loading

0 comments on commit 808604b

Please sign in to comment.