-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add GitHub App OAuth provider (#12020)
Extracted from readthedocs/common#259. The provider is not exposed to users yet, and we allow only staff users to use it (manually going to /accounts/githubapp/login). There are steps for ops team and Eric or Anthony to do: - Create a new GH app from https://github.com/organizations/readthedocs/settings/apps/new (the name will be used when we do actions as the installation, like when creating a comment). - Callback URL should be https://app.readthedocs.org/accounts/githubapp/login/callback/ - Keep marked "Expire user authorization tokens" - Don't active the webhook, since we aren't going to use it yet. - Permissions (can be updated later if required): - 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) - Subscribe to events (can be updated later if required): Installation target, Member, Organization, Membership, Pull request, Push, Repository. - Where can this GitHub App be installed?: any account - Copy the client ID and client secret into ops repo for the githubapp provider, we can skip setting a webhook secret and private key, as they won't be used for now. Same process for the app for .com.
- Loading branch information
Showing
10 changed files
with
295 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from allauth.socialaccount.providers.github.provider import GitHubProvider | ||
|
||
from readthedocs.allauth.providers.githubapp.views import GitHubAppOAuth2Adapter | ||
|
||
|
||
class GitHubAppProvider(GitHubProvider): | ||
""" | ||
Provider for GitHub App. | ||
We subclass the GitHubProvider to have two separate providers for the GitHub OAuth App and the GitHub App. | ||
""" | ||
|
||
id = "githubapp" | ||
oauth2_adapter_class = GitHubAppOAuth2Adapter | ||
|
||
|
||
provider_classes = [GitHubAppProvider] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
"""Copied from allauth.socialaccount.providers.github.urls.""" | ||
|
||
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns | ||
|
||
from readthedocs.allauth.providers.githubapp.provider import GitHubAppProvider | ||
|
||
urlpatterns = default_urlpatterns(GitHubAppProvider) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
"""Copied from allauth.socialaccount.providers.github.views.""" | ||
|
||
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter | ||
from allauth.socialaccount.providers.oauth2.views import ( | ||
OAuth2CallbackView, | ||
OAuth2LoginView, | ||
) | ||
|
||
|
||
class GitHubAppOAuth2Adapter(GitHubOAuth2Adapter): | ||
provider_id = "githubapp" | ||
|
||
|
||
oauth2_login = OAuth2LoginView.adapter_view(GitHubAppOAuth2Adapter) | ||
oauth2_callback = OAuth2CallbackView.adapter_view(GitHubAppOAuth2Adapter) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
from unittest import mock | ||
|
||
from allauth.exceptions import ImmediateHttpResponse | ||
from allauth.socialaccount.adapter import get_adapter as get_social_account_adapter | ||
from allauth.socialaccount.models import SocialAccount, SocialLogin | ||
from allauth.socialaccount.providers.github.provider import GitHubProvider | ||
from django.contrib.auth.models import AnonymousUser, User | ||
from django.test import TestCase | ||
from django_dynamic_fixture import get | ||
|
||
from readthedocs.allauth.providers.githubapp.provider import GitHubAppProvider | ||
|
||
|
||
class SocialAdapterTest(TestCase): | ||
def setUp(self): | ||
self.user = get(User, username="test") | ||
self.adapter = get_social_account_adapter() | ||
|
||
def test_dont_allow_using_githubapp_for_non_staff_users(self): | ||
assert not SocialAccount.objects.filter(provider=GitHubAppProvider.id).exists() | ||
|
||
# Anonymous user | ||
request = mock.MagicMock(user=AnonymousUser()) | ||
sociallogin = SocialLogin( | ||
user=User(email="me@example.com"), | ||
account=SocialAccount(provider=GitHubAppProvider.id), | ||
) | ||
with self.assertRaises(ImmediateHttpResponse) as exc: | ||
self.adapter.pre_social_login(request, sociallogin) | ||
self.assertEqual(exc.exception.response.status_code, 302) | ||
|
||
assert not SocialAccount.objects.filter(provider=GitHubAppProvider.id).exists() | ||
|
||
# Existing non-staff user | ||
assert not self.user.is_staff | ||
request = mock.MagicMock(user=self.user) | ||
sociallogin = SocialLogin( | ||
user=User(email="me@example.com"), | ||
account=SocialAccount(provider=GitHubAppProvider.id), | ||
) | ||
with self.assertRaises(ImmediateHttpResponse) as exc: | ||
self.adapter.pre_social_login(request, sociallogin) | ||
self.assertEqual(exc.exception.response.status_code, 302) | ||
assert not self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
def test_allow_using_githubapp_for_staff_users(self): | ||
self.user.is_staff = True | ||
self.user.save() | ||
assert self.user.is_staff | ||
|
||
request = mock.MagicMock(user=self.user) | ||
sociallogin = SocialLogin( | ||
user=User(email="me@example.com"), | ||
account=SocialAccount(provider=GitHubAppProvider.id), | ||
) | ||
self.adapter.pre_social_login(request, sociallogin) | ||
# No exception raised, but the account is not created, as that is done in another step by allauth. | ||
assert not self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
def test_connect_to_existing_github_account_from_staff_user(self): | ||
self.user.is_staff = True | ||
self.user.save() | ||
assert self.user.is_staff | ||
assert not self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
github_account = get( | ||
SocialAccount, | ||
provider=GitHubProvider.id, | ||
uid="1234", | ||
user=self.user, | ||
) | ||
|
||
request = mock.MagicMock(user=AnonymousUser()) | ||
sociallogin = SocialLogin( | ||
user=User(email="me@example.com"), | ||
account=SocialAccount( | ||
provider=GitHubAppProvider.id, uid=github_account.uid | ||
), | ||
) | ||
self.adapter.pre_social_login(request, sociallogin) | ||
# A new user is not created, but the existing user is connected to the GitHub App. | ||
assert self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
def test_connect_to_existing_github_account_from_staff_user_logged_in(self): | ||
self.user.is_staff = True | ||
self.user.save() | ||
assert self.user.is_staff | ||
assert not self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
github_account = get( | ||
SocialAccount, | ||
provider=GitHubProvider.id, | ||
uid="1234", | ||
user=self.user, | ||
) | ||
|
||
request = mock.MagicMock(user=self.user) | ||
sociallogin = SocialLogin( | ||
user=User(email="me@example.com"), | ||
account=SocialAccount( | ||
provider=GitHubAppProvider.id, uid=github_account.uid | ||
), | ||
) | ||
self.adapter.pre_social_login(request, sociallogin) | ||
# A new user is not created, but the existing user is connected to the GitHub App. | ||
assert self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
def test_dont_connect_to_existing_github_account_if_user_is_logged_in_with_different_account( | ||
self, | ||
): | ||
self.user.is_staff = True | ||
self.user.save() | ||
assert self.user.is_staff | ||
assert not self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
github_account = get( | ||
SocialAccount, | ||
provider=GitHubProvider.id, | ||
uid="1234", | ||
user=self.user, | ||
) | ||
|
||
another_user = get(User, username="another") | ||
request = mock.MagicMock(user=another_user) | ||
sociallogin = SocialLogin( | ||
user=User(email="me@example.com"), | ||
account=SocialAccount( | ||
provider=GitHubAppProvider.id, uid=github_account.uid | ||
), | ||
) | ||
with self.assertRaises(ImmediateHttpResponse) as exc: | ||
self.adapter.pre_social_login(request, sociallogin) | ||
self.assertEqual(exc.exception.response.status_code, 302) | ||
assert not self.user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
assert not another_user.socialaccount_set.filter( | ||
provider=GitHubAppProvider.id | ||
).exists() | ||
|
||
def test_allow_existing_githubapp_accounts_to_login(self): | ||
assert not self.user.is_staff | ||
githubapp_account = get( | ||
SocialAccount, | ||
provider=GitHubAppProvider.id, | ||
uid="1234", | ||
user=self.user, | ||
) | ||
|
||
request = mock.MagicMock(user=AnonymousUser()) | ||
sociallogin = SocialLogin( | ||
user=self.user, | ||
account=githubapp_account, | ||
) | ||
self.adapter.pre_social_login(request, sociallogin) | ||
|
||
self.user.is_staff = True | ||
self.user.save() | ||
assert self.user.is_staff | ||
self.adapter.pre_social_login(request, sociallogin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters