-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Add GitHub App service #12072
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 App service #12072
Changes from 3 commits
2f045d6
7769ed4
6045d3c
1f7d5e1
3883df0
1e3aff6
2ee1b18
db4fbce
586d133
8b95371
b8a8fd6
acd6f63
0518dab
a562f75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
GitHub App | ||
========== | ||
|
||
Our GitHub App integration consists of a GitHub App (one for each platform, readthedocs.org and readthedocs.com), | ||
which can be installed on a user's account or organization. | ||
|
||
After installing the GitHub App, users can grant acccess to all repositories or select specific repositories, | ||
this allows Read the Docs to access the repositories and perform actions on them, such as reporting build statuses, | ||
and subscribe to events like push and pull request events. | ||
|
||
Unlike our other Git integrations, this doesn't rely on user's OAuth2 tokens to interact with the repositories, instead it makes use of the installation. | ||
As long as the installation is active and has access to the repositories, Read the Docs can perform actions on them. | ||
|
||
You can read more about GitHub Apps in `GitHub's documentation <https://docs.github.com/en/apps/overview>`__. | ||
|
||
Modeling | ||
-------- | ||
|
||
Since we no longer make use of user's OAuth2 tokens, we need to keep track of all the installations of our GitHub App in the database (``GitHubAppInstallation`` model). | ||
Each remote repository has a foreign key to the installation that has access to it, | ||
so we can use that installation to perform actions on the repository. | ||
|
||
Keeping data in sync | ||
-------------------- | ||
|
||
Once a GitHub app is installed, GitHub will send webhook events to our application (``readthedocs.oauth.views``) related to the installation | ||
(even when the installation is uninstalled or revoked), repositories, and organizations. | ||
This allows us to always have the ``RemoteRepository`` and ``RemoteOrganization`` models and its permissions in sync with GitHub. | ||
|
||
The only use case for using the user's OAuth2 token is to get all the installations the user has access to, | ||
this helps us to keep permissions in sync with GitHub. | ||
humitos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Security | ||
-------- | ||
|
||
- All webhooks are signed with a secret, which we verify before processing each event. | ||
- Since we make use of the installation to perform actions on the repositories instead of the user's OAuth2 token, | ||
we make sure that only users with admin permissions on the repository can link the repository to a Read the Docs project. | ||
- Once we lose access to a repository (e.g. the installation is uninstalled or revoked, or the project was deselected from the installation), | ||
we remove the remote repository from the database, as we don't want to keep the relation bettween the project and the repository. | ||
This is to prevent connecting the repository to the project again without the user's consent if they grant access to the repository again. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -270,17 +270,19 @@ Configuring GitHub App | |
~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
- Create a new GitHub app from https://github.com/settings/apps/new. | ||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- Callback URL should be ``http://dev.readthedocs.org/accounts/githubapp/login/callback/``. | ||
- Callback URL should be ``http://devthedocs.org/accounts/githubapp/login/callback/``. | ||
- Keep marked "Expire user authorization tokens" | ||
- Activate the webhook, and set the URL to one provided by a service like `Webhook.site <https://docs.webhook.site/cli.html>`__ to forward all incoming webhooks to your local development instance. | ||
You should forward all events to ``http://dev.readthedocs.org/webhook/githubapp/``. | ||
You should forward all events to ``http://devthedocs.org/webhook/githubapp/``. | ||
- In permissions, select the following: | ||
|
||
- Repository permissions: Commit statuses (read and write, so we can create commit statuses), | ||
Contents (read only, so we can clone repos with a token), | ||
Metadata (read only, so we read the repo collaborators), | ||
Pull requests (read and write, so we can post a comment on PRs in the future). | ||
- Organization permissions: Members (read only so we can read the organization members). | ||
- Account permissions: Email addresses (read only, so allauth can fetch all verified emails). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be preferred if these permissions are on an opt-in basis. Many projects may want to start small, and i've seen plenty of them that are skeptical about every little permission. So instead it could be like The minimum permissions necessary for this app are
- Repository permissions:
Contents (read only, so we can clone the repo contents)
Additionally you may grand additional permissions depending on the features you wish to have
- Repository permissions:
Commit statuses (read and write, so we can create commit statuses) It does require the app to read the granted permissions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, don't think GH allows for this, the permissions are defined by the app, installations can't choose a subset of permissions. What can happen is that the app requires more permissions and the user can accept or reject those new permissions, but all new installations will always require granting all permissions designated by the app. And even if GH allows for this, not sure that we will support that case, as it requires considering more cases to support, and also support time, as some users don't know why a feature isn't working. |
||
|
||
- Subscribe to the following events: Installation target, Member, Organization, Membership, Pull request, Push, and Repository. | ||
- Copy the "Client ID" and "Client Secret" and set them as :ref:`environment variables <settings:Allauth secrets>`. | ||
- Generate a webhook secret and a private key from the GitHub App settings, | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -108,8 +108,10 @@ def delete_repositories(self, repository_ids: list[int] | None = None): | |||||||||||||
:param repository_ids: List of repository ids (remote ID) to delete. | ||||||||||||||
If None, all repositories will be considered for deletion. | ||||||||||||||
""" | ||||||||||||||
# repository_ids is optionaal (None, which means all repositories), | ||||||||||||||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
# but if it's an empty list, we don't want to delete anything. | ||||||||||||||
if repository_ids is not None and not repository_ids: | ||||||||||||||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
log.info("No repositories to delete") | ||||||||||||||
log.info("No remote repositories to delete") | ||||||||||||||
return | ||||||||||||||
|
||||||||||||||
remote_organizations = RemoteOrganization.objects.filter( | ||||||||||||||
|
@@ -128,7 +130,7 @@ def delete_repositories(self, repository_ids: list[int] | None = None): | |||||||||||||
|
||||||||||||||
count, deleted = remote_repositories.delete() | ||||||||||||||
log.info( | ||||||||||||||
"Deleted repositories projects", | ||||||||||||||
"Deleted remote repositories", | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The concept of orphaned projects doesn't exist in this case, only for organizations (like we still have access to the installation of an organization, but without access to nay of its projects) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to put something not as scary as it is now. Why are we deleting these remote repositories? They are outdated/old/invalid/the user lost access to them/etc? I would mention that. Example,
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We delete them when our app no longer has access to them, is not that they are outdated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏼 -- It's important to note that in the log.
Suggested change
|
||||||||||||||
count=count, | ||||||||||||||
deleted=deleted, | ||||||||||||||
installation_id=self.installation_id, | ||||||||||||||
humitos marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
@@ -139,7 +141,7 @@ def delete_repositories(self, repository_ids: list[int] | None = None): | |||||||||||||
repositories=None, | ||||||||||||||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
).delete() | ||||||||||||||
log.info( | ||||||||||||||
"Deleted orphaned organizations", | ||||||||||||||
"Deleted orphaned remote organizations", | ||||||||||||||
count=count, | ||||||||||||||
deleted=deleted, | ||||||||||||||
installation_id=self.installation_id, | ||||||||||||||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
@@ -152,7 +154,7 @@ def delete_organization(self, organization_id: int): | |||||||||||||
vcs_provider=GITHUB_APP, | ||||||||||||||
).delete() | ||||||||||||||
log.info( | ||||||||||||||
"Deleted organization", | ||||||||||||||
"Deleted remote organization", | ||||||||||||||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
count=count, | ||||||||||||||
deleted=deleted, | ||||||||||||||
organization_id=organization_id, | ||||||||||||||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.