Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.8", "3.9", "3.10", "3.11"]
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4

Expand Down
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.8.0 (2026-02-05)
==================
- bump required version of django-otp and update middleware


1.7.1 (2025-07-24)
=================
- Update python release workflow to use Python 3.11
Expand Down
40 changes: 19 additions & 21 deletions sandbox/sandbox/apps/user/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,29 @@ class UserAdmin(BaseUserAdmin):
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ['email']
list_filter = ['is_superuser']
list_display = ["email"]
list_filter = ["is_superuser"]

fieldsets = (
(None, {
'fields': ['email', 'password']
}),
('Personal info', {
'fields': ['first_name', 'last_name']
}),
('Permissions', {
'fields': [
'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions'
]
}),
(None, {"fields": ["email", "password"]}),
("Personal info", {"fields": ["first_name", "last_name"]}),
(
"Permissions",
{
"fields": [
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
]
},
),
)
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ['email', 'password1', 'password2']
}),
(None, {"classes": ("wide",), "fields": ["email", "password1", "password2"]}),
)
search_fields = ['first_name', 'last_name', 'email']
ordering = ['email']
search_fields = ["first_name", "last_name", "email"]
ordering = ["email"]
filter_horizontal = []
143 changes: 66 additions & 77 deletions sandbox/sandbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
DEBUG = True

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'VnFOCFWNnyeYRsyjerSCvxOiKf0fvH57ajVBZIG8ie4xlSTLAzRkj7aYYEqJAKpR'
SECRET_KEY = "VnFOCFWNnyeYRsyjerSCvxOiKf0fvH57ajVBZIG8ie4xlSTLAzRkj7aYYEqJAKpR"


EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
Expand All @@ -33,100 +33,89 @@
# Application definition

INSTALLED_APPS = [

'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail',
# 'wagtail_modeladmin', # if Wagtail >=5.1; Don't repeat if it's there already
'wagtail.contrib.modeladmin', # if Wagtail <5.1; Don't repeat if it's there already
'wagtail.contrib.styleguide',

'modelcluster',
'taggit',
'debug_toolbar',

'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'wagtail_2fa',
'django_otp',
'django_otp.plugins.otp_totp',

'sandbox.apps.home',
'sandbox.apps.user',

"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.embeds",
"wagtail.sites",
"wagtail.users",
"wagtail.snippets",
"wagtail.documents",
"wagtail.images",
"wagtail.search",
"wagtail.admin",
"wagtail",
"wagtail.contrib.styleguide",
"modelcluster",
"taggit",
"debug_toolbar",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"wagtail_2fa",
"django_otp",
"django_otp.plugins.otp_totp",
"sandbox.apps.home",
"sandbox.apps.user",
]

MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',

'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',

'wagtail_2fa.middleware.VerifyUserPermissionsMiddleware',

'wagtail.contrib.redirects.middleware.RedirectMiddleware',
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"wagtail_2fa.middleware.VerifyUserPermissionsMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
]

ROOT_URLCONF = 'sandbox.urls'
ROOT_URLCONF = "sandbox.urls"

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(PROJECT_DIR, 'templates'),
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
os.path.join(PROJECT_DIR, "templates"),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]

WSGI_APPLICATION = 'sandbox.wsgi.application'
WSGI_APPLICATION = "sandbox.wsgi.application"

AUTH_USER_MODEL = 'user.User'
AUTH_USER_MODEL = "user.User"


# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "db.sqlite3",
}
}


# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/

LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"

TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"

USE_I18N = True

Expand All @@ -139,19 +128,19 @@
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]

STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
os.path.join(PROJECT_DIR, "static"),
]

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL = "/static/"

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"


# Wagtail settings
Expand All @@ -161,11 +150,11 @@
# Base URL to use when referring to full URLs within the Wagtail admin backend -
# e.g. in notification emails. Don't include '/admin' or a trailing slash

WAGTAILADMIN_BASE_URL = 'http://example.com'
WAGTAILADMIN_BASE_URL = "http://example.com"

