Skip to content

Commit 5058d65

Browse files
authored
Add repository-scoped token support to GithubAppClient (#258)
Adds ability to create installation access tokens scoped to specific repositories instead of all repositories in an installation. Changes: - Add AccessTokenRequest model with repositories and repository_ids fields - Add overloaded getAccessToken(installationId, AccessTokenRequest) method - Maintain backward compatibility with existing getAccessToken(installationId) - Add comprehensive test coverage for scoped and unscoped tokens
1 parent 6edf7d5 commit 5058d65

File tree

4 files changed

+142
-0
lines changed

4 files changed

+142
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*-
2+
* -\-\-
3+
* github-api
4+
* --
5+
* Copyright (C) 2016 - 2020 Spotify AB
6+
* --
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* -/-/-
19+
*/
20+
package com.spotify.github.v3.apps.requests;
21+
22+
import com.fasterxml.jackson.annotation.JsonInclude;
23+
import com.fasterxml.jackson.annotation.JsonProperty;
24+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
25+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
26+
import com.spotify.github.GithubStyle;
27+
import java.util.List;
28+
import java.util.Optional;
29+
import org.immutables.value.Value;
30+
31+
/** Request to create an installation access token with repository scoping. */
32+
@Value.Immutable
33+
@GithubStyle
34+
@JsonSerialize(as = ImmutableAccessTokenRequest.class)
35+
@JsonDeserialize(as = ImmutableAccessTokenRequest.class)
36+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
37+
public interface AccessTokenRequest {
38+
39+
/**
40+
* List of repository names that the token should be scoped to.
41+
*
42+
* @return list of repository names
43+
*/
44+
Optional<List<String>> repositories();
45+
46+
/**
47+
* List of repository IDs that the token should be scoped to.
48+
*
49+
* @return list of repository IDs
50+
*/
51+
@JsonProperty("repository_ids")
52+
Optional<List<Integer>> repositoryIds();
53+
}

src/main/java/com/spotify/github/v3/clients/GithubAppClient.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.fasterxml.jackson.core.type.TypeReference;
2424
import com.google.common.collect.ImmutableMap;
2525
import com.spotify.github.v3.apps.InstallationRepositoriesResponse;
26+
import com.spotify.github.v3.apps.requests.AccessTokenRequest;
2627
import com.spotify.github.v3.checks.AccessToken;
2728
import com.spotify.github.v3.checks.App;
2829
import com.spotify.github.v3.checks.Installation;
@@ -155,12 +156,28 @@ public CompletableFuture<Installation> getUserInstallation() {
155156
* Authenticates as an installation
156157
*
157158
* @return an Installation Token
159+
* @see #getAccessToken(Integer, AccessTokenRequest) for repository-scoped tokens
158160
*/
159161
public CompletableFuture<AccessToken> getAccessToken(final Integer installationId) {
160162
final String path = String.format(GET_ACCESS_TOKEN_URL, installationId);
161163
return github.post(path, "", AccessToken.class, extraHeaders);
162164
}
163165

166+
/**
167+
* Authenticates as an installation with repository scoping.
168+
*
169+
* @param installationId the installation ID
170+
* @param request the access token request with optional repository scoping
171+
* @return an Installation Token
172+
* @see "https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app"
173+
*/
174+
public CompletableFuture<AccessToken> getAccessToken(
175+
final Integer installationId,
176+
final AccessTokenRequest request) {
177+
final String path = String.format(GET_ACCESS_TOKEN_URL, installationId);
178+
return github.post(path, github.json().toJsonUnchecked(request), AccessToken.class, extraHeaders);
179+
}
180+
164181
/**
165182
* Lists the repositories that an app installation can access.
166183
*

src/test/java/com/spotify/github/v3/clients/GithubAppClientTest.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@
2424
import static java.nio.charset.Charset.defaultCharset;
2525
import static org.hamcrest.MatcherAssert.assertThat;
2626
import static org.hamcrest.Matchers.containsInAnyOrder;
27+
import static org.hamcrest.Matchers.containsString;
2728
import static org.hamcrest.core.Is.is;
2829

2930
import com.fasterxml.jackson.databind.ObjectMapper;
3031
import com.google.common.io.Resources;
3132
import com.spotify.github.FixtureHelper;
3233
import com.spotify.github.v3.apps.InstallationRepositoriesResponse;
34+
import com.spotify.github.v3.apps.requests.AccessTokenRequest;
35+
import com.spotify.github.v3.apps.requests.ImmutableAccessTokenRequest;
36+
import com.spotify.github.v3.checks.AccessToken;
3337
import com.spotify.github.v3.checks.App;
3438
import com.spotify.github.v3.checks.Installation;
3539
import java.io.File;
@@ -182,4 +186,68 @@ public void getAuthenticatedApp() throws Exception {
182186
recordedRequest.getHeaders().values("Authorization").get(0).startsWith("Bearer "),
183187
is(true));
184188
}
189+
190+
@Test
191+
public void getAccessTokenWithoutScoping() throws Exception {
192+
mockServer.enqueue(
193+
new MockResponse()
194+
.setResponseCode(201)
195+
.setBody(FixtureHelper.loadFixture("githubapp/access-token.json")));
196+
197+
AccessToken token = client.getAccessToken(1234).join();
198+
199+
assertThat(token.token(), is("ghs_16C7e42F292c6912E7710c838347Ae178B4a"));
200+
201+
RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
202+
assertThat(recordedRequest.getMethod(), is("POST"));
203+
assertThat(
204+
recordedRequest.getRequestUrl().encodedPath(),
205+
is("/app/installations/1234/access_tokens"));
206+
assertThat(recordedRequest.getBody().readUtf8(), is(""));
207+
}
208+
209+
@Test
210+
public void getAccessTokenWithRepositoryScoping() throws Exception {
211+
mockServer.enqueue(
212+
new MockResponse()
213+
.setResponseCode(201)
214+
.setBody(FixtureHelper.loadFixture("githubapp/access-token.json")));
215+
216+
AccessTokenRequest request = ImmutableAccessTokenRequest.builder()
217+
.repositories(List.of("Hello-World"))
218+
.repositoryIds(List.of(1))
219+
.build();
220+
221+
AccessToken token = client.getAccessToken(1234, request).join();
222+
223+
assertThat(token.token(), is("ghs_16C7e42F292c6912E7710c838347Ae178B4a"));
224+
225+
RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
226+
assertThat(recordedRequest.getMethod(), is("POST"));
227+
assertThat(
228+
recordedRequest.getRequestUrl().encodedPath(),
229+
is("/app/installations/1234/access_tokens"));
230+
231+
String requestBody = recordedRequest.getBody().readUtf8();
232+
assertThat(requestBody, containsString("\"repositories\":[\"Hello-World\"]"));
233+
assertThat(requestBody, containsString("\"repository_ids\":[1]"));
234+
}
235+
236+
@Test
237+
public void getAccessTokenWithEmptyRequest() throws Exception {
238+
mockServer.enqueue(
239+
new MockResponse()
240+
.setResponseCode(201)
241+
.setBody(FixtureHelper.loadFixture("githubapp/access-token.json")));
242+
243+
AccessTokenRequest emptyRequest = ImmutableAccessTokenRequest.builder().build();
244+
245+
AccessToken token = client.getAccessToken(1234, emptyRequest).join();
246+
247+
assertThat(token.token(), is("ghs_16C7e42F292c6912E7710c838347Ae178B4a"));
248+
249+
RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
250+
String requestBody = recordedRequest.getBody().readUtf8();
251+
assertThat(requestBody, is("{}"));
252+
}
185253
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"token": "ghs_16C7e42F292c6912E7710c838347Ae178B4a",
3+
"expires_at": "2024-08-10T05:54:58Z"
4+
}

0 commit comments

Comments
 (0)