From 112c81c0d2851faf152eb6e1d884f2417ca5e8db Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Wed, 12 Aug 2020 07:54:30 +0200 Subject: [PATCH] Add _legacy_get_session_auth_hash to fix Django 3.1 support --- .github/workflows/ci.yml | 33 +++++++++++++++---- mailauth/contrib/admin/views.py | 2 +- .../migrations/0002_emailuser_session_salt.py | 5 +-- .../migrations/0004_auto_20200812_0722.py | 21 ++++++++++++ mailauth/contrib/user/models.py | 26 +++++++++++++-- setup.cfg | 1 - tests/conftest.py | 5 ++- tests/contrib/auth/test_models.py | 27 +++++++++++++++ tests/contrib/wagtail/test_views.py | 7 +++- tests/test_backends.py | 2 +- tests/test_models.py | 11 +++++++ tests/testapp/settings.py | 17 +++++++--- tests/testapp/urls.py | 13 ++++++-- 13 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 mailauth/contrib/user/migrations/0004_auto_20200812_0722.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd87235..a6e4047 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,8 +4,8 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v1.1.1 - - uses: actions/checkout@v2.0.0 + - uses: actions/setup-python@v2 + - uses: actions/checkout@v2 - run: python -m pip install -r requirements.txt - run: python setup.py develop - run: python setup.py build_sphinx -W @@ -18,16 +18,37 @@ jobs: python-version: [3.6, 3.7, 3.8] steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1.1.1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - uses: actions/checkout@v2.0.0 + - uses: actions/checkout@v2 - run: python setup.py test - name: Codecov run: | python -m pip install codecov codecov -t ${{secrets.CODECOV_TOKEN}} + extras: + needs: [docs] + runs-on: ubuntu-latest + strategy: + matrix: + extras: + - wagtail + python-version: [3.8] + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v2 + - run: python -m pip install -e ".[${{ matrix.extras }}]" + - run: python setup.py test + - name: Codecov + run: | + python -m pip install codecov + codecov -t ${{ secrets.CODECOV_TOKEN }} + PostgreSQL: needs: [docs] @@ -50,10 +71,10 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1.1.1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - uses: actions/checkout@v2.0.0 + - uses: actions/checkout@v2 - run: python -m pip install psycopg2-binary Django==${{ matrix.django-version }} - run: python setup.py test env: diff --git a/mailauth/contrib/admin/views.py b/mailauth/contrib/admin/views.py index 8311e87..b26cd73 100644 --- a/mailauth/contrib/admin/views.py +++ b/mailauth/contrib/admin/views.py @@ -1,6 +1,6 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from mailauth.views import LoginView diff --git a/mailauth/contrib/user/migrations/0002_emailuser_session_salt.py b/mailauth/contrib/user/migrations/0002_emailuser_session_salt.py index b7efd02..a9bd9cc 100644 --- a/mailauth/contrib/user/migrations/0002_emailuser_session_salt.py +++ b/mailauth/contrib/user/migrations/0002_emailuser_session_salt.py @@ -1,8 +1,9 @@ # Generated by Django 2.2.1 on 2019-05-27 10:39 -import django.utils.crypto from django.db import migrations, models +import mailauth + class Migration(migrations.Migration): @@ -14,6 +15,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='emailuser', name='session_salt', - field=models.CharField(default=django.utils.crypto.get_random_string, editable=False, max_length=12), + field=models.CharField(default=mailauth.contrib.user.models._get_session_salt, editable=False, max_length=12), ), ] diff --git a/mailauth/contrib/user/migrations/0004_auto_20200812_0722.py b/mailauth/contrib/user/migrations/0004_auto_20200812_0722.py new file mode 100644 index 0000000..a523d25 --- /dev/null +++ b/mailauth/contrib/user/migrations/0004_auto_20200812_0722.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1 on 2020-08-12 07:22 + +import django +from django.db import migrations, models + +import mailauth.contrib.user.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailauth_user', '0003_ci_unique_index'), + ] + + operations = [ + migrations.AlterField( + model_name='emailuser', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + ] diff --git a/mailauth/contrib/user/models.py b/mailauth/contrib/user/models.py index 3069107..20b94c0 100644 --- a/mailauth/contrib/user/models.py +++ b/mailauth/contrib/user/models.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth.base_user import BaseUserManager from django.contrib.auth.models import AbstractUser from django.db import models @@ -37,6 +38,10 @@ def create_superuser(self, email, **extra_fields): return self._create_user(email, **extra_fields) +def _get_session_salt(): + return get_random_string(12) + + class AbstractEmailUser(AbstractUser): EMAIL_FIELD = 'email' USERNAME_FIELD = 'email' @@ -51,7 +56,7 @@ class AbstractEmailUser(AbstractUser): # Salt for the session hash replacing the password in this function. session_salt = models.CharField( max_length=12, editable=False, - default=get_random_string, + default=_get_session_salt, ) def has_usable_password(self): @@ -62,12 +67,29 @@ def has_usable_password(self): class Meta(AbstractUser.Meta): abstract = True + def _legacy_get_session_auth_hash(self): + # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid. + key_salt = "mailauth.contrib.user.models.EmailUserManager.get_session_auth_hash" + if not self.session_salt: + raise ValueError("'session_salt' must be set") + return salted_hmac(key_salt, self.session_salt, algorithm='sha1').hexdigest() + def get_session_auth_hash(self): """Return an HMAC of the :attr:`.session_salt` field.""" key_salt = "mailauth.contrib.user.models.EmailUserManager.get_session_auth_hash" if not self.session_salt: raise ValueError("'session_salt' must be set") - return salted_hmac(key_salt, self.session_salt).hexdigest() + algorithm = getattr(settings, 'DEFAULT_HASHING_ALGORITHM', None) + if algorithm is None: + return salted_hmac(key_salt, self.session_salt).hexdigest() + return salted_hmac( + key_salt, + self.session_salt, + # RemovedInDjango40Warning: when the deprecation ends, replace + # with: + # algorithm='sha256', + algorithm=algorithm, + ).hexdigest() delattr(AbstractEmailUser, 'password') diff --git a/setup.cfg b/setup.cfg index f891b08..e8820e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,6 @@ tests_require = pytest pytest-django pytest-cov - wagtail [options.package_data] * = *.txt, *.rst, *.html, *.po diff --git a/tests/conftest.py b/tests/conftest.py index af6e74f..f13b80c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import django import pytest from django.contrib.auth import get_user_model from django.utils import timezone @@ -36,7 +37,9 @@ def admin_user(db): @pytest.fixture() def signature(): """Return a signature matching the user fixture.""" - return 'LZ.173QUS.1Hjptg.lf2hFgOXQtjQsFypS2ItRG2hkpA' + if django.VERSION < (3, 1): + return 'LZ.173QUS.1Hjptg.lf2hFgOXQtjQsFypS2ItRG2hkpA' + return 'LZ.173QUS.1Hjptg.UtFdkTPoyrSA0IB6AUEhtz_hMyFZY0kcREE1HnWdFq4' @pytest.fixture() diff --git a/tests/contrib/auth/test_models.py b/tests/contrib/auth/test_models.py index a947dec..81ad111 100644 --- a/tests/contrib/auth/test_models.py +++ b/tests/contrib/auth/test_models.py @@ -1,3 +1,4 @@ +import django import pytest from django.core.exceptions import FieldDoesNotExist @@ -28,6 +29,32 @@ def test_get_session_auth_hash__unique(self, db): assert spiderman.get_session_auth_hash() != ironman.get_session_auth_hash() + @pytest.mark.skipif(django.VERSION < (3, 1), reason="requires Django 3.1 or higher") + def test_legacy_get_session_auth_hash__default(self, db): + user = EmailUser(email='spiderman@avengers.com') + + assert user.session_salt + assert user._legacy_get_session_auth_hash() + + @pytest.mark.skipif(django.VERSION < (3, 1), reason="requires Django 3.1 or higher") + def test_legacy_get_session_auth_hash__value_error(self, db): + user = EmailUser(email='spiderman@avengers.com', session_salt=None) + + with pytest.raises(ValueError) as e: + user._legacy_get_session_auth_hash() + + assert "'session_salt' must be set" in str(e.value) + + @pytest.mark.skipif(django.VERSION < (3, 1), reason="requires Django 3.1 or higher") + def test_legacy_get_session_auth_hash__unique(self, db): + spiderman = EmailUser(email='spiderman@avengers.com') + ironman = EmailUser(email='ironman@avengers.com') + + assert ( + spiderman._legacy_get_session_auth_hash() != + ironman._legacy_get_session_auth_hash() + ) + def test_password_field(self): user = EmailUser(email='spiderman@avengers.com') with pytest.raises(FieldDoesNotExist): diff --git a/tests/contrib/wagtail/test_views.py b/tests/contrib/wagtail/test_views.py index f08b4fe..fcd3d0e 100644 --- a/tests/contrib/wagtail/test_views.py +++ b/tests/contrib/wagtail/test_views.py @@ -1,12 +1,17 @@ -from mailauth.contrib.wagtail.views import LoginView +import pytest + from mailauth.forms import EmailLoginForm class TestLoginView: def test_get_from_class(self): + pytest.importorskip('wagtail') + from mailauth.contrib.wagtail.views import LoginView assert issubclass(LoginView().get_form_class(), EmailLoginForm) def test_form_valid(self, rf, db): + pytest.importorskip('wagtail') + from mailauth.contrib.wagtail.views import LoginView view = LoginView() request = rf.get('/') diff --git a/tests/test_backends.py b/tests/test_backends.py index db03636..6eed6de 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -62,5 +62,5 @@ def test_get_login_url(self, signer, signature): backend = MailAuthBackend() MailAuthBackend.signer = signer assert backend.get_login_url(signature) == ( - "/accounts/login/LZ.173QUS.1Hjptg.lf2hFgOXQtjQsFypS2ItRG2hkpA" + f"/accounts/login/{signature}" ) diff --git a/tests/test_models.py b/tests/test_models.py index 82ed8c0..266064f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -3,9 +3,20 @@ from mailauth.contrib.user import models +try: + import psycopg2 +except ImportError: + psycopg2 = None + + +postgres_only = pytest.mark.skipif( + psycopg2 is None, reason="at least mymodule-1.1 required" +) + class TestEmailUser: + @postgres_only def test_email__ci_unique(self, db): models.EmailUser.objects.create_user('IronMan@avengers.com') with pytest.raises(IntegrityError): diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py index 5071a2d..ffcedba 100644 --- a/tests/testapp/settings.py +++ b/tests/testapp/settings.py @@ -41,12 +41,21 @@ 'mailauth', 'mailauth.contrib.admin', 'mailauth.contrib.user', - 'mailauth.contrib.wagtail', - 'wagtail.admin', - 'wagtail.core', - 'wagtail', + ] +try: + import wagtail # NoQA +except ImportError: + pass +else: + INSTALLED_APPS += [ + 'mailauth.contrib.wagtail', + 'wagtail.admin', + 'wagtail.core', + 'wagtail', + ] + AUTHENTICATION_BACKENDS = ( 'mailauth.backends.MailAuthBackend', ) diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py index 9cfa58b..9bdfa66 100644 --- a/tests/testapp/urls.py +++ b/tests/testapp/urls.py @@ -1,11 +1,18 @@ from django.contrib import admin from django.urls import include, path -from wagtail.admin import urls as wagtailadmin_urls urlpatterns = [ path('accounts/', include('mailauth.urls')), - path("", include("mailauth.contrib.wagtail.urls")), path("django-admin/", admin.site.urls), - path("admin/", include(wagtailadmin_urls)), ] + +try: + from wagtail.admin import urls as wagtailadmin_urls +except ImportError: + pass +else: + urlpatterns += [ + path("", include("mailauth.contrib.wagtail.urls")), + path("admin/", include(wagtailadmin_urls)), + ]