Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GitHub#getInstallation to access endpoints related to the authenticated installation #1523

Merged
merged 1 commit into from Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/main/java/org/kohsuke/github/GHAppInstallation.java
Expand Up @@ -117,7 +117,15 @@ public String getRepositoriesUrl() {
* List repositories that this app installation can access.
*
* @return the paged iterable
* @deprecated This method cannot work on a {@link GHAppInstallation} retrieved from
* {@link GHApp#listInstallations()} (for example), except when resorting to unsupported hacks involving
* {@link GHAppInstallation#setRoot(GitHub)} to switch from an application client to an installation
* client. This method will be removed. You should instead use an installation client (with an
* installation token, not a JWT), retrieve a {@link GHAuthenticatedAppInstallation} from
* {@link GitHub#getInstallation()}, then call
* {@link GHAuthenticatedAppInstallation#listRepositories()}.
*/
@Deprecated
@Preview(MACHINE_MAN)
public PagedSearchIterable<GHRepository> listRepositories() {
GitHubRequest request;
Expand Down
@@ -0,0 +1,40 @@
package org.kohsuke.github;

import javax.annotation.Nonnull;

import static org.kohsuke.github.internal.Previews.MACHINE_MAN;

/**
* The Github App Installation corresponding to the installation token used in a client.
*
* @see GitHub#getInstallation() GitHub#getAuthenticatedAppInstallation()
*/
public class GHAuthenticatedAppInstallation extends GitHubInteractiveObject {
protected GHAuthenticatedAppInstallation(@Nonnull GitHub root) {
super(root);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible with the current codebase to detect whether the current GitHub client is authenticated as an installation when this object is instantiated on the client side? If so, maybe it would be nice to throw an exception with a helpful error message.

That said, do you know what the error is from GitHub when you call listRepositories with incorrect authentication? If it is already pretty clear, then there's probably no reason to do anything extra here.

Copy link
Contributor Author

@yrodiere yrodiere Sep 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible with the current codebase to detect whether the current GitHub client is authenticated as an installation when this object is instantiated on the client side?

Judging from how GitHub#getApp() fails with an obscure HTTP authentication error when we try to use it with an installation token, I would say there is no such feature yet. That would probably be a nice improvement in another PR that would be applied across the board to all relevant methods, not just this one? I think the hardest part would probably be testing...

That said, do you know what the error is from GitHub when you call listRepositories with incorrect authentication? If it is already pretty clear, then there's probably no reason to do anything extra here.

We get this, so I suppose it depends what you call "pretty clear":

org.kohsuke.github.HttpException: {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}
        at org.kohsuke.github.GitHubConnectorResponseErrorHandler$1.onError(GitHubConnectorResponseErrorHandler.java:56)
        at org.kohsuke.github.GitHubClient.detectKnownErrors(GitHubClient.java:424)
        at org.kohsuke.github.GitHubClient.sendRequest(GitHubClient.java:386)
        at org.kohsuke.github.GitHubPageIterator.fetch(GitHubPageIterator.java:142)
        at org.kohsuke.github.GitHubPageIterator.hasNext(GitHubPageIterator.java:89)
        at org.kohsuke.github.PagedSearchIterable$1.hasNext(PagedSearchIterable.java:86)
        at org.kohsuke.github.PagedIterator.fetch(PagedIterator.java:106)
        at org.kohsuke.github.PagedIterator.nextPageArray(PagedIterator.java:134)
        at org.kohsuke.github.PagedIterable.toArray(PagedIterable.java:78)
        at org.kohsuke.github.PagedIterable.toArray(PagedIterable.java:106)
        at org.kohsuke.github.PagedIterable.toList(PagedIterable.java:118)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}

Well that is true, but the message is a bit opaque 😄

Either way this is something that could easily be improved in the future if users are actually getting confused by it, so there's probably no reason to complicate this PR.

}

/**
* List repositories that this app installation can access.
*
* @return the paged iterable
*/
@Preview(MACHINE_MAN)
public PagedSearchIterable<GHRepository> listRepositories() {
GitHubRequest request;

request = root().createRequest().withPreview(MACHINE_MAN).withUrlPath("/installation/repositories").build();

return new PagedSearchIterable<>(root(), request, GHAuthenticatedAppInstallationRepositoryResult.class);
}

private static class GHAuthenticatedAppInstallationRepositoryResult extends SearchResult<GHRepository> {
private GHRepository[] repositories;

@Override
GHRepository[] getItems(GitHub root) {
return repositories;
}
}

}
16 changes: 16 additions & 0 deletions src/main/java/org/kohsuke/github/GitHub.java
Expand Up @@ -1160,6 +1160,22 @@ public GHApp getApp() throws IOException {
return createRequest().withPreview(MACHINE_MAN).withUrlPath("/app").fetch(GHApp.class);
}

/**
* Returns the GitHub App Installation associated with the authentication credentials used.
* <p>
* You must use an installation token to access this endpoint; otherwise consider {@link #getApp()} and its various
* ways of retrieving installations.
*
* @return the app
* @throws IOException
* the io exception
* @see <a href="https://docs.github.com/en/rest/apps/installations">GitHub App installations</a>
*/
@Preview(MACHINE_MAN)
public GHAuthenticatedAppInstallation getInstallation() throws IOException {
bitwiseman marked this conversation as resolved.
Show resolved Hide resolved
return new GHAuthenticatedAppInstallation(this);
}

/**
* Ensures that the credential is valid.
*
Expand Down
@@ -0,0 +1,32 @@
package org.kohsuke.github;

import org.junit.Test;
import org.kohsuke.github.authorization.OrgAppInstallationAuthorizationProvider;

import java.io.IOException;
import java.util.List;

import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.equalTo;

public class GHAuthenticatedAppInstallationTest extends AbstractGHAppInstallationTest {

@Override
protected GitHubBuilder getGitHubBuilder() {
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider("hub4j-test-org",
jwtProvider1);
return super.getGitHubBuilder().withAuthorizationProvider(provider);
}

@Test
public void testListRepositoriesTwoRepos() throws IOException {
GHAuthenticatedAppInstallation appInstallation = gitHub.getInstallation();

List<GHRepository> repositories = appInstallation.listRepositories().toList();

assertThat(repositories.size(), equalTo(2));
assertThat(repositories.stream().map(GHRepository::getName).toArray(),
arrayContainingInAnyOrder("empty", "test-readme"));
}

}
@@ -0,0 +1,37 @@
{
"id": 82994,
"slug": "ghapi-test-app-1",
"node_id": "MDM6QXBwODI5OTQ=",
"owner": {
"login": "hub4j-test-org",
"id": 7544739,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjc1NDQ3Mzk=",
"avatar_url": "https://avatars.githubusercontent.com/u/7544739?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/hub4j-test-org",
"html_url": "https://github.com/hub4j-test-org",
"followers_url": "https://api.github.com/users/hub4j-test-org/followers",
"following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
"gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
"starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
"organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
"repos_url": "https://api.github.com/users/hub4j-test-org/repos",
"events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
"received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
"type": "Organization",
"site_admin": false
},
"name": "GHApi Test app 1",
"description": "",
"external_url": "http://localhost",
"html_url": "https://github.com/apps/ghapi-test-app-1",
"created_at": "2020-09-30T13:40:56Z",
"updated_at": "2020-09-30T13:40:56Z",
"permissions": {
"contents": "read",
"metadata": "read"
},
"events": [],
"installations_count": 1
}
@@ -0,0 +1,218 @@
{
"total_count": 2,
"repository_selection": "selected",
"repositories": [
{
"id": 60391080,
"node_id": "MDEwOlJlcG9zaXRvcnk2MDM5MTA4MA==",
"name": "empty",
"full_name": "hub4j-test-org/empty",
"private": false,
"owner": {
"login": "hub4j-test-org",
"id": 7544739,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjc1NDQ3Mzk=",
"avatar_url": "https://avatars.githubusercontent.com/u/7544739?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/hub4j-test-org",
"html_url": "https://github.com/hub4j-test-org",
"followers_url": "https://api.github.com/users/hub4j-test-org/followers",
"following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
"gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
"starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
"organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
"repos_url": "https://api.github.com/users/hub4j-test-org/repos",
"events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
"received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/hub4j-test-org/empty",
"description": "Repository that has no contributor",
"fork": false,
"url": "https://api.github.com/repos/hub4j-test-org/empty",
"forks_url": "https://api.github.com/repos/hub4j-test-org/empty/forks",
"keys_url": "https://api.github.com/repos/hub4j-test-org/empty/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/hub4j-test-org/empty/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/hub4j-test-org/empty/teams",
"hooks_url": "https://api.github.com/repos/hub4j-test-org/empty/hooks",
"issue_events_url": "https://api.github.com/repos/hub4j-test-org/empty/issues/events{/number}",
"events_url": "https://api.github.com/repos/hub4j-test-org/empty/events",
"assignees_url": "https://api.github.com/repos/hub4j-test-org/empty/assignees{/user}",
"branches_url": "https://api.github.com/repos/hub4j-test-org/empty/branches{/branch}",
"tags_url": "https://api.github.com/repos/hub4j-test-org/empty/tags",
"blobs_url": "https://api.github.com/repos/hub4j-test-org/empty/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/hub4j-test-org/empty/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/hub4j-test-org/empty/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/hub4j-test-org/empty/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/hub4j-test-org/empty/statuses/{sha}",
"languages_url": "https://api.github.com/repos/hub4j-test-org/empty/languages",
"stargazers_url": "https://api.github.com/repos/hub4j-test-org/empty/stargazers",
"contributors_url": "https://api.github.com/repos/hub4j-test-org/empty/contributors",
"subscribers_url": "https://api.github.com/repos/hub4j-test-org/empty/subscribers",
"subscription_url": "https://api.github.com/repos/hub4j-test-org/empty/subscription",
"commits_url": "https://api.github.com/repos/hub4j-test-org/empty/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/hub4j-test-org/empty/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/hub4j-test-org/empty/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/hub4j-test-org/empty/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/hub4j-test-org/empty/contents/{+path}",
"compare_url": "https://api.github.com/repos/hub4j-test-org/empty/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/hub4j-test-org/empty/merges",
"archive_url": "https://api.github.com/repos/hub4j-test-org/empty/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/hub4j-test-org/empty/downloads",
"issues_url": "https://api.github.com/repos/hub4j-test-org/empty/issues{/number}",
"pulls_url": "https://api.github.com/repos/hub4j-test-org/empty/pulls{/number}",
"milestones_url": "https://api.github.com/repos/hub4j-test-org/empty/milestones{/number}",
"notifications_url": "https://api.github.com/repos/hub4j-test-org/empty/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/hub4j-test-org/empty/labels{/name}",
"releases_url": "https://api.github.com/repos/hub4j-test-org/empty/releases{/id}",
"deployments_url": "https://api.github.com/repos/hub4j-test-org/empty/deployments",
"created_at": "2016-06-04T03:22:22Z",
"updated_at": "2016-06-04T03:22:22Z",
"pushed_at": "2016-06-04T03:22:23Z",
"git_url": "git://github.com/hub4j-test-org/empty.git",
"ssh_url": "git@github.com:hub4j-test-org/empty.git",
"clone_url": "https://github.com/hub4j-test-org/empty.git",
"svn_url": "https://github.com/hub4j-test-org/empty",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": null,
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"topics": [],
"visibility": "public",
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "master",
"permissions": {
"admin": false,
"maintain": false,
"push": false,
"triage": false,
"pull": false
}
},
{
"id": 30829547,
"node_id": "MDEwOlJlcG9zaXRvcnkzMDgyOTU0Nw==",
"name": "test-readme",
"full_name": "hub4j-test-org/test-readme",
"private": false,
"owner": {
"login": "hub4j-test-org",
"id": 7544739,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjc1NDQ3Mzk=",
"avatar_url": "https://avatars.githubusercontent.com/u/7544739?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/hub4j-test-org",
"html_url": "https://github.com/hub4j-test-org",
"followers_url": "https://api.github.com/users/hub4j-test-org/followers",
"following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
"gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
"starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
"organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
"repos_url": "https://api.github.com/users/hub4j-test-org/repos",
"events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
"received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/hub4j-test-org/test-readme",
"description": "Checks the readme of content",
"fork": false,
"url": "https://api.github.com/repos/hub4j-test-org/test-readme",
"forks_url": "https://api.github.com/repos/hub4j-test-org/test-readme/forks",
"keys_url": "https://api.github.com/repos/hub4j-test-org/test-readme/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/hub4j-test-org/test-readme/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/hub4j-test-org/test-readme/teams",
"hooks_url": "https://api.github.com/repos/hub4j-test-org/test-readme/hooks",
"issue_events_url": "https://api.github.com/repos/hub4j-test-org/test-readme/issues/events{/number}",
"events_url": "https://api.github.com/repos/hub4j-test-org/test-readme/events",
"assignees_url": "https://api.github.com/repos/hub4j-test-org/test-readme/assignees{/user}",
"branches_url": "https://api.github.com/repos/hub4j-test-org/test-readme/branches{/branch}",
"tags_url": "https://api.github.com/repos/hub4j-test-org/test-readme/tags",
"blobs_url": "https://api.github.com/repos/hub4j-test-org/test-readme/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/hub4j-test-org/test-readme/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/hub4j-test-org/test-readme/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/hub4j-test-org/test-readme/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/hub4j-test-org/test-readme/statuses/{sha}",
"languages_url": "https://api.github.com/repos/hub4j-test-org/test-readme/languages",
"stargazers_url": "https://api.github.com/repos/hub4j-test-org/test-readme/stargazers",
"contributors_url": "https://api.github.com/repos/hub4j-test-org/test-readme/contributors",
"subscribers_url": "https://api.github.com/repos/hub4j-test-org/test-readme/subscribers",
"subscription_url": "https://api.github.com/repos/hub4j-test-org/test-readme/subscription",
"commits_url": "https://api.github.com/repos/hub4j-test-org/test-readme/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/hub4j-test-org/test-readme/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/hub4j-test-org/test-readme/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/hub4j-test-org/test-readme/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/hub4j-test-org/test-readme/contents/{+path}",
"compare_url": "https://api.github.com/repos/hub4j-test-org/test-readme/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/hub4j-test-org/test-readme/merges",
"archive_url": "https://api.github.com/repos/hub4j-test-org/test-readme/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/hub4j-test-org/test-readme/downloads",
"issues_url": "https://api.github.com/repos/hub4j-test-org/test-readme/issues{/number}",
"pulls_url": "https://api.github.com/repos/hub4j-test-org/test-readme/pulls{/number}",
"milestones_url": "https://api.github.com/repos/hub4j-test-org/test-readme/milestones{/number}",
"notifications_url": "https://api.github.com/repos/hub4j-test-org/test-readme/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/hub4j-test-org/test-readme/labels{/name}",
"releases_url": "https://api.github.com/repos/hub4j-test-org/test-readme/releases{/id}",
"deployments_url": "https://api.github.com/repos/hub4j-test-org/test-readme/deployments",
"created_at": "2015-02-15T14:24:54Z",
"updated_at": "2021-04-19T20:17:38Z",
"pushed_at": "2021-04-19T20:17:36Z",
"git_url": "git://github.com/hub4j-test-org/test-readme.git",
"ssh_url": "git@github.com:hub4j-test-org/test-readme.git",
"clone_url": "https://github.com/hub4j-test-org/test-readme.git",
"svn_url": "https://github.com/hub4j-test-org/test-readme",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": null,
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"topics": [],
"visibility": "public",
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "main",
"permissions": {
"admin": false,
"maintain": false,
"push": false,
"triage": false,
"pull": false
}
}
]
}