Skip to content
Open
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
25 changes: 16 additions & 9 deletions openwisp_users/middleware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import redirect
from django.urls import resolve, reverse_lazy
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from rest_framework.views import APIView


class PasswordExpirationMiddleware:
Expand All @@ -25,14 +26,20 @@ def __init__(self, get_response):
def __call__(self, request):
response = self.get_response(request)
# Check if the user is authenticated and their password has expired
if (
request.user.is_authenticated
and request.user.has_password_expired()
# We use `resolve()` here to get the `url_name` from the `request.path`.
# This is more flexible than using `reverse()` as it doesn't require
# passing arguments to get the correct path.
and resolve(request.path).url_name not in self.exempted_url_names
):
if not (request.user.is_authenticated and request.user.has_password_expired()):
return response
# `request.resolver_match` is already populated by Django while handling
# the request, no need to call `resolve()` again (which would raise
# `Resolver404` for genuinely unmatched paths).
url_match = request.resolver_match
if url_match is None:
return response
# DRF sets `cls` on the view function returned by `APIView.as_view()`,
# regular Django views don't have this attribute, so this reliably
# tells apart API requests, which must not be redirected to an HTML
# page since API clients expect a proper API response.
is_api_request = issubclass(getattr(url_match.func, "cls", object), APIView)
if not is_api_request and url_match.url_name not in self.exempted_url_names:
messages.warning(
request,
_("Your password has expired, please update your password."),
Expand Down
24 changes: 23 additions & 1 deletion openwisp_users/tests/test_middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
from django.utils.timezone import now, timedelta

from .. import settings as app_settings
from .test_api import AuthenticationMixin
from .utils import TestOrganizationMixin

User = get_user_model()


class TestPasswordExpirationMiddleware(TestOrganizationMixin, TestCase):
class TestPasswordExpirationMiddleware(
TestOrganizationMixin, AuthenticationMixin, TestCase
):
@modify_settings(
MIDDLEWARE={
"remove": ["openwisp_users.middleware.PasswordExpirationMiddleware"]
Expand Down Expand Up @@ -47,3 +50,22 @@ def test_queries_middleware_present(self):
self.assertEqual(response.url, "/accounts/password/change/?next=/admin/")
with self.assertNumQueries(1):
self.client.force_login(admin)

@modify_settings(
MIDDLEWARE={
"append": ["openwisp_users.middleware.PasswordExpirationMiddleware"]
}
)
@patch.object(app_settings, "STAFF_USER_PASSWORD_EXPIRATION", 10)
def test_api_request_not_redirected(self):
admin = self._create_admin(
username="apiuser",
password="tester",
password_updated=now().date() - timedelta(days=180),
)
token = self._obtain_auth_token(username="apiuser", password="tester")
response = self.client.get(
reverse("users:user_detail", args=(admin.pk,)),
HTTP_AUTHORIZATION=f"Bearer {token}",
)
self.assertEqual(response.status_code, 200)
Loading