Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python x Django updates #410

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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
17 changes: 5 additions & 12 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']
Expand Down
14 changes: 11 additions & 3 deletions djangosaml2/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -322,27 +322,31 @@ 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)

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

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", "")

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()
Expand All @@ -353,20 +357,23 @@ 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)

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)

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)

Expand All @@ -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
6 changes: 3 additions & 3 deletions djangosaml2/overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 9 additions & 5 deletions djangosaml2/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -308,8 +308,12 @@ def test_unknown_idp(self):
metadata_file="remote_metadata_three_idps.xml",
)

response = self.client.get(reverse("saml2_login") + "?idp=<b>https://unknown.org</b>")
self.assertContains(response, "&lt;b&gt;https://unknown.org&lt;/b&gt;", status_code=403)
response = self.client.get(
reverse("saml2_login") + "?idp=<b>https://unknown.org</b>"
)
self.assertContains(
response, "&lt;b&gt;https://unknown.org&lt;/b&gt;", status_code=403
)

def test_login_authn_context(self):
sp_kwargs = {
Expand Down
13 changes: 4 additions & 9 deletions djangosaml2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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."""

Expand Down
39 changes: 22 additions & 17 deletions djangosaml2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -592,21 +592,21 @@ 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())

logger.debug("Redirecting to the RelayState: %s", relay_state)
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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.black]
force-exclude = '''/(migrations)/'''
target-version = ["py36"]
target-version = ["py39"]

[tool.isort]
src_paths = ["djangosaml2", "tests"]
Expand Down
7 changes: 3 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"],
)
2 changes: 0 additions & 2 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@

USE_I18N = True

USE_L10N = True

USE_TZ = True


Expand Down
Loading
Loading