INTERNAL_IPS = ['127.0.0.1']
INTERNAL_IPS = ["127.0.0.1"]


WAGTAIL_2FA_REQUIRED = True

DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
22 changes: 11 additions & 11 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from setuptools import find_packages, setup

install_requires = [
"Django>=3.2",
"Wagtail>=4.1",
"django-otp>=0.8.1",
"Django>=5.2",
"Wagtail>=7.0",
"django-otp>=1.7.0",
"six>=1.14.0",
"qrcode>=6.1",
]
Expand All @@ -17,14 +17,14 @@

tests_require = [
"coverage==5.5",
"pytest==7.2.2",
"pytest-cov==2.12.1",
"pytest-django==4.4.0",
"pytest>=7.2.2",
"pytest-cov>=2.12.1",
"pytest-django>=4.4.0",
# Linting
"flake8==3.9.2", # 3.7.9
"isort==5.12.0",
"flake8-blind-except==0.2.0",
"flake8-debugger==4.0.0",
"flake8>=3.9.2", # 3.7.9
"isort>=5.12.0",
"flake8-blind-except>=0.2.0",
"flake8-debugger>=4.0.0",
]

with open("README.rst") as fh:
Expand All @@ -46,7 +46,7 @@
"docs": docs_require,
"test": tests_require,
},
python_requires=">=3.8",
python_requires=">=3.10",
use_scm_version=True,
entry_points={},
package_dir={"": "src"},
Expand Down
2 changes: 1 addition & 1 deletion src/wagtail_2fa/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __call__(self, request):
def process_request(self, request):
if request.user:
request.user = SimpleLazyObject(
partial(self._verify_user, request, request.user)
partial(self._verify_user_sync, request, request.user)
)
user = request.user
if self._require_verified_user(request):
Expand Down
18 changes: 12 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from unittest import mock
from django.conf import settings


Expand Down Expand Up @@ -29,8 +30,6 @@ def pytest_configure():
"wagtail.search",
"wagtail.admin",
"wagtail",
# "wagtail_modeladmin", # if Wagtail >=5.1; Don't repeat if it's there already
"wagtail.contrib.modeladmin", # if Wagtail <5.1; Don't repeat if it's there already
"modelcluster",
"taggit",
"django.contrib.admin",
Expand Down Expand Up @@ -99,18 +98,25 @@ def user(django_user_model):


@pytest.fixture
def verified_user(django_user_model, rf):
def otpmiddleware():
from django_otp.middleware import OTPMiddleware

get_response = mock.MagicMock()
return OTPMiddleware(get_response)


@pytest.fixture
def verified_user(django_user_model, rf, otpmiddleware):
"""Create a user and verify it using the OTP middleware. Add a device
to complete the verification for the user."""
from django_otp.middleware import OTPMiddleware as _OTPMiddleware
from django_otp.plugins.otp_totp.models import TOTPDevice

user = django_user_model.objects.create(username="verified-user")
device = TOTPDevice.objects.create(user=user, confirmed=True)
request = rf.get("/foo/")
request.user = user
middleware = _OTPMiddleware()
user = middleware._verify_user(request, user)
middleware = otpmiddleware
user = middleware._verify_user_sync(request, user)
user.otp_device = device
return user

Expand Down
7 changes: 3 additions & 4 deletions tests/test_hooks.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
from django.test import override_settings
from django_otp import user_has_device
from django_otp.middleware import OTPMiddleware as _OTPMiddleware
from wagtail.admin.menu import MenuItem

from wagtail_2fa.wagtail_hooks import remove_menu_if_unverified


class TestHooks:
def test_remove_menu_if_unverified(self, user, rf):
def test_remove_menu_if_unverified(self, user, rf, otpmiddleware):
with override_settings(WAGTAIL_2FA_REQUIRED=True):
request = rf.get("/cms/")
request.user = user
middleware = _OTPMiddleware()
user = middleware._verify_user(request, user)
middleware = otpmiddleware
user = middleware._verify_user_sync(request, user)
assert not user_has_device(user)
assert user.is_authenticated

Expand Down
Loading