From 9626fb05036a56d9b6d8e828a140f34ae0a5f178 Mon Sep 17 00:00:00 2001 From: Jonas Carson Date: Wed, 9 Oct 2024 11:17:46 -0700 Subject: [PATCH 1/6] Updates to our drupal oauth provider and adapter to be compatible with updated django-allauth --- config/settings/base.py | 1 + .../drupal_oauth_provider/provider.py | 10 ++ gregor_django/drupal_oauth_provider/tests.py | 110 +++++++++++++-- gregor_django/drupal_oauth_provider/views.py | 4 +- .../socialaccount/authentication_error.html | 2 +- gregor_django/users/adapters.py | 21 ++- gregor_django/users/tests/test_adapters.py | 131 ++++++++++++------ requirements/requirements.in | 2 +- requirements/requirements.txt | 2 +- 9 files changed, 222 insertions(+), 61 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index 5539f254..f33bb9ec 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -155,6 +155,7 @@ "maintenance_mode.middleware.MaintenanceModeMiddleware", "simple_history.middleware.HistoryRequestMiddleware", "django_htmx.middleware.HtmxMiddleware", + "allauth.account.middleware.AccountMiddleware", ] # STATIC diff --git a/gregor_django/drupal_oauth_provider/provider.py b/gregor_django/drupal_oauth_provider/provider.py index ef2d9c93..c7c36f60 100644 --- a/gregor_django/drupal_oauth_provider/provider.py +++ b/gregor_django/drupal_oauth_provider/provider.py @@ -2,11 +2,14 @@ from allauth.account.models import EmailAddress from allauth.socialaccount import app_settings, providers +from allauth.socialaccount.adapter import get_adapter from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from .views import CustomAdapter + logger = logging.getLogger(__name__) DRUPAL_PROVIDER_ID = "drupal_oauth_provider" @@ -27,6 +30,13 @@ class CustomProvider(OAuth2Provider): id = DRUPAL_PROVIDER_ID name = OVERRIDE_NAME account_class = CustomAccount + oauth2_adapter_class = CustomAdapter + supports_token_authentication = True + + def __init__(self, request, app=None): + if app is None: + app = get_adapter().get_app(request, self.id) + super().__init__(request, app=app) def extract_uid(self, data): return str(data["sub"]) diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py index 7fc52d06..2d9b5af5 100644 --- a/gregor_django/drupal_oauth_provider/tests.py +++ b/gregor_django/drupal_oauth_provider/tests.py @@ -1,11 +1,18 @@ +import base64 import datetime +import hashlib import json +from urllib.parse import parse_qs, urlparse import jwt +import requests +from allauth.socialaccount import app_settings from allauth.socialaccount.adapter import get_adapter +from allauth.socialaccount.models import SocialApp from allauth.socialaccount.tests import OAuth2TestsMixin from allauth.tests import MockedResponse, TestCase from django.conf import settings +from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory from django.test.utils import override_settings @@ -83,10 +90,18 @@ def sign_id_token(payload): # disable token storing for testing as it conflicts with drupals use # of tokens for user info -@override_settings(SOCIALACCOUNT_STORE_TOKENS=False) +@override_settings(SOCIALACCOUNT_STORE_TOKENS=True) class CustomProviderTests(OAuth2TestsMixin, TestCase): provider_id = CustomProvider.id + def setUp(self): + super(CustomProviderTests, self).setUp() + # workaround to create a session. see: + # https://code.djangoproject.com/ticket/11475 + User = get_user_model() + User.objects.create_user("testuser", "testuser@testuser.com", "testpw") + self.client.login(username="testuser", password="testpw") + # Provide two mocked responses, first is to the public key request # second is used for the profile request for extra data def get_mocked_response(self): @@ -104,13 +119,68 @@ def get_mocked_response(self): ), ] - # This login response mimics drupals in that it contains a set of scopes - # and the uid which has the name sub - def get_login_response_json(self, with_refresh_token=True): + def login(self, resp_mock=None, process="login", with_refresh_token=True): + """ + Unfortunately due to how our provider works we need to alter + this test login function as the default one fails. + """ + with self.mocked_response(): + resp = self.client.post(self.provider.get_login_url(self.request, process=process)) + p = urlparse(resp["location"]) + q = parse_qs(p.query) + pkce_enabled = app_settings.PROVIDERS.get(self.app.provider, {}).get( + "OAUTH_PKCE_ENABLED", self.provider.pkce_enabled_default + ) + + self.assertEqual("code_challenge" in q, pkce_enabled) + self.assertEqual("code_challenge_method" in q, pkce_enabled) + if pkce_enabled: + code_challenge = q["code_challenge"][0] + self.assertEqual(q["code_challenge_method"][0], "S256") + + complete_url = self.provider.get_callback_url() + self.assertGreater(q["redirect_uri"][0].find(complete_url), 0) + response_json = self.get_login_response_json(with_refresh_token=with_refresh_token) + + resp_mocks = resp_mock if isinstance(resp_mock, list) else ([resp_mock] if resp_mock is not None else []) + + with self.mocked_response( + MockedResponse(200, response_json, {"content-type": "application/json"}), + *resp_mocks, + ): + resp = self.client.get(complete_url, self.get_complete_parameters(q)) + + # Find the access token POST request, and assert that it contains + # the correct code_verifier if and only if PKCE is enabled + request_calls = requests.Session.request.call_args_list + import sys + + print(f"REQUEST CALLS {request_calls}", file=sys.stderr) + for args, kwargs in request_calls: + print(f"RC: {args} kwargs: {kwargs}", file=sys.stderr) + data = kwargs.get("data", {}) + if ( + args + and args[0] == "POST" + and isinstance(data, dict) + and data.get("redirect_uri", "").endswith(complete_url) + ): + self.assertEqual("code_verifier" in data, pkce_enabled) + + if pkce_enabled: + hashed_code_verifier = hashlib.sha256(data["code_verifier"].encode("ascii")) + expected_code_challenge = ( + base64.urlsafe_b64encode(hashed_code_verifier.digest()).rstrip(b"=").decode() + ) + self.assertEqual(code_challenge, expected_code_challenge) + + return resp + + def get_id_token(self): now = datetime.datetime.now(datetime.timezone.utc) app = get_adapter().get_app(request=None, provider=self.provider_id) allowed_audience = app.client_id - id_token = sign_id_token( + return sign_id_token( { "exp": now + datetime.timedelta(hours=1), "iat": now, @@ -119,6 +189,17 @@ def get_login_response_json(self, with_refresh_token=True): "sub": 20122, } ) + + def get_access_token(self) -> str: + return self.get_id_token() + + def get_expected_to_str(self): + return "test@testmaster.net" + + # This login response mimics drupals in that it contains a set of scopes + # and the uid which has the name sub + def get_login_response_json(self, with_refresh_token=True): + id_token = self.get_id_token() response_data = { "access_token": id_token, "expires_in": 3600, @@ -131,6 +212,19 @@ def get_login_response_json(self, with_refresh_token=True): class TestProviderConfig(TestCase): + def setUp(self): + # workaround to create a session. see: + # https://code.djangoproject.com/ticket/11475 + + app = SocialApp.objects.create( + provider=CustomProvider.id, + name=CustomProvider.id, + client_id="app123id", + key=CustomProvider.id, + secret="dummy", + ) + self.app = app + def test_custom_provider_scope_config(self): custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS rf = RequestFactory() @@ -138,7 +232,7 @@ def test_custom_provider_scope_config(self): custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = None with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): with self.assertRaises(ImproperlyConfigured): - CustomProvider(request).get_provider_scope_config() + CustomProvider(request, app=self.app).get_provider_scope_config() def test_custom_provider_scope_detail_config(self): custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS @@ -153,7 +247,7 @@ def test_custom_provider_scope_detail_config(self): ] with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): with self.assertRaises(ImproperlyConfigured): - CustomProvider(request).get_provider_managed_scope_status() + CustomProvider(request, app=self.app).get_provider_managed_scope_status() def test_custom_provider_has_scope(self): custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS @@ -167,4 +261,4 @@ def test_custom_provider_has_scope(self): } ] with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings): - CustomProvider(request).get_provider_managed_scope_status(scopes_granted=["X"]) + CustomProvider(request, app=self.app).get_provider_managed_scope_status(scopes_granted=["X"]) diff --git a/gregor_django/drupal_oauth_provider/views.py b/gregor_django/drupal_oauth_provider/views.py index 317021a8..655f6f4d 100644 --- a/gregor_django/drupal_oauth_provider/views.py +++ b/gregor_django/drupal_oauth_provider/views.py @@ -12,13 +12,13 @@ OAuth2LoginView, ) -from .provider import CustomProvider +# from .provider import CustomProvider logger = logging.getLogger(__name__) class CustomAdapter(OAuth2Adapter): - provider_id = CustomProvider.id + provider_id = "drupal_oauth_provider" provider_settings = app_settings.PROVIDERS.get(provider_id, {}) diff --git a/gregor_django/templates/socialaccount/authentication_error.html b/gregor_django/templates/socialaccount/authentication_error.html index 92a5b4c8..ba0bb87a 100644 --- a/gregor_django/templates/socialaccount/authentication_error.html +++ b/gregor_django/templates/socialaccount/authentication_error.html @@ -1,4 +1,4 @@ -{% extends "socialaccount/base.html" %} +{% extends "base.html" %} {% load i18n %} diff --git a/gregor_django/users/adapters.py b/gregor_django/users/adapters.py index 26f0075c..90272c3b 100644 --- a/gregor_django/users/adapters.py +++ b/gregor_django/users/adapters.py @@ -24,6 +24,10 @@ def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) def update_user_info(self, user, extra_data: Dict): + import sys + + print(f"USER3: {user} {user.username} id: {user.id}", file=sys.stderr) + logger.info(f"User {user} username {user.username}") drupal_username = extra_data.get("preferred_username") drupal_email = extra_data.get("email") first_name = extra_data.get("first_name") @@ -32,21 +36,21 @@ def update_user_info(self, user, extra_data: Dict): user_changed = False if user.name != full_name: logger.info( - f"[SocialAccountAdatpter:update_user_name] user {user} " f"name updated from {user.name} to {full_name}" + f"[SocialAccountAdatpter:update_user_info] user {user} " f"name updated from {user.name} to {full_name}" ) user.name = full_name user_changed = True if user.username != drupal_username: logger.info( - f"[SocialAccountAdatpter:update_user_name] user {user} " + f"[SocialAccountAdatpter:update_user_info] user {user} " f"username updated from {user.username} to {drupal_username}" ) user.username = drupal_username user_changed = True if user.email != drupal_email: logger.info( - f"[SocialAccountAdatpter:update_user_name] user {user}" - f" email updated from {user.email} to {drupal_email}" + f"[SocialAccountAdatpter:update_user_info] user {user.username}" + # f" email updated from {user.email} to {drupal_email}" ) user.email = drupal_email user_changed = True @@ -186,10 +190,13 @@ def update_user_data(self, sociallogin: Any): self.update_user_partner_groups(user, extra_data) self.update_user_groups(user, extra_data) - def authentication_error(self, request, provider_id, error, exception, extra_context): + def on_authentication_error(self, request, provider_id, error, exception, extra_context): """ Invoked when there is an error in auth cycle. Log so we know what is going on. """ - logger.error(f"[SocialAccountAdapter:authentication_error] Error {error} Exception: {exception}") - super().authentication_error(request, provider_id, error, exception, extra_context) + logger.error( + f"[SocialAccountAdapter:on_authentication_error] Provider: {provider_id} " + f"Error {error} Exception: {exception} extra {extra_context}" + ) + super().on_authentication_error(request, provider_id, error, exception, extra_context) diff --git a/gregor_django/users/tests/test_adapters.py b/gregor_django/users/tests/test_adapters.py index 4e37d11e..4e6b77ae 100644 --- a/gregor_django/users/tests/test_adapters.py +++ b/gregor_django/users/tests/test_adapters.py @@ -1,15 +1,20 @@ +# myapp/tests.py + import pytest from allauth.account import app_settings as account_settings -from allauth.socialaccount.helpers import complete_social_login -from allauth.socialaccount.models import SocialAccount, SocialLogin -from allauth.utils import get_user_model +from allauth.account import signals +from allauth.socialaccount.models import SocialAccount, SocialApp, SocialLogin +from django.contrib.auth import get_user_model +from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.auth.models import AnonymousUser -from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings +from gregor_django.drupal_oauth_provider.provider import CustomProvider from gregor_django.gregor_anvil.tests.factories import ( PartnerGroupFactory, ResearchCenterFactory, @@ -18,48 +23,77 @@ from .factories import GroupFactory, UserFactory +User = get_user_model() -@pytest.mark.django_db -class TestsUserSocialLoginAdapter(object): - @override_settings( - SOCIALACCOUNT_AUTO_SIGNUP=True, - ACCOUNT_SIGNUP_FORM_CLASS=None, - ACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.NONE, # noqa - ) - def test_drupal_social_login_adapter(self): - factory = RequestFactory() - request = factory.get("/accounts/login/callback/") - request.user = AnonymousUser() - SessionMiddleware(lambda request: None).process_request(request) - MessageMiddleware(lambda request: None).process_request(request) - - User = get_user_model() - user = User() - old_name = "Old Name" - old_username = "test" - old_email = "test@example.com" - setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, "test") - setattr(user, "name", "Old Name") - setattr(user, account_settings.USER_MODEL_EMAIL_FIELD, "test@example.com") - account = SocialAccount( +class SocialAccountAdapterTest(TestCase): + def setUp(self): + self.factory = RequestFactory() + # Setup a mock social app + current_site = Site.objects.get_current() + self.social_app = SocialApp.objects.create( + provider=CustomProvider.id, + name="DOA", + client_id="test-client-id", + secret="test-client-secret", + ) + self.social_app.sites.add(current_site) + + def extract_state_from_url(self, url): + from urllib.parse import parse_qs, urlparse + + parsed_url = urlparse(url) + query_params = parse_qs(parsed_url.query) + return query_params.get("state", [None])[0] + + def test_social_login_success(self): + # Mock user + request = self.factory.get("/") + middleware = SessionMiddleware(lambda x: None) + middleware.process_request(request) + request.session.save() + middleware = AuthenticationMiddleware(lambda x: None) + middleware.process_request(request) + request.user = AnonymousUser() + user = User.objects.create(username="testuser", email="testuser@example.com") + + # # Mock social login + # Create a mock SocialAccount and link it to the user + new_first_name = "Bob" + new_last_name = "Rob" + social_account = SocialAccount.objects.create( + user=user, provider="drupal_oauth_provider", - uid="123", - extra_data=dict( - first_name="Old", - last_name="Name", - email=old_email, - preferred_username=old_username, - ), + uid="12345", + extra_data={ + "preferred_username": "testuser", + "first_name": new_first_name, + "last_name": new_last_name, + "email": "testuser@example.com", + }, ) - sociallogin = SocialLogin(user=user, account=account) - complete_social_login(request, sociallogin) - user = User.objects.get(**{account_settings.USER_MODEL_USERNAME_FIELD: "test"}) - assert SocialAccount.objects.filter(user=user, uid=account.uid).exists() is True - assert user.name == old_name - assert user.username == old_username - assert user.email == old_email + # Create a mock SocialLogin object and associate the user and social account + sociallogin = SocialLogin(user=user, account=social_account) + + # Simulate social login + from allauth.account.adapter import get_adapter + + # adapter = SocialAccountAdapter() + adapter = get_adapter(request) + + adapter.login(request, user) + + signals.user_logged_in.send( + sender=user.__class__, + request=request, + user=user, + sociallogin=sociallogin, + ) + # Check if the login completed successfully + self.assertEqual(sociallogin.user, user) + self.assertEqual(request.user, user) + self.assertEqual(user.name, f"{new_first_name} {new_last_name}") def test_update_user_info(self): adapter = SocialAccountAdapter() @@ -105,6 +139,21 @@ def test_update_user_research_centers_add(self): assert user.research_centers.filter(pk=rc1.pk).exists() assert user.research_centers.all().count() == 1 + def test_update_user_research_centers_short_name_add(self): + adapter = SocialAccountAdapter() + rc1 = ResearchCenterFactory(short_name="rc1") + + User = get_user_model() + user = User() + setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, "test") + setattr(user, account_settings.USER_MODEL_EMAIL_FIELD, "test@example.com") + + user.save() + + adapter.update_user_research_centers(user, dict(research_center_or_site=[rc1.short_name])) + assert user.research_centers.filter(pk=rc1.pk).exists() + assert user.research_centers.all().count() == 1 + def test_update_user_research_centers_remove(self): adapter = SocialAccountAdapter() rc1 = ResearchCenterFactory(short_name="rc1") diff --git a/requirements/requirements.in b/requirements/requirements.in index 313b7167..ecf9c6a2 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -5,7 +5,7 @@ pip-tools whitenoise # https://github.com/evansd/whitenoise # Login via oauth oauthlib # https://github.com/oauthlib/oauthlib - +cryptography # https://github.com/pyca/cryptography # Password hashing argon2-cffi # https://github.com/hynek/argon2_cffi diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a314a609..789a5fa5 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -51,7 +51,7 @@ django==4.2.16 # django-picklefield # django-simple-history # django-tables2 -django-allauth==0.54.0 +django-allauth==65.0.2 # via -r requirements/requirements.in django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.25 # via -r requirements/requirements.in From 705893b6f85ae687cf85a4b19c6d5e7ef8a05f19 Mon Sep 17 00:00:00 2001 From: Jonas Carson Date: Thu, 10 Oct 2024 08:50:31 -0700 Subject: [PATCH 2/6] Clean up un-needed changes introduced when working through compatiblity issues with new allauth. --- gregor_django/drupal_oauth_provider/tests.py | 3 --- gregor_django/drupal_oauth_provider/views.py | 2 -- gregor_django/users/adapters.py | 8 ++------ 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py index 2d9b5af5..c27164ab 100644 --- a/gregor_django/drupal_oauth_provider/tests.py +++ b/gregor_django/drupal_oauth_provider/tests.py @@ -153,11 +153,8 @@ def login(self, resp_mock=None, process="login", with_refresh_token=True): # Find the access token POST request, and assert that it contains # the correct code_verifier if and only if PKCE is enabled request_calls = requests.Session.request.call_args_list - import sys - print(f"REQUEST CALLS {request_calls}", file=sys.stderr) for args, kwargs in request_calls: - print(f"RC: {args} kwargs: {kwargs}", file=sys.stderr) data = kwargs.get("data", {}) if ( args diff --git a/gregor_django/drupal_oauth_provider/views.py b/gregor_django/drupal_oauth_provider/views.py index 655f6f4d..80274a40 100644 --- a/gregor_django/drupal_oauth_provider/views.py +++ b/gregor_django/drupal_oauth_provider/views.py @@ -12,8 +12,6 @@ OAuth2LoginView, ) -# from .provider import CustomProvider - logger = logging.getLogger(__name__) diff --git a/gregor_django/users/adapters.py b/gregor_django/users/adapters.py index 90272c3b..27ac8a2b 100644 --- a/gregor_django/users/adapters.py +++ b/gregor_django/users/adapters.py @@ -24,10 +24,6 @@ def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) def update_user_info(self, user, extra_data: Dict): - import sys - - print(f"USER3: {user} {user.username} id: {user.id}", file=sys.stderr) - logger.info(f"User {user} username {user.username}") drupal_username = extra_data.get("preferred_username") drupal_email = extra_data.get("email") first_name = extra_data.get("first_name") @@ -49,8 +45,8 @@ def update_user_info(self, user, extra_data: Dict): user_changed = True if user.email != drupal_email: logger.info( - f"[SocialAccountAdatpter:update_user_info] user {user.username}" - # f" email updated from {user.email} to {drupal_email}" + f"[SocialAccountAdatpter:update_user_info] user {user}" + f" email updated from {user.email} to {drupal_email}" ) user.email = drupal_email user_changed = True From a1262e97c01403c1672345fa4e30401ee399629a Mon Sep 17 00:00:00 2001 From: Jonas Carson Date: Thu, 10 Oct 2024 10:09:23 -0700 Subject: [PATCH 3/6] Updated requirements needed due to allauth upgrade --- requirements/requirements.in | 2 ++ requirements/requirements.txt | 15 +++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index ecf9c6a2..e39662ee 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -6,6 +6,8 @@ whitenoise # https://github.com/evansd/whitenoise # Login via oauth oauthlib # https://github.com/oauthlib/oauthlib cryptography # https://github.com/pyca/cryptography +pyjwt # https://github.com/jpadilla/pyjwt +requests-oauthlib # https://github.com/requests/requests-oauthlib # Password hashing argon2-cffi # https://github.com/hynek/argon2_cffi diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d66cb52f..36695d7a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -32,9 +32,7 @@ crispy-bootstrap5==2024.2 # -r requirements/requirements.in # django-anvil-consortium-manager cryptography==43.0.1 - # via pyjwt -defusedxml==0.7.1 - # via python3-openid + # via -r requirements/requirements.in django==4.2.16 # via # -r requirements/requirements.in @@ -124,28 +122,25 @@ pyasn1-modules==0.3.0 # via google-auth pycparser==2.21 # via cffi -pyjwt[crypto]==2.8.0 - # via django-allauth +pyjwt==2.8.0 + # via -r requirements/requirements.in pyproject-hooks==1.0.0 # via # build # pip-tools python-fsutil==0.13.1 # via django-maintenance-mode -python3-openid==3.2.0 - # via django-allauth pytz==2023.4 # via # django-anvil-consortium-manager # django-dbbackup requests==2.32.3 # via - # django-allauth # django-anvil-consortium-manager # jsonapi-requests # requests-oauthlib -requests-oauthlib==1.3.1 - # via django-allauth +requests-oauthlib==2.0.0 + # via -r requirements/requirements.in rsa==4.9 # via google-auth six==1.16.0 From 89b640c1f7718c9db98e5e2d8cb86031730b10ef Mon Sep 17 00:00:00 2001 From: Jonas Carson Date: Thu, 10 Oct 2024 11:01:41 -0700 Subject: [PATCH 4/6] Fix missing coverage and remove unused test function --- gregor_django/drupal_oauth_provider/tests.py | 10 +++++++++- .../templates/socialaccount/authentication_error.html | 2 +- gregor_django/users/tests/test_adapters.py | 7 ------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py index c27164ab..214787e6 100644 --- a/gregor_django/drupal_oauth_provider/tests.py +++ b/gregor_django/drupal_oauth_provider/tests.py @@ -13,6 +13,7 @@ from allauth.tests import MockedResponse, TestCase from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory from django.test.utils import override_settings @@ -212,7 +213,7 @@ class TestProviderConfig(TestCase): def setUp(self): # workaround to create a session. see: # https://code.djangoproject.com/ticket/11475 - + current_site = Site.objects.get_current() app = SocialApp.objects.create( provider=CustomProvider.id, name=CustomProvider.id, @@ -221,6 +222,13 @@ def setUp(self): secret="dummy", ) self.app = app + self.app.sites.add(current_site) + + def test_custom_provider_no_app(self): + rf = RequestFactory() + request = rf.get("/fake-url/") + provider = CustomProvider(request) + assert provider.app is not None def test_custom_provider_scope_config(self): custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS diff --git a/gregor_django/templates/socialaccount/authentication_error.html b/gregor_django/templates/socialaccount/authentication_error.html index ba0bb87a..76166bfe 100644 --- a/gregor_django/templates/socialaccount/authentication_error.html +++ b/gregor_django/templates/socialaccount/authentication_error.html @@ -2,7 +2,7 @@ {% load i18n %} -{% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %} +{% block title %}{% trans "Social Network Login Failure" %}{% endblock %} {% block content %}

