diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index ef280e58..0f9a1634 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -15,20 +15,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11"]
- django-version: ["3.2", "4.1", "4.2", "5.0"]
+ python-version: ["3.10", "3.11", "3.12"]
+ django-version: ["4.2", "5.0", "5.1"]
include:
- - python-version: "3.12"
- django-version: "4.2"
- - python-version: "3.12"
- django-version: "5.0"
- exclude:
- - python-version: "3.11"
- django-version: "3.2"
- - python-version: "3.8"
- django-version: "5.0"
- python-version: "3.9"
- django-version: "5.0"
+ django-version: "4.2"
+ - python-version: "3.13"
+ django-version: "5.1"
steps:
- uses: actions/checkout@v4
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a662b442..c3e6be30 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: 'docs|migrations'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -13,37 +13,37 @@ repos:
- id: debug-statements
- repo: https://github.com/asottile/pyupgrade
- rev: v2.34.0
+ rev: v3.19.0
hooks:
- id: pyupgrade
- args: [--py37-plus]
+ args: [--py39-plus]
- repo: https://github.com/myint/autoflake
- rev: 'v1.4'
+ rev: 'v2.3.1'
hooks:
- id: autoflake
args: ['--in-place', '--remove-all-unused-imports', '--ignore-init-module-imports']
- repo: https://github.com/pycqa/isort
- rev: 5.10.1
+ rev: 5.13.2
hooks:
- id: isort
name: isort (python)
args: ['--settings-path=pyproject.toml']
- repo: https://github.com/psf/black
- rev: 22.6.0
+ rev: 24.10.0
hooks:
- id: black
- repo: https://github.com/adamchainz/django-upgrade
- rev: 1.7.0
+ rev: 1.22.2
hooks:
- id: django-upgrade
- args: [--target-version, "3.2"]
+ args: [--target-version, "4.2"]
- repo: https://github.com/pycqa/flake8
- rev: 4.0.1
+ rev: 7.1.1
hooks:
- id: flake8
args: ['--config=setup.cfg']
diff --git a/djangosaml2/backends.py b/djangosaml2/backends.py
index d1c01407..06b8bf8f 100644
--- a/djangosaml2/backends.py
+++ b/djangosaml2/backends.py
@@ -15,7 +15,7 @@
import logging
import warnings
-from typing import Any, Optional, Tuple
+from typing import Any, Optional
from django.apps import apps
from django.conf import settings
@@ -72,7 +72,7 @@ def _user_lookup_attribute(self) -> str:
def _extract_user_identifier_params(
self, session_info: dict, attributes: dict, attribute_mapping: dict
- ) -> Tuple[str, Optional[Any]]:
+ ) -> tuple[str, Optional[Any]]:
"""Returns the attribute to perform a user lookup on, and the value to use for it.
The value could be the name_id, or any other saml attribute from the request.
"""
@@ -262,7 +262,7 @@ def get_or_create_user(
attributes: dict,
attribute_mapping: dict,
request,
- ) -> Tuple[Optional[settings.AUTH_USER_MODEL], bool]:
+ ) -> tuple[Optional[settings.AUTH_USER_MODEL], bool]:
"""Look up the user to authenticate. If he doesn't exist, this method creates him (if so desired).
The default implementation looks only at the user_identifier. Override this method in order to do more complex behaviour,
e.g. customize this per IdP.
@@ -322,6 +322,7 @@ def get_attribute_value(self, django_field, attributes, attribute_mapping):
warnings.warn(
"get_attribute_value() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return self._get_attribute_value(django_field, attributes, attribute_mapping)
@@ -329,6 +330,7 @@ def get_django_user_main_attribute(self):
warnings.warn(
"get_django_user_main_attribute() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return self._user_lookup_attribute
@@ -336,6 +338,7 @@ def get_django_user_main_attribute_lookup(self):
warnings.warn(
"get_django_user_main_attribute_lookup() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return getattr(settings, "SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP", "")
@@ -343,6 +346,7 @@ def get_user_query_args(self, main_attribute):
warnings.warn(
"get_user_query_args() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return {
self.get_django_user_main_attribute()
@@ -353,6 +357,7 @@ def configure_user(self, user, attributes, attribute_mapping):
warnings.warn(
"configure_user() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return self._update_user(user, attributes, attribute_mapping)
@@ -360,6 +365,7 @@ def update_user(self, user, attributes, attribute_mapping, force_save=False):
warnings.warn(
"update_user() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return self._update_user(user, attributes, attribute_mapping)
@@ -367,6 +373,7 @@ def _set_attribute(self, obj, attr, value):
warnings.warn(
"_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return set_attribute(obj, attr, value)
@@ -375,5 +382,6 @@ def get_saml_user_model():
warnings.warn(
"_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it",
DeprecationWarning,
+ stacklevel=2,
)
return Saml2Backend()._user_model
diff --git a/djangosaml2/overrides.py b/djangosaml2/overrides.py
index f2b8ce34..8f6bc6bf 100644
--- a/djangosaml2/overrides.py
+++ b/djangosaml2/overrides.py
@@ -19,9 +19,9 @@ class Saml2Client(saml2.client.Saml2Client):
def do_logout(self, *args, **kwargs):
if not kwargs.get("expected_binding"):
try:
- kwargs[
- "expected_binding"
- ] = settings.SAML_LOGOUT_REQUEST_PREFERRED_BINDING
+ kwargs["expected_binding"] = (
+ settings.SAML_LOGOUT_REQUEST_PREFERRED_BINDING
+ )
except AttributeError:
logger.warning(
"SAML_LOGOUT_REQUEST_PREFERRED_BINDING setting is"
diff --git a/djangosaml2/tests/__init__.py b/djangosaml2/tests/__init__.py
index 8b8602cc..56dc7aeb 100644
--- a/djangosaml2/tests/__init__.py
+++ b/djangosaml2/tests/__init__.py
@@ -139,9 +139,9 @@ def add_outstanding_query(self, session_id, came_from):
came_from,
)
self.saml_session.save()
- self.client.cookies[
- settings.SESSION_COOKIE_NAME
- ] = self.saml_session.session_key
+ self.client.cookies[settings.SESSION_COOKIE_NAME] = (
+ self.saml_session.session_key
+ )
def b64_for_post(self, xml_text, encoding="utf-8"):
return base64.b64encode(xml_text.encode(encoding)).decode("ascii")
@@ -308,8 +308,12 @@ def test_unknown_idp(self):
metadata_file="remote_metadata_three_idps.xml",
)
- response = self.client.get(reverse("saml2_login") + "?idp=https://unknown.org")
- self.assertContains(response, "<b>https://unknown.org</b>", status_code=403)
+ response = self.client.get(
+ reverse("saml2_login") + "?idp=https://unknown.org"
+ )
+ self.assertContains(
+ response, "<b>https://unknown.org</b>", status_code=403
+ )
def test_login_authn_context(self):
sp_kwargs = {
diff --git a/djangosaml2/utils.py b/djangosaml2/utils.py
index e13182a0..3426ec16 100644
--- a/djangosaml2/utils.py
+++ b/djangosaml2/utils.py
@@ -47,7 +47,7 @@ def available_idps(config: SPConfig, langpref=None, idp_to_check: str = None) ->
for metadata in config.metadata.metadata.values():
# initiate a fetch to the selected idp when using MDQ, otherwise the MetaDataMDX is an empty database
if isinstance(metadata, MetaDataMDX) and idp_to_check:
- m = metadata[idp_to_check]
+ m = metadata[idp_to_check] # noqa: F841
result = metadata.any("idpsso_descriptor", "single_sign_on_service")
if result:
idps.update(result.keys())
@@ -108,11 +108,7 @@ def validate_referral_url(request, url):
# SAML_STRICT_URL_VALIDATION setting can be used to turn off this check.
# This should only happen if there is no slash, host and/or protocol in the
# given URL. A better fix would be to add those to the RelayState.
- saml_strict_url_validation = getattr(
- settings,
- "SAML_STRICT_URL_VALIDATION",
- True
- )
+ saml_strict_url_validation = getattr(settings, "SAML_STRICT_URL_VALIDATION", True)
try:
if saml_strict_url_validation:
# This will also resolve Django URL pattern names
@@ -133,8 +129,7 @@ def validate_referral_url(request, url):
)
if not url_has_allowed_host_and_scheme(url=url, allowed_hosts=saml_allowed_hosts):
- logger.debug("Referral URL not in SAML_ALLOWED_HOSTS or of the origin "
- "host.")
+ logger.debug("Referral URL not in SAML_ALLOWED_HOSTS or of the origin host.")
return None
return url
@@ -210,7 +205,7 @@ def add_idp_hinting(request, http_response) -> bool:
return False
-@lru_cache()
+@lru_cache
def get_csp_handler():
"""Returns a view decorator for CSP."""
diff --git a/djangosaml2/views.py b/djangosaml2/views.py
index 2bf82f86..245caa12 100644
--- a/djangosaml2/views.py
+++ b/djangosaml2/views.py
@@ -135,7 +135,7 @@ def get_state_client(self, request: HttpRequest):
return state, client
-@method_decorator(saml2_csp_update, name='dispatch')
+@method_decorator(saml2_csp_update, name="dispatch")
class LoginView(SPConfigMixin, View):
"""SAML Authorization Request initiator.
@@ -389,7 +389,7 @@ def get(self, request, *args, **kwargs):
except TemplateDoesNotExist as e:
logger.debug(
f"TemplateDoesNotExist: [{self.post_binding_form_template}] - {e}",
- exc_info=True
+ exc_info=True,
)
if not http_response:
@@ -574,7 +574,7 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
session_info,
attribute_mapping,
create_unknown_user,
- assertion_info
+ assertion_info,
)
except PermissionDenied as e:
return self.handle_acs_failure(
@@ -592,7 +592,7 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
if not relay_state:
logger.debug(
"RelayState is not a valid URL, redirecting to fallback: %s",
- relay_state
+ relay_state,
)
return HttpResponseRedirect(get_fallback_login_redirect_url())
@@ -600,13 +600,13 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
return HttpResponseRedirect(relay_state)
def authenticate_user(
- self,
- request,
- session_info,
- attribute_mapping,
- create_unknown_user,
- assertion_info
- ):
+ self,
+ request,
+ session_info,
+ attribute_mapping,
+ create_unknown_user,
+ assertion_info,
+ ):
"""Calls Django's authenticate method after the SAML response is verified"""
logger.debug("Trying to authenticate the user. Session info: %s", session_info)
@@ -685,7 +685,7 @@ def get(self, request, *args, **kwargs):
)
-@method_decorator(saml2_csp_update, name='dispatch')
+@method_decorator(saml2_csp_update, name="dispatch")
class LogoutInitView(LoginRequiredMixin, SPConfigMixin, View):
"""SAML Logout Request initiator
@@ -801,7 +801,9 @@ def do_logout_service(self, request, data, binding, *args, **kwargs):
)
except StatusError as e:
response = None
- logger.warning(f"Error logging out from remote provider: {e}", exc_info=True)
+ logger.warning(
+ f"Error logging out from remote provider: {e}", exc_info=True
+ )
state.sync()
return finish_logout(request, response)
@@ -853,13 +855,16 @@ def finish_logout(request, response):
return HttpResponseRedirect(next_path)
elif settings.LOGOUT_REDIRECT_URL is not None:
fallback_url = resolve_url(settings.LOGOUT_REDIRECT_URL)
- logger.debug("No valid RelayState found; Redirecting to "
- "LOGOUT_REDIRECT_URL")
+ logger.debug(
+ "No valid RelayState found; Redirecting to " "LOGOUT_REDIRECT_URL"
+ )
return HttpResponseRedirect(fallback_url)
else:
current_site = get_current_site(request)
- logger.debug("No valid RelayState or LOGOUT_REDIRECT_URL found, "
- "rendering fallback template.")
+ logger.debug(
+ "No valid RelayState or LOGOUT_REDIRECT_URL found, "
+ "rendering fallback template."
+ )
return render(
request,
"registration/logged_out.html",
diff --git a/pyproject.toml b/pyproject.toml
index 5d906c1e..28b8614c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.black]
force-exclude = '''/(migrations)/'''
-target-version = ["py36"]
+target-version = ["py39"]
[tool.isort]
src_paths = ["djangosaml2", "tests"]
diff --git a/setup.py b/setup.py
index 267921a2..64dbe111 100644
--- a/setup.py
+++ b/setup.py
@@ -35,19 +35,18 @@ def read(*rnames):
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
- "Framework :: Django :: 3.2",
- "Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
+ "Framework :: Django :: 5.1",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: WSGI",
"Topic :: Security",
@@ -63,5 +62,5 @@ def read(*rnames):
packages=find_packages(exclude=["tests", "tests.*"]),
include_package_data=True,
zip_safe=False,
- install_requires=["defusedxml>=0.4.1", "Django>=3.2", "pysaml2>=6.5.1"],
+ install_requires=["defusedxml>=0.4.1", "Django>=4.2", "pysaml2>=6.5.1"],
)
diff --git a/tests/settings.py b/tests/settings.py
index 58a2e28c..ca741c77 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -104,8 +104,6 @@
USE_I18N = True
-USE_L10N = True
-
USE_TZ = True
diff --git a/tox.ini b/tox.ini
index 06f57b11..ccfc3f7d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
envlist =
- py{3.8,3.9,3.10,3.11,3.12}-django{3.2,4.1,4.2,5.0}
+ py{3.9,3.10,3.11,3.12,3.13}-django{4.2,5.0,5.1}
[testenv]
commands =
@@ -8,10 +8,9 @@ commands =
python tests/run_tests.py
deps =
- django3.2: django~=3.2
- django4.1: django~=4.1
django4.2: django~=4.2
- django5.0: django==5.0a1
+ django5.0: django~=5.0
+ django5.1: django~=5.1
djangomaster: https://github.com/django/django/archive/master.tar.gz
.