mozilla-django-oidc-db
Advanced tools
+2
-82
@@ -5,4 +5,4 @@ ========= | ||
| 1.1.1 (2025-11-19) | ||
| ================== | ||
| 0.25.2 (2025-11-19) | ||
| =================== | ||
@@ -14,82 +14,2 @@ Hotfix release. | ||
| 1.1.0 (2025-11-18) | ||
| ================== | ||
| Minor release. | ||
| * [#157] Relocate shared test utility ``keycloak_login`` to ``mozilla_django_oidc_db/tests/utils`` | ||
| to make sure upstream projects can make use of it | ||
| 1.0.2 (2025-10-24) | ||
| ================== | ||
| Bugfix release - same patch as 1.0.1 but fixed some missed cases. | ||
| 1.0.1 (2025-10-24) | ||
| ================== | ||
| Bugfix release. | ||
| * Relaxed the user model inheritance check in the backend. | ||
| 1.0.0 (2025-10-23) | ||
| ================== | ||
| After a long time we feel the library is finally ready for a 1.0 version! | ||
| Releases 0.17.0 and 0.24.0 included a large rework of the architecture of the library, | ||
| which we considered essential before even thinking of a 1.0 version. Since then, we've | ||
| found no major issues and have adapted the library in a number of real projects in | ||
| production with varying degrees of complexity. | ||
| From now on, breaking changes will result in a major version bump. | ||
| This release itself contains some (technically) breaking changes, but we expect they won't | ||
| really affect you. | ||
| **💥 Breaking changes** | ||
| * Dropped support for Python 3.10 | ||
| * Dropped support for Python 3.11 | ||
| * Reworked types and classes used for the plugin system, in particular: | ||
| * Removed :class:`mozilla_django_oidc_db.plugins.OIDCBasePluginProtocol`, instead there is | ||
| an abstract base class :class:`mozilla_django_oidc_db.plugins.BaseOIDCPlugin`. | ||
| * Removed :class:`mozilla_django_oidc_db.plugins.BaseOIDCPlugin`, instead there is | ||
| :class:`mozilla_django_oidc_db.plugins.BaseOIDCPlugin`. | ||
| * Removed :class:`mozilla_django_oidc_db.plugins.AnonymousUserOIDCPluginProtocol`, | ||
| instead there is an abstract base class | ||
| :class:`mozilla_django_oidc_db.plugins.AnonymousUserOIDCPlugin`. | ||
| * Removed :class:`mozilla_django_oidc_db.plugins.AbstractUserOIDCPluginProtocol`, | ||
| instead there is an abstract base class | ||
| :class:`mozilla_django_oidc_db.plugins.AbstractUserOIDCPlugin`. | ||
| Typically now you should only be subclassing either ``AnonymousUserOIDCPlugin`` or | ||
| ``AbstractUserOIDCPlugin`` - they inherit from the abstract base class and provide | ||
| all necessary functionalities. | ||
| * The django-setup-configuration format appears to not be (fully) backwards compatible | ||
| since release 0.24.0. Downstream projects should mention this in their changelogs | ||
| and/or provide a migration path. | ||
| **New features** | ||
| * [`#121`_] Added Dutch translations. | ||
| **Bugfixes** | ||
| * [`#120`_] Fixed the retrieval of optional endpoints causing database errors. | ||
| * [`#113`_] Removed Open Forms reference in generic failure template. | ||
| **Project maintenance** | ||
| * [`#154`_] Improved documentation for setup-configuration integration. | ||
| * Improved the static type hints and added type-checking to the CI pipeline. | ||
| * Updated to modern Python syntax. | ||
| .. _#154: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/154 | ||
| .. _#120: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/120 | ||
| .. _#113: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/113 | ||
| .. _#121: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/121 | ||
| 0.25.1 (2025-08-25) | ||
@@ -96,0 +16,0 @@ =================== |
| Metadata-Version: 2.4 | ||
| Name: mozilla-django-oidc-db | ||
| Version: 1.1.1 | ||
| Version: 0.25.2 | ||
| Summary: A database-backed configuration for mozilla-django-oidc | ||
@@ -13,3 +13,3 @@ Author-email: Maykin Media <support@maykinmedia.nl> | ||
| Keywords: OIDC,django,database,authentication | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: Framework :: Django | ||
@@ -22,6 +22,8 @@ Classifier: Framework :: Django :: 4.2 | ||
| Classifier: Operating System :: Microsoft :: Windows | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Classifier: Topic :: Software Development :: Libraries :: Python Modules | ||
| Requires-Python: >=3.12 | ||
| Requires-Python: >=3.10 | ||
| Description-Content-Type: text/x-rst | ||
@@ -35,3 +37,3 @@ License-File: LICENSE | ||
| Provides-Extra: setup-configuration | ||
| Requires-Dist: django-setup-configuration>=0.11.0; extra == "setup-configuration" | ||
| Requires-Dist: django-setup-configuration>=0.8.2; extra == "setup-configuration" | ||
| Provides-Extra: tests | ||
@@ -49,4 +51,2 @@ Requires-Dist: psycopg; extra == "tests" | ||
| Requires-Dist: ruff; extra == "tests" | ||
| Requires-Dist: pyright; extra == "tests" | ||
| Requires-Dist: django-stubs; extra == "tests" | ||
| Provides-Extra: docs | ||
@@ -68,3 +68,3 @@ Requires-Dist: sphinx; extra == "docs" | ||
| :Version: 1.1.1 | ||
| :Version: 0.25.2 | ||
| :Source: https://github.com/maykinmedia/mozilla-django-oidc-db | ||
@@ -108,3 +108,3 @@ :Keywords: OIDC, django, database, authentication | ||
| .. |coverage| image:: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db/branch/master/graph/badge.svg | ||
| :target: https://app.codecov.io/gh/maykinmedia/mozilla-django-oidc-db | ||
| :target: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db | ||
| :alt: Coverage status | ||
@@ -111,0 +111,0 @@ |
@@ -15,3 +15,3 @@ django>=4.2 | ||
| [setup-configuration] | ||
| django-setup-configuration>=0.11.0 | ||
| django-setup-configuration>=0.8.2 | ||
@@ -30,3 +30,1 @@ [tests] | ||
| ruff | ||
| pyright | ||
| django-stubs |
@@ -32,3 +32,2 @@ CHANGELOG.rst | ||
| mozilla_django_oidc_db.egg-info/top_level.txt | ||
| mozilla_django_oidc_db/locale/nl/LC_MESSAGES/django.po | ||
| mozilla_django_oidc_db/migrations/0001_initial_to_v015.py | ||
@@ -53,3 +52,2 @@ mozilla_django_oidc_db/migrations/0001_initial_to_v023.py | ||
| mozilla_django_oidc_db/tests/mixins.py | ||
| mozilla_django_oidc_db/tests/utils.py | ||
| tests/test_admin_form.py | ||
@@ -56,0 +54,0 @@ tests/test_backend.py |
| from __future__ import annotations | ||
| import logging | ||
| from typing import TYPE_CHECKING, Any, override | ||
| from typing import Any, cast | ||
@@ -17,2 +17,3 @@ from django.contrib.auth import get_user_model | ||
| from mozilla_django_oidc.auth import OIDCAuthenticationBackend as BaseBackend | ||
| from typing_extensions import override | ||
@@ -23,3 +24,6 @@ from .config import dynamic_setting, lookup_config | ||
| from .models import OIDCClient, UserInformationClaimsSources | ||
| from .plugins import AbstractUserOIDCPlugin, AnonymousUserOIDCPlugin | ||
| from .plugins import ( | ||
| AbstractUserOIDCPluginProtocol, | ||
| AnonymousUserOIDCPluginProtocol, | ||
| ) | ||
| from .registry import register as registry | ||
@@ -77,9 +81,6 @@ from .typing import JSONObject | ||
| # AbstractUser. | ||
| UserModel = get_user_model() | ||
| if TYPE_CHECKING: | ||
| assert issubclass(UserModel, AbstractUser) | ||
| self.UserModel = UserModel | ||
| self.UserModel = cast(AbstractUser, get_user_model()) | ||
| @override | ||
| def get_settings(self, attr: str, *args: Any) -> Any: # pyright: ignore[reportIncompatibleMethodOverride] | ||
| def get_settings(self, attr: str, *args: Any) -> Any: # type: ignore | ||
| """ | ||
@@ -112,3 +113,3 @@ Override the upstream library get_settings. | ||
| @override | ||
| def authenticate( # pyright: ignore[reportIncompatibleMethodOverride] | ||
| def authenticate( # type: ignore | ||
| self, | ||
@@ -145,3 +146,3 @@ request: HttpRequest | None, | ||
| if user: | ||
| user._oidcdb_config_identifier = self.config.identifier # pyright: ignore[reportAttributeAccessIssue] | ||
| user._oidcdb_config_identifier = self.config.identifier # type: ignore | ||
@@ -157,3 +158,3 @@ return user | ||
| plugin = registry[self.config.identifier] | ||
| assert isinstance(plugin, AbstractUserOIDCPlugin) | ||
| assert isinstance(plugin, AbstractUserOIDCPluginProtocol) | ||
| return plugin.verify_claims(claims) | ||
@@ -223,7 +224,7 @@ | ||
| self, claims: JSONObject | ||
| ) -> models.QuerySet[AbstractUser]: | ||
| ) -> models.Manager[AbstractUser]: # type: ignore (parent function returns UserManager which is more specific than Manager) | ||
| assert self.config | ||
| plugin = registry[self.config.identifier] | ||
| assert isinstance(plugin, AbstractUserOIDCPlugin) | ||
| assert isinstance(plugin, AbstractUserOIDCPluginProtocol) | ||
| return plugin.filter_users_by_claims(claims) | ||
@@ -237,3 +238,3 @@ | ||
| assert isinstance(plugin, AbstractUserOIDCPlugin) | ||
| assert isinstance(plugin, AbstractUserOIDCPluginProtocol) | ||
| return plugin.create_user(claims) | ||
@@ -246,3 +247,3 @@ | ||
| assert isinstance(plugin, AbstractUserOIDCPlugin) | ||
| assert isinstance(plugin, AbstractUserOIDCPluginProtocol) | ||
| return plugin.update_user(user, claims) | ||
@@ -253,3 +254,3 @@ | ||
| self, access_token: str, id_token: str, payload: JSONObject | ||
| ) -> AnonymousUser | AbstractUser | None: | ||
| ) -> AnonymousUser | AbstractUser | None: # type: ignore (parent function returns only an AbstractUser | None) | ||
| """Get or create a user based on the tokens received.""" | ||
@@ -261,5 +262,3 @@ assert self.config | ||
| # shortcut for "anonymous users" where the OIDC authentication *does* happen, | ||
| # but no actual Django user instance is created. | ||
| if isinstance(plugin, AnonymousUserOIDCPlugin): | ||
| if isinstance(plugin, AnonymousUserOIDCPluginProtocol): | ||
| return plugin.get_or_create_user( | ||
@@ -269,5 +268,2 @@ access_token, id_token, payload, self.request | ||
| user = super().get_or_create_user(access_token, id_token, payload) | ||
| if user is not None: | ||
| assert isinstance(user, self.UserModel) | ||
| return user | ||
| return super().get_or_create_user(access_token, id_token, payload) |
@@ -9,3 +9,3 @@ """ | ||
| from typing import Any, Protocol, Self, TypeVar, Unpack, overload | ||
| from typing import Any, Generic, Protocol, TypeVar, overload | ||
@@ -18,3 +18,3 @@ from django.core.exceptions import ( | ||
| from mozilla_django_oidc.utils import import_from_settings | ||
| from typing_extensions import TypedDict | ||
| from typing_extensions import Self, TypedDict, Unpack | ||
@@ -75,7 +75,7 @@ from .constants import CONFIG_IDENTIFIER_SESSION_KEY | ||
| class DynamicSettingKwargs[T](TypedDict, total=False): | ||
| class DynamicSettingKwargs(TypedDict, Generic[T], total=False): | ||
| default: T | ||
| class dynamic_setting[T]: | ||
| class dynamic_setting(Generic[T]): | ||
| """ | ||
@@ -82,0 +82,0 @@ Descriptor to lazily access settings while explicitly defining them. |
@@ -1,8 +0,4 @@ | ||
| from collections.abc import Mapping | ||
| from .typing import EndpointFieldNames | ||
| # Mapping the configuration model fieldnames for endpoints to their | ||
| # corresponding names in the OIDC spec | ||
| OIDC_MAPPING: Mapping[EndpointFieldNames, str] = { | ||
| OIDC_MAPPING = { | ||
| "oidc_op_authorization_endpoint": "authorization_endpoint", | ||
@@ -9,0 +5,0 @@ "oidc_op_token_endpoint": "token_endpoint", |
| import json | ||
| from collections.abc import Mapping | ||
| from urllib.parse import urljoin | ||
@@ -12,7 +10,4 @@ from django import forms | ||
| from .models import OIDCProvider | ||
| from .typing import EndpointFieldNames | ||
| type EndpointsMapping = Mapping[EndpointFieldNames, str] | ||
| class OIDCProviderForm(forms.ModelForm): | ||
@@ -53,11 +48,10 @@ required_endpoints = [ | ||
| @classmethod | ||
| def get_endpoints_from_discovery(cls, base_url: str) -> EndpointsMapping: | ||
| response = requests.get(urljoin(base_url, OPEN_ID_CONFIG_PATH), timeout=10) | ||
| def get_endpoints_from_discovery(cls, base_url: str): | ||
| response = requests.get(f"{base_url}{OPEN_ID_CONFIG_PATH}", timeout=10) | ||
| response.raise_for_status() | ||
| configuration = response.json() | ||
| endpoints: EndpointsMapping = { | ||
| model_attr: endpoint | ||
| endpoints = { | ||
| model_attr: configuration.get(oidc_attr) | ||
| for model_attr, oidc_attr in cls.oidc_mapping.items() | ||
| if (endpoint := configuration.get(oidc_attr)) | ||
| } | ||
@@ -67,5 +61,5 @@ return endpoints | ||
| def clean(self): | ||
| super().clean() | ||
| cleaned_data = super().clean() | ||
| discovery_endpoint = self.cleaned_data.get("oidc_op_discovery_endpoint") | ||
| discovery_endpoint = cleaned_data.get("oidc_op_discovery_endpoint") | ||
@@ -76,3 +70,3 @@ # Derive the endpoints from the discovery endpoint | ||
| endpoints = self.get_endpoints_from_discovery(discovery_endpoint) | ||
| self.cleaned_data.update(**endpoints) | ||
| cleaned_data.update(**endpoints) | ||
| except ( | ||
@@ -93,5 +87,5 @@ requests.exceptions.RequestException, | ||
| for field in self.required_endpoints: | ||
| if not self.cleaned_data.get(field): | ||
| if not cleaned_data.get(field): | ||
| self.add_error(field, _("This field is required.")) | ||
| return self.cleaned_data | ||
| return cleaned_data |
@@ -1,2 +0,2 @@ | ||
| from typing import Any, override | ||
| from typing import Any | ||
@@ -6,2 +6,3 @@ from django.urls import reverse | ||
| from mozilla_django_oidc.middleware import SessionRefresh as BaseSessionRefresh | ||
| from typing_extensions import override | ||
@@ -72,7 +73,4 @@ from .config import ( | ||
| # we can't use cached_property, because a middleware instance exists for the whole | ||
| # duration of the django server life cycle, and the relevant config can change | ||
| # between requests. See ``process_request``. | ||
| @property | ||
| def exempt_urls(self): # pyright: ignore[reportIncompatibleVariableOverride] | ||
| def exempt_urls(self): | ||
| # In many cases, the OIDC_AUTHENTICATION_CALLBACK_URL will be the generic | ||
@@ -79,0 +77,0 @@ # callback handler and already be part of super().exempt_urls. However, this is |
@@ -63,3 +63,3 @@ # Generated by Django 4.2.9 on 2024-05-01 15:32 | ||
| "oidc_rp_scopes_list", | ||
| django_jsonform.models.fields.ArrayField( # type: ignore | ||
| django_jsonform.models.fields.ArrayField( | ||
| base_field=models.CharField( | ||
@@ -180,3 +180,3 @@ max_length=50, verbose_name="OpenID Connect scope" | ||
| "oidc_exempt_urls", | ||
| django_jsonform.models.fields.ArrayField( # type: ignore | ||
| django_jsonform.models.fields.ArrayField( | ||
| base_field=models.CharField( | ||
@@ -240,3 +240,3 @@ max_length=1000, verbose_name="Exempt URL" | ||
| "superuser_group_names", | ||
| django_jsonform.models.fields.ArrayField( # type: ignore | ||
| django_jsonform.models.fields.ArrayField( | ||
| base_field=models.CharField( | ||
@@ -243,0 +243,0 @@ max_length=50, verbose_name="Superuser group name" |
@@ -75,3 +75,3 @@ # Generated by Django 5.0.4 on 2025-08-01 09:25 | ||
| "oidc_rp_scopes_list", | ||
| django_jsonform.models.fields.ArrayField( # type: ignore | ||
| django_jsonform.models.fields.ArrayField( | ||
| base_field=models.CharField( | ||
@@ -219,3 +219,3 @@ max_length=50, verbose_name="OpenID Connect scope" | ||
| "superuser_group_names", | ||
| django_jsonform.models.fields.ArrayField( # type: ignore | ||
| django_jsonform.models.fields.ArrayField( | ||
| base_field=models.CharField( | ||
@@ -222,0 +222,0 @@ max_length=50, verbose_name="Superuser group name" |
@@ -17,3 +17,3 @@ # Generated by Django 4.2.9 on 2024-05-01 16:10 | ||
| name="new_groups_claim", | ||
| field=mozilla_django_oidc_db.fields.ClaimField( # type: ignore | ||
| field=mozilla_django_oidc_db.fields.ClaimField( | ||
| base_field=models.CharField( | ||
@@ -32,3 +32,3 @@ max_length=50, verbose_name="claim path segment" | ||
| name="new_username_claim", | ||
| field=mozilla_django_oidc_db.fields.ClaimField( # type: ignore | ||
| field=mozilla_django_oidc_db.fields.ClaimField( | ||
| base_field=models.CharField( | ||
@@ -35,0 +35,0 @@ max_length=50, verbose_name="claim path segment" |
@@ -178,3 +178,3 @@ # Generated by Django 4.2.20 on 2025-05-08 10:06 | ||
| "oidc_rp_scopes_list", | ||
| django_jsonform.models.fields.ArrayField( # type: ignore | ||
| django_jsonform.models.fields.ArrayField( | ||
| base_field=models.CharField( | ||
@@ -181,0 +181,0 @@ max_length=50, verbose_name="Scope" |
| from __future__ import annotations | ||
| import logging | ||
| from abc import ABC, abstractmethod | ||
| from typing import TYPE_CHECKING, Any | ||
| from abc import abstractmethod | ||
| from typing import Any, Protocol, TypeAlias, runtime_checkable | ||
@@ -11,3 +11,3 @@ from django.contrib.auth import get_user_model | ||
| from django.db import models | ||
| from django.http import HttpRequest, HttpResponseBase | ||
| from django.http import HttpRequest, HttpResponse | ||
@@ -22,3 +22,3 @@ from glom import Path, glom | ||
| from .schemas import ADMIN_OPTIONS_SCHEMA | ||
| from .typing import ClaimPath, GetParams, JSONObject | ||
| from .typing import ClaimPath, JSONObject | ||
| from .utils import get_groups_by_name, obfuscate_claims | ||
@@ -31,52 +31,14 @@ from .views import AdminCallbackView | ||
| # | ||
| # ABSTRACT BASE CLASSES | ||
| # | ||
| class BaseOIDCPlugin(ABC): | ||
| """ | ||
| Base class/interface for all plugins to implement. | ||
| """ | ||
| class OIDCBasePluginProtocol(Protocol): | ||
| identifier: str | ||
| """ | ||
| The unique identifier for the plugin. | ||
| schema: JSONObject | ||
| Typically provided through the ``@register(IDENTIFIER)`` decorator when registering | ||
| a plugin in downstream code. | ||
| """ | ||
| def get_config(self) -> OIDCClient: ... | ||
| def __init__(self, identifier: str): | ||
| self.identifier = identifier | ||
| def get_setting(self, attr: str, *args) -> Any: ... | ||
| def get_config(self) -> OIDCClient: | ||
| """ | ||
| Resolve the instance holding the configuration options. | ||
| """ | ||
| return OIDCClient.objects.resolve(self.identifier) | ||
| def get_setting(self, attr: str, *args) -> Any: | ||
| """ | ||
| Look up a particular configuration parameter for the configuration options. | ||
| :param attr: The setting/configuration parameter to look up. | ||
| :param args: Any additional arguments for the lookup behaviour, typically a | ||
| default value for missing settings is provided here. | ||
| """ | ||
| config = self.get_config() | ||
| return get_setting_from_config(config, attr, *args) | ||
| @abstractmethod | ||
| def get_schema(self) -> JSONObject: | ||
| """ | ||
| Return the JSON Schema definition for the client configuration options. | ||
| Each plugin provides certain behaviour that may have configuration parameters. | ||
| The configuration parameters are stored in the ``options`` JSONField of the | ||
| :class:`~mozilla_django_oidc_db.models.OIDCClient` model. | ||
| The admin integration needs a JSON Schema definitions to be able to configure | ||
| and validate the options when editing the client configuration. | ||
| """ | ||
| """Get the JSON schema of the ``options`` field on the :class:`~mozilla_django_oidc_db.models.OIDCClient` model.""" | ||
| ... | ||
@@ -86,95 +48,72 @@ | ||
| def validate_settings(self) -> None: | ||
| """ | ||
| Check the validity of the settings in the provider and client configuration. | ||
| :raises ImproperlyConfigured: if invalid configuration is detected. | ||
| """ | ||
| """Check the validity of the settings in the provider and client configuration.""" | ||
| ... | ||
| def get_extra_params( | ||
| self, request: HttpRequest, extra_params: GetParams | ||
| ) -> GetParams: | ||
| """ | ||
| Return (additional) ``GET`` parameters for the redirect to the identity provider. | ||
| @abstractmethod | ||
| def handle_callback(self, request: HttpRequest) -> HttpResponse: | ||
| """Return an HttpResponse using a specific callback view. | ||
| By default, the passed in ``extra_params`` are returned unmodified. | ||
| .. code:: python | ||
| :arg extra_params: A mapping of query parameters already produced by | ||
| :class:`mozilla_django_oidc_db.views.OIDCAuthenticationRequestInitView`. | ||
| def handle_callback(self, request: HttpRequest) -> HttpResponse: | ||
| return admin_callback_view(request) | ||
| """ | ||
| return extra_params | ||
| ... | ||
| @abstractmethod | ||
| def handle_callback(self, request: HttpRequest) -> HttpResponseBase: | ||
| """ | ||
| Return an HttpResponse using a specific callback view. | ||
| def get_extra_params( | ||
| self, request: HttpRequest, extra_params: dict[str, str | bytes] | ||
| ) -> dict[str, str | bytes]: ... | ||
| Typed as ``HttpResponseBase`` because that's the annotation for | ||
| ``View.as_view()`` in django-stubs. | ||
| For example: | ||
| class BaseOIDCPlugin: | ||
| def __init__(self, identifier: str): | ||
| self.identifier = identifier | ||
| .. code:: python | ||
| def get_config(self) -> OIDCClient: | ||
| return OIDCClient.objects.resolve(self.identifier) | ||
| def handle_callback(self, request: HttpRequest) -> HttpResponseBase: | ||
| return admin_callback_view(request) | ||
| def get_setting(self, attr: str, *args) -> Any: | ||
| config = self.get_config() | ||
| """ | ||
| ... | ||
| return get_setting_from_config(config, attr, *args) | ||
| def get_extra_params( | ||
| self, request: HttpRequest, extra_params: dict[str, str | bytes] | ||
| ) -> dict[str, str | bytes]: | ||
| return extra_params | ||
| class AnonymousUserOIDCPlugin(BaseOIDCPlugin): | ||
| if TYPE_CHECKING: | ||
| def get_or_create_user( | ||
| self, | ||
| access_token: str, | ||
| id_token: str, | ||
| payload: JSONObject, | ||
| request: HttpRequest, | ||
| ) -> AnonymousUser | None: ... | ||
| @runtime_checkable | ||
| class AnonymousUserOIDCPluginProtocol(OIDCBasePluginProtocol, Protocol): | ||
| def get_or_create_user( | ||
| self, | ||
| access_token: str, | ||
| id_token: str, | ||
| payload: JSONObject, | ||
| request: HttpRequest, | ||
| ) -> AnonymousUser | None: ... | ||
| class AbstractUserOIDCPlugin(BaseOIDCPlugin): | ||
| if TYPE_CHECKING: | ||
| @runtime_checkable | ||
| class AbstractUserOIDCPluginProtocol(OIDCBasePluginProtocol, Protocol): | ||
| def create_user(self, claims: JSONObject) -> AbstractUser: ... | ||
| @abstractmethod | ||
| def create_user(self, claims: JSONObject) -> AbstractUser: | ||
| """ | ||
| Create and return the Django user in the database from the validated claims. | ||
| """ | ||
| ... | ||
| def update_user(self, user: AbstractUser, claims: JSONObject) -> AbstractUser: ... | ||
| @abstractmethod | ||
| def update_user(self, user: AbstractUser, claims: JSONObject) -> AbstractUser: | ||
| """ | ||
| Update and return the Django user in the database from the validated claims. | ||
| """ | ||
| ... | ||
| def filter_users_by_claims( | ||
| self, claims: JSONObject | ||
| ) -> models.Manager[AbstractUser]: | ||
| """Return all users matching the specified subject.""" | ||
| ... | ||
| @abstractmethod | ||
| def filter_users_by_claims( | ||
| self, | ||
| claims: JSONObject, | ||
| ) -> models.QuerySet[AbstractUser]: | ||
| """ | ||
| Given the validated claims, filter for existing users in the database. | ||
| def verify_claims(self, claims: JSONObject) -> bool: | ||
| """Verify the provided claims to decide if authentication should be allowed.""" | ||
| ... | ||
| This method is called to test if a user already exists that should be | ||
| updated rather than created. | ||
| """ | ||
| ... | ||
| def verify_claims(self, claims: JSONObject) -> bool: | ||
| """ | ||
| Verify the provided claims to decide if authentication should be allowed. | ||
| """ | ||
| ... | ||
| OIDCPlugin: TypeAlias = AbstractUserOIDCPluginProtocol | AnonymousUserOIDCPluginProtocol | ||
| # | ||
| # CONCRETE IMPLEMENTATIONS | ||
| # | ||
| admin_callback_view = AdminCallbackView.as_view() | ||
@@ -184,6 +123,4 @@ | ||
| @register(OIDC_ADMIN_CONFIG_IDENTIFIER) | ||
| class OIDCAdminPlugin(AbstractUserOIDCPlugin): | ||
| """ | ||
| Implement the core plugin for admin authentication via OpenID Connect. | ||
| """ | ||
| class OIDCAdminPlugin(BaseOIDCPlugin, AbstractUserOIDCPluginProtocol): | ||
| schema = ADMIN_OPTIONS_SCHEMA | ||
@@ -248,9 +185,5 @@ def verify_claims(self, claims: JSONObject) -> bool: | ||
| self, claims: JSONObject | ||
| ) -> models.QuerySet[AbstractUser]: | ||
| ) -> models.Manager[AbstractUser]: | ||
| """Return all users matching the specified subject.""" | ||
| UserModel = get_user_model() | ||
| if TYPE_CHECKING: | ||
| assert issubclass(UserModel, AbstractUser), ( | ||
| "The user model must inherit from AbstractUser." | ||
| ) | ||
@@ -273,8 +206,2 @@ username = self.get_username(claims) | ||
| """Return object for a newly created user account.""" | ||
| UserModel = get_user_model() | ||
| if TYPE_CHECKING: | ||
| assert issubclass(UserModel, AbstractUser), ( | ||
| "The user model must inherit from AbstractUser." | ||
| ) | ||
| username = self.get_username(claims) | ||
@@ -284,2 +211,3 @@ | ||
| UserModel = get_user_model() | ||
| user = UserModel.objects.create_user(**{UserModel.USERNAME_FIELD: username}) | ||
@@ -420,3 +348,3 @@ self.update_user(user, claims) | ||
| def handle_callback(self, request: HttpRequest) -> HttpResponseBase: | ||
| def handle_callback(self, request: HttpRequest) -> HttpResponse: | ||
| return admin_callback_view(request) |
| from __future__ import annotations | ||
| from collections.abc import Callable, Iterable, MutableMapping | ||
| from collections.abc import Callable, Iterable | ||
| from typing import TYPE_CHECKING | ||
@@ -9,7 +9,7 @@ | ||
| if TYPE_CHECKING: | ||
| from .plugins import BaseOIDCPlugin | ||
| from .plugins import OIDCPlugin | ||
| class OIDCRegistry[T: type[BaseOIDCPlugin]]: | ||
| _registry: MutableMapping[str, T] | ||
| class OIDCRegistry: | ||
| _registry: dict[str, OIDCPlugin] | ||
@@ -19,3 +19,5 @@ def __init__(self): | ||
| def __call__(self, unique_identifier: str) -> Callable[[T], T]: | ||
| def __call__( | ||
| self, unique_identifier: str | ||
| ) -> Callable[[type[OIDCPlugin]], type[OIDCPlugin]]: | ||
| if len(unique_identifier) > UNIQUE_PLUGIN_ID_MAX_LENGTH: | ||
@@ -27,3 +29,3 @@ raise ValueError( | ||
| def decorator(plugin_cls: T) -> T: | ||
| def decorator(plugin_cls: type[OIDCPlugin]) -> type[OIDCPlugin]: | ||
| if unique_identifier in self._registry: | ||
@@ -41,3 +43,3 @@ raise ValueError( | ||
| def items(self) -> Iterable[tuple[str, T]]: | ||
| def items(self) -> Iterable[tuple[str, OIDCPlugin]]: | ||
| return self._registry.items() | ||
@@ -48,3 +50,3 @@ | ||
| def __getitem__(self, key: str) -> T: | ||
| def __getitem__(self, key: str) -> OIDCPlugin: | ||
| return self._registry[key] | ||
@@ -51,0 +53,0 @@ |
@@ -73,3 +73,3 @@ from typing import Annotated, Literal | ||
| description="a unique identifier for this OIDC provider.", | ||
| examples=["test-oidc-provider"], | ||
| examples=["admin-oidc-provider"], | ||
| ) | ||
@@ -99,27 +99,3 @@ endpoint_config: OIDCProviderConfigUnion | ||
| oidc_rp_scopes_list: list[str] = DjangoModelRef(OIDCClient, "oidc_rp_scopes_list") | ||
| options: dict = DjangoModelRef( | ||
| OIDCClient, | ||
| "options", | ||
| default_factory=dict, | ||
| examples=[ | ||
| { | ||
| "user_settings": { | ||
| "claim_mappings": { | ||
| "username": ["sub"], | ||
| "email": ["email"], | ||
| "first_name": ["given_name"], | ||
| "last_name": ["family_name"], | ||
| }, | ||
| "username_case_sensitive": False, | ||
| }, | ||
| "groups_settings": { | ||
| "make_users_staff": True, | ||
| "superuser_group_names": ["superuser"], | ||
| "sync": True, | ||
| "sync_pattern": "*", | ||
| "claim_mapping": ["roles"], | ||
| }, | ||
| } | ||
| ], | ||
| ) | ||
| options: dict = DjangoModelRef(OIDCClient, "options", default_factory=dict) | ||
@@ -129,3 +105,3 @@ endpoint_config: OIDCProviderConfigUnion | None = Field( | ||
| default=None, | ||
| deprecated="Moved to `providers.endpoint_config`", | ||
| deprecated=True, | ||
| ) | ||
@@ -144,3 +120,3 @@ oidc_provider_identifier: str = DjangoModelRef( | ||
| description=_("Mapping from User model field names to a path in the claim."), | ||
| deprecated="Moved to `items.options.user_settings.claim_mappings`", | ||
| deprecated=True, | ||
| ) | ||
@@ -154,3 +130,3 @@ oidc_token_use_basic_auth: bool = Field( | ||
| ), | ||
| deprecated="Moved to `providers.oidc_token_use_basic_auth`", | ||
| deprecated=True, | ||
| ) | ||
@@ -160,3 +136,3 @@ oidc_use_nonce: bool = Field( | ||
| description=_("Controls whether the client uses nonce verification"), | ||
| deprecated="Moved to providers.oidc_use_nonce", | ||
| deprecated=True, | ||
| ) | ||
@@ -168,3 +144,3 @@ oidc_nonce_size: int = Field( | ||
| ), | ||
| deprecated="Moved to `providers.oidc_nonce_size`", | ||
| deprecated=True, | ||
| ) | ||
@@ -176,3 +152,3 @@ oidc_state_size: int = Field( | ||
| ), | ||
| deprecated="Moved to `providers.oidc_state_size`", | ||
| deprecated=True, | ||
| ) | ||
@@ -183,3 +159,3 @@ # Arrays are overridden to make the typing simpler (the underlying Django field is an ArrayField, which is non-standard) | ||
| description=_("Path in the claims to the value to use as username."), | ||
| deprecated="Moved to `items.options.user_settings.claim_mappings.username`", | ||
| deprecated=True, | ||
| examples=[["nested", "username", "claim"]], | ||
@@ -190,3 +166,3 @@ ) | ||
| description=_("Path in the claims to the value with group names."), | ||
| deprecated="Moved to `items.options.group_settings.claim_mapping`", | ||
| deprecated=True, | ||
| examples=[["nested", "group", "claim"]], | ||
@@ -197,3 +173,3 @@ ) | ||
| description=_("Superuser group names"), | ||
| deprecated="Moved to `items.options.group_settings.superuser_group_names`", | ||
| deprecated=True, | ||
| examples=[["superusers"]], | ||
@@ -204,3 +180,3 @@ ) | ||
| description=_("Default group names"), | ||
| deprecated="Moved `items.options.group_settings.default_groups`", | ||
| deprecated=True, | ||
| examples=[["read-only-users"]], | ||
@@ -210,3 +186,3 @@ ) | ||
| description=_("Whether to sync local groups"), | ||
| deprecated="Moved to `items.options.group_settings.sync`", | ||
| deprecated=True, | ||
| examples=[True], | ||
@@ -217,3 +193,3 @@ default=True, | ||
| description=_("Pattern that the group names to sync should follow."), | ||
| deprecated="Moved to `items.options.group_settings.sync_pattern`", | ||
| deprecated=True, | ||
| examples=["*"], | ||
@@ -224,3 +200,3 @@ default="*", | ||
| description=_("Whether to make the users staff."), | ||
| deprecated="Moved to `items.options.groups_settings.make_users_staff`", | ||
| deprecated=True, | ||
| examples=[False], | ||
@@ -227,0 +203,0 @@ default=False, |
@@ -15,3 +15,4 @@ {% extends "admin/login.html" %} | ||
| There was a problem while logging in with your organization account. | ||
| Please contact your administrator with this error message to resolve this: | ||
| Please contact your Open Forms administrator with this error message to | ||
| resolve this: | ||
| {% endblocktrans %} | ||
@@ -27,2 +28,2 @@ </p> | ||
| {% endblock %} | ||
| {% endblock %} |
| from __future__ import annotations | ||
| from collections.abc import Mapping, MutableMapping, Sequence | ||
| from typing import Literal, Protocol | ||
| from collections.abc import Sequence | ||
| from typing import Protocol, TypeAlias | ||
| from django.http import HttpRequest, HttpResponseBase | ||
| type EndpointFieldNames = Literal[ | ||
| "oidc_op_authorization_endpoint", | ||
| "oidc_op_token_endpoint", | ||
| "oidc_op_user_endpoint", | ||
| "oidc_op_jwks_endpoint", | ||
| "oidc_op_logout_endpoint", | ||
| ] | ||
| JSONPrimitive: TypeAlias = str | int | float | bool | None | ||
| JSONValue: TypeAlias = "JSONPrimitive | list[JSONValue] | JSONObject" | ||
| JSONObject: TypeAlias = dict[str, JSONValue] | ||
| type JSONPrimitive = str | int | float | bool | None | ||
| type JSONValue = JSONPrimitive | list[JSONValue] | JSONObject | ||
| type JSONObject = MutableMapping[str, JSONValue] | ||
| ClaimPath: TypeAlias = Sequence[str] | ||
| type ClaimPath = Sequence[str] | ||
| type GetParams = Mapping[str, str | bytes] | ||
| class DjangoView(Protocol): | ||
| def __call__(self, request: HttpRequest) -> HttpResponseBase: ... |
@@ -10,5 +10,3 @@ import fnmatch | ||
| from glom import Path, PathAccessError, assign, glom | ||
| from requests.utils import ( | ||
| _parse_content_type_header, # pyright: ignore[reportAttributeAccessIssue] | ||
| ) | ||
| from requests.utils import _parse_content_type_header | ||
@@ -15,0 +13,0 @@ from .models import OIDCClient |
@@ -9,3 +9,3 @@ import logging | ||
| from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect | ||
| from django.urls import reverse | ||
| from django.urls import reverse_lazy | ||
| from django.utils.http import url_has_allowed_host_and_scheme | ||
@@ -25,3 +25,2 @@ from django.views import View | ||
| from .registry import register as registry | ||
| from .typing import GetParams | ||
@@ -107,5 +106,3 @@ logger = logging.getLogger(__name__) | ||
| @property | ||
| def failure_url(self): | ||
| return reverse("admin-oidc-error") | ||
| failure_url = reverse_lazy("admin-oidc-error") | ||
@@ -322,15 +319,17 @@ def get(self, request: HttpRequest, *args, **kwargs): | ||
| def get_extra_params(self, request: HttpRequest) -> GetParams: | ||
| def get_extra_params(self, request: HttpRequest) -> dict[str, str]: | ||
| """ | ||
| Add a keycloak identity provider hint if configured. | ||
| """ | ||
| extra: GetParams = super().get_extra_params(request) | ||
| extra = super().get_extra_params(request) | ||
| if kc_idp_hint := self.get_settings("OIDC_KEYCLOAK_IDP_HINT", ""): | ||
| extra = {**extra, "kc_idp_hint": kc_idp_hint} | ||
| extra["kc_idp_hint"] = kc_idp_hint | ||
| plugin = registry[self.identifier] | ||
| return plugin.get_extra_params(request, extra) | ||
| extra = plugin.get_extra_params(request, extra) | ||
| return extra | ||
| class OIDCAuthenticationRequestView(OIDCAuthenticationRequestInitView): | ||
@@ -337,0 +336,0 @@ """ |
+8
-8
| Metadata-Version: 2.4 | ||
| Name: mozilla-django-oidc-db | ||
| Version: 1.1.1 | ||
| Version: 0.25.2 | ||
| Summary: A database-backed configuration for mozilla-django-oidc | ||
@@ -13,3 +13,3 @@ Author-email: Maykin Media <support@maykinmedia.nl> | ||
| Keywords: OIDC,django,database,authentication | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: Framework :: Django | ||
@@ -22,6 +22,8 @@ Classifier: Framework :: Django :: 4.2 | ||
| Classifier: Operating System :: Microsoft :: Windows | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Classifier: Topic :: Software Development :: Libraries :: Python Modules | ||
| Requires-Python: >=3.12 | ||
| Requires-Python: >=3.10 | ||
| Description-Content-Type: text/x-rst | ||
@@ -35,3 +37,3 @@ License-File: LICENSE | ||
| Provides-Extra: setup-configuration | ||
| Requires-Dist: django-setup-configuration>=0.11.0; extra == "setup-configuration" | ||
| Requires-Dist: django-setup-configuration>=0.8.2; extra == "setup-configuration" | ||
| Provides-Extra: tests | ||
@@ -49,4 +51,2 @@ Requires-Dist: psycopg; extra == "tests" | ||
| Requires-Dist: ruff; extra == "tests" | ||
| Requires-Dist: pyright; extra == "tests" | ||
| Requires-Dist: django-stubs; extra == "tests" | ||
| Provides-Extra: docs | ||
@@ -68,3 +68,3 @@ Requires-Dist: sphinx; extra == "docs" | ||
| :Version: 1.1.1 | ||
| :Version: 0.25.2 | ||
| :Source: https://github.com/maykinmedia/mozilla-django-oidc-db | ||
@@ -108,3 +108,3 @@ :Keywords: OIDC, django, database, authentication | ||
| .. |coverage| image:: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db/branch/master/graph/badge.svg | ||
| :target: https://app.codecov.io/gh/maykinmedia/mozilla-django-oidc-db | ||
| :target: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db | ||
| :alt: Coverage status | ||
@@ -111,0 +111,0 @@ |
+8
-24
@@ -7,3 +7,3 @@ [build-system] | ||
| name = "mozilla-django-oidc-db" | ||
| version = "1.1.1" | ||
| version = "0.25.2" | ||
| description = "A database-backed configuration for mozilla-django-oidc" | ||
@@ -18,3 +18,3 @@ authors = [ | ||
| classifiers = [ | ||
| "Development Status :: 5 - Production/Stable", | ||
| "Development Status :: 4 - Beta", | ||
| "Framework :: Django", | ||
@@ -27,2 +27,4 @@ "Framework :: Django :: 4.2", | ||
| "Operating System :: Microsoft :: Windows", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
@@ -32,3 +34,3 @@ "Programming Language :: Python :: 3.13", | ||
| ] | ||
| requires-python = ">=3.12" | ||
| requires-python = ">=3.10" | ||
| dependencies = [ | ||
@@ -51,3 +53,3 @@ "django>=4.2", | ||
| setup-configuration = [ | ||
| "django-setup-configuration>=0.11.0", | ||
| "django-setup-configuration>=0.8.2", | ||
| ] | ||
@@ -66,4 +68,2 @@ tests = [ | ||
| "ruff", | ||
| "pyright", | ||
| "django-stubs", | ||
| ] | ||
@@ -93,3 +93,3 @@ docs = [ | ||
| [tool.bumpversion] | ||
| current_version = "1.1.1" | ||
| current_version = "0.25.2" | ||
| files = [ | ||
@@ -119,3 +119,3 @@ {filename = "pyproject.toml"}, | ||
| "\\.\\.\\.", | ||
| "\\bpass$", | ||
| "pass", | ||
| ] | ||
@@ -126,18 +126,2 @@ omit = [ | ||
| [tool.pyright] | ||
| include = [ | ||
| "mozilla_django_oidc_db", | ||
| "testapp", | ||
| "tests", | ||
| ] | ||
| ignore = [ | ||
| # at the time of writing, the type definitions in factory-boy are still broken | ||
| "mozilla_django_oidc_db/tests/factories.py", | ||
| # don't know what's going on here, in particular because it's based on Pydantic I'd | ||
| # expect this to have zero type checking issues. TODO for later | ||
| "mozilla_django_oidc_db/setup_configuration/", | ||
| ] | ||
| # pythonVersion = "3.10" | ||
| pythonPlatform = "Linux" | ||
| [tool.ruff.lint] | ||
@@ -144,0 +128,0 @@ extend-select = [ |
+2
-2
@@ -10,3 +10,3 @@ | ||
| :Version: 1.1.1 | ||
| :Version: 0.25.2 | ||
| :Source: https://github.com/maykinmedia/mozilla-django-oidc-db | ||
@@ -50,3 +50,3 @@ :Keywords: OIDC, django, database, authentication | ||
| .. |coverage| image:: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db/branch/master/graph/badge.svg | ||
| :target: https://app.codecov.io/gh/maykinmedia/mozilla-django-oidc-db | ||
| :target: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db | ||
| :alt: Coverage status | ||
@@ -53,0 +53,0 @@ |
@@ -133,16 +133,2 @@ from json.decoder import JSONDecodeError | ||
| @pytest.mark.vcr | ||
| def test_derive_endpoints_google_oidc(): | ||
| endpoints = OIDCProviderForm.get_endpoints_from_discovery( | ||
| base_url="https://accounts.google.com" | ||
| ) | ||
| assert endpoints == { | ||
| "oidc_op_authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", | ||
| "oidc_op_token_endpoint": "https://oauth2.googleapis.com/token", | ||
| "oidc_op_user_endpoint": "https://openidconnect.googleapis.com/v1/userinfo", | ||
| "oidc_op_jwks_endpoint": "https://www.googleapis.com/oauth2/v3/certs", | ||
| } | ||
| @pytest.mark.django_db | ||
@@ -149,0 +135,0 @@ def test_no_discovery_endpoint_other_fields_required(): |
+24
-25
| import logging | ||
| from django.contrib.auth.models import Group, User | ||
| from django.contrib.sessions.backends.base import SessionBase | ||
| from django.core.exceptions import BadRequest, ImproperlyConfigured | ||
@@ -21,4 +20,2 @@ from django.http import HttpRequest | ||
| from .conftest import callback_request_mark as callback_request, oidcconfig | ||
| # | ||
@@ -29,3 +26,3 @@ # DYNAMIC CONFIGURATION TESTS | ||
| @oidcconfig(enabled=False) | ||
| @pytest.mark.oidcconfig(enabled=False) | ||
| def test_authenticate_oidc_not_enabled(dummy_config, callback_request: HttpRequest): | ||
@@ -42,4 +39,4 @@ backend = OIDCAuthenticationBackend() | ||
| @pytest.mark.django_db | ||
| @oidcconfig | ||
| @callback_request( | ||
| @pytest.mark.oidcconfig | ||
| @pytest.mark.callback_request( | ||
| init_view=OIDCAuthenticationRequestInitView.as_view(identifier="test-oidc-disabled") | ||
@@ -98,3 +95,3 @@ ) | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -144,3 +141,3 @@ extra_options={ | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -177,3 +174,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -207,3 +204,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -251,3 +248,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -285,3 +282,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -317,3 +314,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -379,3 +376,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -409,3 +406,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -438,3 +435,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -473,3 +470,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -498,3 +495,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -527,3 +524,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -555,3 +552,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -590,3 +587,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig(enabled=True) | ||
| @pytest.mark.oidcconfig(enabled=True) | ||
| def test_authenticate_called_without_args(dummy_config): | ||
@@ -600,3 +597,3 @@ backend = OIDCAuthenticationBackend() | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -616,3 +613,5 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @oidcconfig(enabled=True, userinfo_claims_source=UserInformationClaimsSources.id_token) | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, userinfo_claims_source=UserInformationClaimsSources.id_token | ||
| ) | ||
| def test_empty_claims(dummy_config, callback_request: HttpRequest): | ||
@@ -626,3 +625,3 @@ backend = MockBackend(claims={}) | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -659,3 +658,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| request = rf.get("/oidc/callback", {"state": "foo", "code": "bar"}) | ||
| request.session = SessionBase() | ||
| request.session = {} | ||
| backend = OIDCAuthenticationBackend() | ||
@@ -662,0 +661,0 @@ |
@@ -1,6 +0,4 @@ | ||
| from typing import Protocol | ||
| from django.contrib.auth.models import User | ||
| from django.db import IntegrityError | ||
| from django.http import HttpRequest, HttpResponseRedirect | ||
| from django.http import HttpRequest | ||
| from django.test import Client | ||
@@ -21,13 +19,5 @@ from django.urls import reverse | ||
| from .conftest import auth_request_mark as auth_request, oidcconfig | ||
| from .factories import UserFactory | ||
| class CallbackRequestMark(Protocol): | ||
| def __call__(self, claims: JSONObject) -> pytest.MarkDecorator: ... | ||
| mock_backend_claims: CallbackRequestMark = pytest.mark.mock_backend_claims | ||
| @pytest.fixture | ||
@@ -55,4 +45,6 @@ def mock_auth_backend(request, mocker): | ||
| @mock_backend_claims({"email": "collision@example.com", "sub": "some_username"}) | ||
| @oidcconfig( | ||
| @pytest.mark.mock_backend_claims( | ||
| {"email": "collision@example.com", "sub": "some_username"} | ||
| ) | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -97,3 +89,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @mock_backend_claims( | ||
| @pytest.mark.mock_backend_claims( | ||
| { | ||
@@ -105,3 +97,3 @@ "sub": "some_username", | ||
| ) | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -139,3 +131,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @mock_backend_claims( | ||
| @pytest.mark.mock_backend_claims( | ||
| { | ||
@@ -146,4 +138,4 @@ "email": "nocollision@example.com", | ||
| ) | ||
| @auth_request(next="/admin/") | ||
| @oidcconfig( | ||
| @pytest.mark.auth_request(next="/admin/") | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -167,4 +159,4 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @mock_backend_claims({}) | ||
| @oidcconfig( | ||
| @pytest.mark.mock_backend_claims({}) | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -225,3 +217,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, | ||
| @pytest.mark.django_db | ||
| @oidcconfig(enabled=False, oidc_op_authorization_endpoint="bad") | ||
| @pytest.mark.oidcconfig(enabled=False, oidc_op_authorization_endpoint="bad") | ||
| def test_wrong_config_model_used( | ||
@@ -247,8 +239,7 @@ dummy_config: OIDCClient, | ||
| assert callback_response.status_code == 302 | ||
| assert isinstance(callback_response, HttpResponseRedirect) | ||
| assert callback_response.url == "/admin/login/failure/" | ||
| @auth_request(next="/admin/") | ||
| @oidcconfig( | ||
| @pytest.mark.auth_request(next="/admin/") | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -255,0 +246,0 @@ userinfo_claims_source=UserInformationClaimsSources.id_token, |
@@ -8,3 +8,2 @@ """ | ||
| from django.core.exceptions import DisallowedRedirect | ||
| from django.http import HttpResponseRedirect | ||
@@ -16,4 +15,2 @@ import pytest | ||
| from .conftest import oidcconfig | ||
| pytestmark = [pytest.mark.django_db] | ||
@@ -26,3 +23,3 @@ | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -37,3 +34,2 @@ oidc_rp_client_id="fixed_client_id", | ||
| assert response.status_code == 302 | ||
| assert isinstance(response, HttpResponseRedirect) | ||
| parsed_url = urlsplit(response.url) | ||
@@ -54,3 +50,3 @@ assert parsed_url.scheme == "https" | ||
| @oidcconfig | ||
| @pytest.mark.oidcconfig() | ||
| def test_suspicious_return_url(dummy_config, auth_request): | ||
@@ -69,3 +65,3 @@ with pytest.raises(DisallowedRedirect): | ||
| ) | ||
| @oidcconfig | ||
| @pytest.mark.oidcconfig() | ||
| def test_forgotten_return_url(dummy_config, auth_request, get_kwargs): | ||
@@ -86,5 +82,5 @@ with pytest.raises(ValueError): | ||
| @oidcconfig(check_op_availability=True) | ||
| @pytest.mark.oidcconfig(check_op_availability=True) | ||
| def test_idp_check_mechanism(dummy_config, auth_request, settings): | ||
| with pytest.raises(OIDCProviderOutage): | ||
| oidc_init_with_idp_check(auth_request) |
+14
-18
@@ -8,3 +8,3 @@ """ | ||
| from django.http import HttpRequest, HttpResponseRedirect | ||
| from django.http import HttpRequest | ||
| from django.urls import reverse | ||
@@ -19,9 +19,8 @@ | ||
| from mozilla_django_oidc_db.tests.factories import OIDCClientFactory | ||
| from mozilla_django_oidc_db.typing import GetParams | ||
| from mozilla_django_oidc_db.views import OIDCAuthenticationRequestInitView | ||
| from .conftest import auth_request_mark as auth_request, oidcconfig | ||
| @oidcconfig(oidc_op_authorization_endpoint="http://localhost:8080/openid-connect/auth") | ||
| @pytest.mark.oidcconfig( | ||
| oidc_op_authorization_endpoint="http://localhost:8080/openid-connect/auth" | ||
| ) | ||
| def test_default_config_flow(filled_admin_config, client): | ||
@@ -50,3 +49,3 @@ start_url = reverse("oidc_authentication_init") | ||
| @oidcconfig(oidc_keycloak_idp_hint="keycloak-idp2") | ||
| @pytest.mark.oidcconfig(oidc_keycloak_idp_hint="keycloak-idp2") | ||
| def test_keycloak_idp_hint_via_config(filled_admin_config, settings, client): | ||
@@ -65,3 +64,3 @@ settings.OIDC_KEYCLOAK_IDP_HINT = "keycloak-idp1" | ||
| @oidcconfig(check_op_availability=True) | ||
| @pytest.mark.oidcconfig(check_op_availability=True) | ||
| def test_check_idp_availability_not_available( | ||
@@ -77,3 +76,3 @@ filled_admin_config, settings, client, requests_mock | ||
| @oidcconfig(check_op_availability=True) | ||
| @pytest.mark.oidcconfig(check_op_availability=True) | ||
| def test_check_idp_availability_available( | ||
@@ -90,4 +89,4 @@ filled_admin_config, settings, client, requests_mock | ||
| @oidcconfig(oidc_rp_scopes_list=["email"]) | ||
| @auth_request | ||
| @pytest.mark.oidcconfig(oidc_rp_scopes_list=["email"]) | ||
| @pytest.mark.auth_request | ||
| def test_overwrite_scope(dummy_config, auth_request): | ||
@@ -98,6 +97,5 @@ """Test whether the scopes specified in the configuration can be overwritten.""" | ||
| class OIDCTestExtraParamsPlugin(OIDCAdminPlugin): | ||
| def get_extra_params( | ||
| self, request: HttpRequest, extra_params: GetParams | ||
| ) -> GetParams: | ||
| return {**extra_params, "scope": "not-email and-extra"} | ||
| def get_extra_params(self, request: HttpRequest, extra_params: dict) -> dict: | ||
| extra_params["scope"] = "not-email and-extra" | ||
| return extra_params | ||
@@ -110,3 +108,2 @@ OIDCClientFactory.create(identifier="test-extra-scope") | ||
| assert redirect_response.status_code == 302 | ||
| assert isinstance(redirect_response, HttpResponseRedirect) | ||
@@ -119,4 +116,4 @@ parsed_url = urlsplit(redirect_response.url) | ||
| @oidcconfig() | ||
| @auth_request | ||
| @pytest.mark.oidcconfig() | ||
| @pytest.mark.auth_request | ||
| def test_override_callback_url_plugin_settings_used(dummy_config, auth_request): | ||
@@ -138,3 +135,2 @@ @register("test-settings-override") | ||
| assert redirect_response.status_code == 302 | ||
| assert isinstance(redirect_response, HttpResponseRedirect) | ||
@@ -141,0 +137,0 @@ parsed_url = urlsplit(redirect_response.url) |
@@ -8,9 +8,8 @@ from django.test import Client | ||
| from mozilla_django_oidc_db.models import OIDCClient | ||
| from mozilla_django_oidc_db.tests.utils import keycloak_login | ||
| from .conftest import oidcconfig | ||
| from .utils import keycloak_login | ||
| @pytest.mark.vcr | ||
| @oidcconfig(extra_options={"make_users_staff": True}) | ||
| @pytest.mark.oidcconfig(make_users_staff=True) | ||
| def test_use_config_class_from_state_over_config_class_from_session( | ||
@@ -17,0 +16,0 @@ keycloak_config: OIDCClient, |
@@ -10,5 +10,4 @@ from django.urls import reverse | ||
| ) | ||
| from mozilla_django_oidc_db.tests.utils import keycloak_login | ||
| from .conftest import oidcconfig | ||
| from .utils import keycloak_login | ||
@@ -93,3 +92,3 @@ KEYCLOAK_BASE_URL = "http://localhost:8080/realms/test/" | ||
| @pytest.mark.vcr | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| # Set up client configured to return JWT from userinfo endpoint instead of plain | ||
@@ -121,3 +120,3 @@ # JSON. Credentials from ``docker/import`` realm export. | ||
| @pytest.mark.vcr | ||
| @oidcconfig(extra_options={"make_users_staff": True}) | ||
| @pytest.mark.oidcconfig(make_users_staff=True) | ||
| def test_session_refresh( | ||
@@ -124,0 +123,0 @@ keycloak_config, |
@@ -8,6 +8,5 @@ from django.test import Client | ||
| from mozilla_django_oidc_db.models import OIDCClient | ||
| from mozilla_django_oidc_db.tests.utils import keycloak_login | ||
| from mozilla_django_oidc_db.utils import do_op_logout | ||
| from .conftest import oidcconfig | ||
| from .utils import keycloak_login | ||
@@ -56,3 +55,3 @@ | ||
| @pytest.mark.vcr | ||
| @oidcconfig(oidc_op_logout_endpoint="") | ||
| @pytest.mark.oidcconfig(oidc_op_logout_endpoint="") | ||
| def test_logout_without_endpoint_configured( | ||
@@ -94,3 +93,3 @@ keycloak_config: OIDCClient, | ||
| @oidcconfig(oidc_op_logout_endpoint="https://example.com/oidc/logout") | ||
| @pytest.mark.oidcconfig(oidc_op_logout_endpoint="https://example.com/oidc/logout") | ||
| def test_logout_response_has_redirect(dummy_config: OIDCClient, requests_mock): | ||
@@ -97,0 +96,0 @@ requests_mock.post( |
+22
-29
@@ -1,2 +0,1 @@ | ||
| from typing import Protocol, Unpack | ||
| from urllib.parse import parse_qs, urlparse | ||
@@ -15,4 +14,3 @@ | ||
| from .conftest import oidcconfig | ||
| from .utils import OIDCConfigOptions, create_or_update_configuration | ||
| from .utils import create_or_update_configuration | ||
@@ -51,27 +49,22 @@ | ||
| class ConfigFactory(Protocol): | ||
| def __call__( | ||
| self, config_identifier: str, /, **overrides: Unpack[OIDCConfigOptions] | ||
| ) -> OIDCClient: ... | ||
| @pytest.fixture() | ||
| def config_factory(db) -> ConfigFactory: | ||
| def _factory(config_identifier: str, /, **overrides: Unpack[OIDCConfigOptions]): | ||
| def config_factory(db): | ||
| def _factory(config_identifier: str, /, **overrides): | ||
| BASE = f"https://mock-oidc-provider-{config_identifier}:9999" | ||
| config_options: OIDCConfigOptions = { | ||
| "enabled": True, | ||
| "oidc_rp_client_id": "fake", | ||
| "oidc_rp_client_secret": "even-faker", | ||
| "oidc_rp_sign_algo": "RS256", | ||
| "oidc_op_discovery_endpoint": f"{BASE}/oidc/", | ||
| "oidc_op_jwks_endpoint": f"{BASE}/oidc/jwks", | ||
| "oidc_op_authorization_endpoint": f"{BASE}/oidc/auth", | ||
| "oidc_op_token_endpoint": f"{BASE}/oidc/token", | ||
| "oidc_op_user_endpoint": f"{BASE}/oidc/user", | ||
| **overrides, | ||
| } | ||
| config = create_or_update_configuration( | ||
| f"{config_identifier}-provider", config_identifier, config_options | ||
| f"{config_identifier}-provider", | ||
| config_identifier, | ||
| { | ||
| "enabled": True, | ||
| "oidc_rp_client_id": "fake", | ||
| "oidc_rp_client_secret": "even-faker", | ||
| "oidc_rp_sign_algo": "RS256", | ||
| "oidc_op_discovery_endpoint": f"{BASE}/oidc/", | ||
| "oidc_op_jwks_endpoint": f"{BASE}/oidc/jwks", | ||
| "oidc_op_authorization_endpoint": f"{BASE}/oidc/auth", | ||
| "oidc_op_token_endpoint": f"{BASE}/oidc/token", | ||
| "oidc_op_user_endpoint": f"{BASE}/oidc/user", | ||
| **overrides, | ||
| }, | ||
| ) | ||
@@ -84,3 +77,3 @@ | ||
| @oidcconfig(enabled=False) | ||
| @pytest.mark.oidcconfig(enabled=False) | ||
| def test_sessionrefresh_oidc_not_enabled( | ||
@@ -99,3 +92,3 @@ dummy_config: OIDCClient, | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -134,3 +127,3 @@ oidc_rp_client_id="initial-client-id", | ||
| @oidcconfig(enabled=True) | ||
| @pytest.mark.oidcconfig(enabled=True) | ||
| def test_sessionrefresh_config_use_defaults( | ||
@@ -169,3 +162,3 @@ dummy_config, | ||
| config_identifier, | ||
| config_factory: ConfigFactory, | ||
| config_factory, | ||
| request_factory, | ||
@@ -206,3 +199,3 @@ session_refresh: SessionRefresh, | ||
| config_identifier, | ||
| config_factory: ConfigFactory, | ||
| config_factory, | ||
| request_factory, | ||
@@ -209,0 +202,0 @@ session_refresh: SessionRefresh, |
@@ -7,10 +7,9 @@ import logging | ||
| from .conftest import oidcconfig | ||
| from .factories import UserFactory | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
| username_claim=["username"], | ||
| extra_options={ | ||
| "user_settings.claim_mappings.username": ["username"], | ||
| "user_settings.claim_mappings.first_name": [], | ||
@@ -51,6 +50,6 @@ "user_settings.claim_mappings.email": ["email"], | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
| username_claim=["username"], | ||
| extra_options={ | ||
| "user_settings.claim_mappings.username": ["username"], | ||
| "groups_settings.claim_mapping": ["not-present"], | ||
@@ -57,0 +56,0 @@ }, |
| from typing import Any | ||
| from django.contrib.sessions.backends.base import SessionBase | ||
| from django.test import RequestFactory | ||
@@ -16,5 +15,3 @@ | ||
| from .conftest import oidcconfig | ||
| @pytest.mark.parametrize( | ||
@@ -51,3 +48,3 @@ "setting", | ||
| ) | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -74,3 +71,3 @@ oidc_rp_client_id="testid", | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -90,3 +87,3 @@ oidc_rp_client_id="testid", | ||
| @oidcconfig( | ||
| @pytest.mark.oidcconfig( | ||
| enabled=True, | ||
@@ -104,4 +101,3 @@ oidc_rp_client_id="testid", | ||
| request = rf.get("/") | ||
| request.session = SessionBase() | ||
| request.session.update({CONFIG_IDENTIFIER_SESSION_KEY: dummy_config.identifier}) | ||
| request.session = {CONFIG_IDENTIFIER_SESSION_KEY: dummy_config.identifier} | ||
@@ -108,0 +104,0 @@ mocker.patch.object(middleware, "is_refreshable_url", return_value=True) |
@@ -9,6 +9,4 @@ import pytest | ||
| from .conftest import oidcconfig | ||
| @oidcconfig(enabled=True) | ||
| @pytest.mark.oidcconfig(enabled=True) | ||
| def test_templatetag_admin_oidc_enabled(filled_admin_config): | ||
@@ -15,0 +13,0 @@ retrieved_client = get_oidc_admin_client() |
Sorry, the diff of this file is not supported yet
| """ | ||
| Keycloak helpers taken from mozilla-django-oidc-db::tests/utils.py & pytest fixtures. | ||
| These help dealing with/stubbing out OpenID Provider configuration. | ||
| The Keycloak client ID/secret and URLs are set up for the config in | ||
| docker/docker-compose.keycloak.yml. See the README.md in docker/keycloak/ for more | ||
| information. | ||
| """ | ||
| from contextlib import nullcontext | ||
| from pyquery import PyQuery as pq | ||
| from requests import Session | ||
| def keycloak_login( | ||
| login_url: str, | ||
| username: str = "testuser", | ||
| password: str = "testuser", | ||
| host: str = "http://testserver/", | ||
| session: Session | None = None, | ||
| ) -> str: | ||
| """ | ||
| Test helper to perform a keycloak login. | ||
| :param login_url: A login URL for keycloak with all query string parameters. E.g. | ||
| `client.get(reverse("login"))["Location"]`. | ||
| :returns: The redirect URI to consume in the django application, with the ``code`` | ||
| ``state`` query parameters. Consume this with ``response = client.get(url)``. | ||
| """ | ||
| cm = Session() if session is None else nullcontext(session) | ||
| with cm as session: | ||
| login_page = session.get(login_url) | ||
| assert login_page.status_code == 200 | ||
| # process keycloak's login form and submit the username + password to | ||
| # authenticate | ||
| document = pq(login_page.text) | ||
| login_form = document("form#kc-form-login") | ||
| submit_url = login_form.attr("action") | ||
| assert isinstance(submit_url, str) | ||
| login_response = session.post( | ||
| submit_url, | ||
| data={ | ||
| "username": username, | ||
| "password": password, | ||
| "credentialId": "", | ||
| "login": "Sign In", | ||
| }, | ||
| allow_redirects=False, | ||
| ) | ||
| assert login_response.status_code == 302 | ||
| assert (redirect_uri := login_response.headers["Location"]).startswith(host) | ||
| return redirect_uri |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
252621
-9.16%69
-2.82%5087
-3.6%