{% trans "Social Network Login Failure" %}

diff --git a/gregor_django/users/tests/test_adapters.py b/gregor_django/users/tests/test_adapters.py index 4e6b77ae..c21abe34 100644 --- a/gregor_django/users/tests/test_adapters.py +++ b/gregor_django/users/tests/test_adapters.py @@ -39,13 +39,6 @@ def setUp(self): ) self.social_app.sites.add(current_site) - def extract_state_from_url(self, url): - from urllib.parse import parse_qs, urlparse - - parsed_url = urlparse(url) - query_params = parse_qs(parsed_url.query) - return query_params.get("state", [None])[0] - def test_social_login_success(self): # Mock user request = self.factory.get("/") From f06f15d8cf9f2dcd904bc88cd490d707041f1bf5 Mon Sep 17 00:00:00 2001 From: Jonas Carson Date: Wed, 16 Oct 2024 08:22:18 -0700 Subject: [PATCH 5/6] Fix issue where id token was changing when test past second boundary. Use same time variable. --- gregor_django/drupal_oauth_provider/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py index 214787e6..ca2b598d 100644 --- a/gregor_django/drupal_oauth_provider/tests.py +++ b/gregor_django/drupal_oauth_provider/tests.py @@ -102,6 +102,7 @@ def setUp(self): User = get_user_model() User.objects.create_user("testuser", "testuser@testuser.com", "testpw") self.client.login(username="testuser", password="testpw") + self.setup_time = datetime.datetime.now(datetime.timezone.utc) # Provide two mocked responses, first is to the public key request # second is used for the profile request for extra data @@ -175,13 +176,12 @@ def login(self, resp_mock=None, process="login", with_refresh_token=True): return resp def get_id_token(self): - now = datetime.datetime.now(datetime.timezone.utc) app = get_adapter().get_app(request=None, provider=self.provider_id) allowed_audience = app.client_id return sign_id_token( { - "exp": now + datetime.timedelta(hours=1), - "iat": now, + "exp": self.setup_time + datetime.timedelta(hours=1), + "iat": self.setup_time, "aud": allowed_audience, "scope": ["authenticated", "oauth_client_user"], "sub": 20122, From 2841a97aad96a4cdb689a1bfb29b4aca3f143a88 Mon Sep 17 00:00:00 2001 From: Jonas Carson Date: Thu, 17 Oct 2024 11:04:23 -0700 Subject: [PATCH 6/6] Add some missing coverage --- gregor_django/users/tests/test_views.py | 56 ++++++++++++++++++++++++- gregor_django/users/views.py | 1 - 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/gregor_django/users/tests/test_views.py b/gregor_django/users/tests/test_views.py index e0d8431a..e1611657 100644 --- a/gregor_django/users/tests/test_views.py +++ b/gregor_django/users/tests/test_views.py @@ -1,19 +1,26 @@ import json import pytest +from allauth.socialaccount.models import SocialApp from anvil_consortium_manager.models import AnVILProjectManagerAccess -from anvil_consortium_manager.tests.factories import AccountFactory +from anvil_consortium_manager.tests.factories import AccountFactory, UserEmailEntryFactory from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser, Permission from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.sites.models import Site from django.http import HttpRequest from django.shortcuts import resolve_url from django.test import RequestFactory, TestCase from django.urls import reverse +from gregor_django.drupal_oauth_provider.provider import CustomProvider +from gregor_django.gregor_anvil.tests.factories import ( + PartnerGroupFactory, + ResearchCenterFactory, +) from gregor_django.users.forms import UserChangeForm, UserLookupForm from gregor_django.users.tests.factories import UserFactory from gregor_django.users.views import UserRedirectView, UserUpdateView, user_detail_view @@ -92,13 +99,42 @@ def test_get_redirect_url(self, user: User, rf: RequestFactory): class TestUserDetailView: - def test_authenticated(self, client, user: User, rf: RequestFactory): + def test_authenticated(self, client, user: User): client.force_login(user) user_detail_url = reverse("users:detail", kwargs=dict(username=user.username)) response = client.get(user_detail_url) assert response.status_code == 200 + def test_configured_user_with_anvil_account(self, client): + account = AccountFactory.create(verified=True) + account_user = account.user + client.force_login(account_user) + pg1 = PartnerGroupFactory(short_name="pg1") + rc1 = ResearchCenterFactory(short_name="rc1") + + account.save() + account_user.partner_groups.add(pg1) + account_user.research_centers.add(rc1) + user_detail_url = reverse("users:detail", kwargs=dict(username=account_user.username)) + response = client.get(user_detail_url) + + assert response.status_code == 200 + + def test_configured_users_no_anvil_account(self, client, user: User): + client.force_login(user) + pg1 = PartnerGroupFactory(short_name="pg1") + rc1 = ResearchCenterFactory(short_name="rc1") + UserEmailEntryFactory.create(user=user) + + user.partner_groups.add(pg1) + user.research_centers.add(rc1) + user_detail_url = reverse("users:detail", kwargs=dict(username=user.username)) + + response = client.get(user_detail_url) + + assert response.status_code == 200 + def test_not_authenticated(self, user: User, rf: RequestFactory): request = rf.get("/fake-url/") request.user = AnonymousUser() @@ -129,6 +165,22 @@ def test_unlinked_accounts(self): self.assertContains(response, "Previously-linked accounts") +class LoginViewTest(TestCase): + def setUp(self): + current_site = Site.objects.get_current() + self.social_app = SocialApp.objects.create( + provider=CustomProvider.id, + name="DOA", + client_id="test-client-id", + secret="test-client-secret", + ) + self.social_app.sites.add(current_site) + + def test_basic_login_view_render(self): + response = self.client.get(reverse("account_login")) + assert response.status_code == 200 + + class UserAutocompleteViewTest(TestCase): def setUp(self): """Set up test class.""" diff --git a/gregor_django/users/views.py b/gregor_django/users/views.py index e27834ab..ef0dc927 100644 --- a/gregor_django/users/views.py +++ b/gregor_django/users/views.py @@ -20,7 +20,6 @@ class UserDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["unlinked_accounts"] = self.object.unlinked_accounts.all() - print(context["unlinked_accounts"]) context["user_email_entries"] = self.object.useremailentry_set.filter(date_verified=None) context["is_me"] = self.request.user == self.object return context