diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index eab9237986..3c78477ffe 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -1665,7 +1665,7 @@ class GitHubAuthBackendTest(SocialAuthBase): @override_settings(SOCIAL_AUTH_GITHUB_TEAM_ID='zulip-webapp') def test_social_auth_github_team_not_member_failed(self) -> None: account_data_dict = self.get_account_data_dict(email=self.email, name=self.name) - with mock.patch('social_core.backends.github.GithubTeamOAuth2.user_data', + with mock.patch('zproject.backends.GithubTeamBackend.user_data', side_effect=AuthFailed('Not found')), \ mock.patch('logging.info') as mock_info: result = self.social_auth_test(account_data_dict, @@ -1677,7 +1677,7 @@ class GitHubAuthBackendTest(SocialAuthBase): @override_settings(SOCIAL_AUTH_GITHUB_TEAM_ID='zulip-webapp') def test_social_auth_github_team_member_success(self) -> None: account_data_dict = self.get_account_data_dict(email=self.email, name=self.name) - with mock.patch('social_core.backends.github.GithubTeamOAuth2.user_data', + with mock.patch('zproject.backends.GithubTeamBackend.user_data', return_value=account_data_dict): result = self.social_auth_test(account_data_dict, expect_choose_email_screen=True, @@ -1690,7 +1690,7 @@ class GitHubAuthBackendTest(SocialAuthBase): @override_settings(SOCIAL_AUTH_GITHUB_ORG_NAME='Zulip') def test_social_auth_github_organization_not_member_failed(self) -> None: account_data_dict = self.get_account_data_dict(email=self.email, name=self.name) - with mock.patch('social_core.backends.github.GithubOrganizationOAuth2.user_data', + with mock.patch('zproject.backends.GithubOrganizationBackend.user_data', side_effect=AuthFailed('Not found')), \ mock.patch('logging.info') as mock_info: result = self.social_auth_test(account_data_dict, @@ -1702,7 +1702,7 @@ class GitHubAuthBackendTest(SocialAuthBase): @override_settings(SOCIAL_AUTH_GITHUB_ORG_NAME='Zulip') def test_social_auth_github_organization_member_success(self) -> None: account_data_dict = self.get_account_data_dict(email=self.email, name=self.name) - with mock.patch('social_core.backends.github.GithubOrganizationOAuth2.user_data', + with mock.patch('zproject.backends.GithubOrganizationBackend.user_data', return_value=account_data_dict): result = self.social_auth_test(account_data_dict, expect_choose_email_screen=True, diff --git a/zproject/backends.py b/zproject/backends.py index 5407b0f613..e6d3ed31b6 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -16,8 +16,10 @@ import copy import logging import magic from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union, \ + no_type_check from typing_extensions import TypedDict +from urllib.parse import urljoin from zxcvbn import zxcvbn from django_auth_ldap.backend import LDAPBackend, LDAPReverseEmailSearch, \ @@ -37,7 +39,7 @@ from django.utils.translation import ugettext as _ from requests import HTTPError from onelogin.saml2.errors import OneLogin_Saml2_Error from social_core.backends.github import GithubOAuth2, GithubOrganizationOAuth2, \ - GithubTeamOAuth2 + GithubTeamOAuth2, GithubMemberOAuth2 from social_core.backends.azuread import AzureADOAuth2 from social_core.backends.gitlab import GitLabOAuth2 from social_core.backends.base import BaseAuth @@ -1316,13 +1318,13 @@ class GitHubAuthBackend(SocialAuthMixin, GithubOAuth2): access_token, *args, **kwargs ) elif team_id is not None: - backend = GithubTeamOAuth2(self.strategy, self.redirect_uri) + backend = GithubTeamBackend(self.strategy, self.redirect_uri) try: return backend.user_data(access_token, *args, **kwargs) except AuthFailed: return dict(auth_failed_reason="GitHub user is not member of required team") elif org_name is not None: - backend = GithubOrganizationOAuth2(self.strategy, self.redirect_uri) + backend = GithubOrganizationBackend(self.strategy, self.redirect_uri) try: return backend.user_data(access_token, *args, **kwargs) except AuthFailed: @@ -1330,6 +1332,42 @@ class GitHubAuthBackend(SocialAuthMixin, GithubOAuth2): raise AssertionError("Invalid configuration") + def _user_data(self, access_token: str, path: Any=None) -> Any: + # Monkey patching. Should be removed once upstream merges a fix for + # https://github.com/python-social-auth/social-core/issues/430 + url = urljoin(self.api_url(), 'user{0}'.format(path or '')) + return self.get_json(url, headers={'Authorization': 'token {0}'.format(access_token)}) + +class GithubMemberUserDataMixin(GithubMemberOAuth2): + """ + This mixin class and the ones inheriting from it serve as a way + to monkey-patch a fix for https://github.com/python-social-auth/social-core/issues/430 + Changes from the commit adding this should be reverted once the issue is fixed upstream. + """ + @no_type_check + def user_data(self, access_token: str, *args: Any, **kwargs: Any) -> Any: # nocoverage + # this is copy-pasted from a good PR upstream that fixes the issue. + """Loads user data from service""" + user_data = super(GithubMemberOAuth2, self).user_data( + access_token, *args, **kwargs + ) + headers = {'Authorization': 'token {0}'.format(access_token)} + try: + self.request(self.member_url(user_data), headers=headers) + except HTTPError as err: + # if the user is a member of the organization, response code + # will be 204, see http://bit.ly/ZS6vFl + if err.response.status_code != 204: + raise AuthFailed(self, + 'User doesn\'t belong to the organization') + return user_data + +class GithubTeamBackend(GithubMemberUserDataMixin, GithubTeamOAuth2): + pass + +class GithubOrganizationBackend(GithubMemberUserDataMixin, GithubOrganizationOAuth2): + pass + @external_auth_method class AzureADAuthBackend(SocialAuthMixin, AzureADOAuth2): sort_order = 50