Skip to content

Commit

Permalink
#508: Support Pagination over AppInstallation
Browse files Browse the repository at this point in the history
When more than a single page worth of app installation is present in the
Github API, the plugin does not currently fetch beyond the initial page,
and therefore doesn't find the access tokens for the required
installation. By loading all pages of installations before searching for
the current application we ensure that we don't ignore any relevant
installation.
  • Loading branch information
bstick12 authored and mc1arke committed Dec 31, 2022
1 parent 2a0d191 commit 4e73292
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.AppToken;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.InstallationRepositories;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.Repository;
import com.google.common.annotations.VisibleForTesting;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultJwtBuilder;
import org.bouncycastle.openssl.PEMKeyPair;
Expand All @@ -43,7 +44,10 @@
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;

public class RestApplicationAuthenticationProvider implements GithubApplicationAuthenticationProvider {
Expand All @@ -69,6 +73,27 @@ public RestApplicationAuthenticationProvider(Clock clock, LinkHeaderReader linkH
this.linkHeaderReader = linkHeaderReader;
}

@VisibleForTesting
protected List<AppInstallation> getAppInstallations(ObjectMapper objectMapper, String apiUrl, String jwtToken) throws IOException {

List<AppInstallation> appInstallations = new ArrayList<>();

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

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

return appInstallations;
}

@Override
public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String appId, String apiPrivateKey,
String projectPath) throws IOException {
Expand All @@ -80,15 +105,7 @@ public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String

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

URLConnection appConnection = urlProvider.createUrlConnection(getV3Url(apiUrl) + "/app/installations");
appConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER);
appConnection.setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken);

AppInstallation[] appInstallations;
try (Reader reader = new InputStreamReader(appConnection.getInputStream())) {
appInstallations = objectMapper.readerFor(AppInstallation[].class).readValue(reader);
}

List<AppInstallation> appInstallations = getAppInstallations(objectMapper, getV3Url(apiUrl) + "/app/installations", jwtToken);

for (AppInstallation installation : appInstallations) {
URLConnection accessTokenConnection = urlProvider.createUrlConnection(installation.getAccessTokensUrl());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Michael Clarke
* Copyright (C) 2020-2022 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 All @@ -18,8 +18,11 @@
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.LinkHeaderReader;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.RepositoryAuthenticationToken;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.AppInstallation;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
Expand All @@ -34,6 +37,7 @@
import java.time.Instant;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -45,6 +49,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class RestApplicationAuthenticationProviderTest {

Expand Down Expand Up @@ -140,6 +145,32 @@ private void testTokenForUrl(String apiUrl, String fullUrl) throws IOException {
requestPropertyArgumentCaptor.getAllValues());
}

@Test
public void testAppInstallationsPagination() throws IOException {

UrlConnectionProvider urlProvider = mock(UrlConnectionProvider.class);
Clock clock = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneId.of("UTC"));

String apiUrl = "apiUrl";

int pages=4;

for(int i=1; i<=pages;i++) {
HttpURLConnection installationsUrlConnection = mock(HttpURLConnection.class);
doReturn(installationsUrlConnection).when(urlProvider).createUrlConnection(eq(apiUrl + "/app/installations?page=" + i));
when(installationsUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(
"[{\"repositories_url\": \"repositories_url\", \"access_tokens_url\": \"tokens_url\"}]"
.getBytes(StandardCharsets.UTF_8)));
when(installationsUrlConnection.getHeaderField("Link")).thenReturn(i == pages ? null: apiUrl + "/app/installations?page=" + (i+1) );
}

RestApplicationAuthenticationProvider testCase = new RestApplicationAuthenticationProvider(clock, Optional::ofNullable, urlProvider);
ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
List<AppInstallation> token = testCase.getAppInstallations(objectMapper, apiUrl + "/app/installations?page=1", "token");
assertThat(token).hasSize(pages);

}

@Test
public void testTokenRetrievedPaginatedHappyPath() throws IOException {
UrlConnectionProvider urlProvider = mock(UrlConnectionProvider.class);
Expand Down

0 comments on commit 4e73292

Please sign in to comment.