Skip to content

Commit e4a173c

Browse files
🏷️ [#43] Restructure classes/types to increase type safety
Fixing one pyright error at a time... The protocols are replaced with abstract base classes, as that is effectively how they are used. This will also improve the type checking situation for downstream projects.
1 parent 6aa0c78 commit e4a173c

File tree

7 files changed

+170
-87
lines changed

7 files changed

+170
-87
lines changed

mozilla_django_oidc_db/backends.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4-
from typing import Any, cast, override
4+
from typing import Any, override
55

66
from django.contrib.auth import get_user_model
77
from django.contrib.auth.models import (
@@ -19,10 +19,7 @@
1919
from .exceptions import MissingInitialisation
2020
from .jwt import verify_and_decode_token
2121
from .models import OIDCClient, UserInformationClaimsSources
22-
from .plugins import (
23-
AbstractUserOIDCPluginProtocol,
24-
AnonymousUserOIDCPluginProtocol,
25-
)
22+
from .plugins import AbstractUserOIDCPlugin, AnonymousUserOIDCPlugin
2623
from .registry import register as registry
2724
from .typing import JSONObject
2825
from .utils import extract_content_type
@@ -75,10 +72,12 @@ def __init__(self, *args, **kwargs) -> None:
7572

7673
# django-stubs returns AbstractBaseUser, but we depend on properties of
7774
# AbstractUser.
78-
self.UserModel = cast(AbstractUser, get_user_model())
75+
UserModel = get_user_model()
76+
assert issubclass(UserModel, AbstractUser)
77+
self.UserModel = UserModel
7978

8079
@override
81-
def get_settings(self, attr: str, *args: Any) -> Any: # type: ignore
80+
def get_settings(self, attr: str, *args: Any) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
8281
"""
8382
Override the upstream library get_settings.
8483
@@ -106,7 +105,7 @@ def _check_candidate_backend(self) -> bool:
106105
# `OIDCAuthenticationCallbackView` for the `auth.authenticate(**kwargs)` call if this
107106
# needs updating.
108107
@override
109-
def authenticate( # type: ignore
108+
def authenticate( # pyright: ignore[reportIncompatibleMethodOverride]
110109
self,
111110
request: HttpRequest | None,
112111
nonce: str | None = None,
@@ -138,7 +137,7 @@ def authenticate( # type: ignore
138137
# Store the config class identifier on the user, so that we can store this in the user's
139138
# session after they have been successfully authenticated (by listening to the `user_logged_in` signal)
140139
if user:
141-
user._oidcdb_config_identifier = self.config.identifier # type: ignore
140+
user._oidcdb_config_identifier = self.config.identifier # pyright: ignore[reportAttributeAccessIssue]
142141

143142
return user
144143

@@ -149,7 +148,7 @@ def verify_claims(self, claims: JSONObject) -> bool:
149148
assert self.config
150149

151150
plugin = registry[self.config.identifier]
152-
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
151+
assert isinstance(plugin, AbstractUserOIDCPlugin)
153152
return plugin.verify_claims(claims)
154153

155154
@override
@@ -214,11 +213,11 @@ def get_userinfo(
214213
@override
215214
def filter_users_by_claims(
216215
self, claims: JSONObject
217-
) -> models.Manager[AbstractUser]: # type: ignore (parent function returns UserManager which is more specific than Manager)
216+
) -> models.QuerySet[AbstractUser]:
218217
assert self.config
219218
plugin = registry[self.config.identifier]
220219

221-
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
220+
assert isinstance(plugin, AbstractUserOIDCPlugin)
222221
return plugin.filter_users_by_claims(claims)
223222

224223
@override
@@ -227,30 +226,35 @@ def create_user(self, claims: JSONObject) -> AbstractUser:
227226
assert self.config
228227
plugin = registry[self.config.identifier]
229228

230-
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
229+
assert isinstance(plugin, AbstractUserOIDCPlugin)
231230
return plugin.create_user(claims)
232231

233232
@override
234233
def update_user(self, user: AbstractUser, claims: JSONObject):
235234
assert self.config
236235
plugin = registry[self.config.identifier]
237236

238-
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
237+
assert isinstance(plugin, AbstractUserOIDCPlugin)
239238
return plugin.update_user(user, claims)
240239

241240
@override
242241
def get_or_create_user(
243242
self, access_token: str, id_token: str, payload: JSONObject
244-
) -> AnonymousUser | AbstractUser | None: # type: ignore (parent function returns only an AbstractUser | None)
243+
) -> AnonymousUser | AbstractUser | None:
245244
"""Get or create a user based on the tokens received."""
246245
assert self.config
247246

248247
plugin = registry[self.config.identifier]
249248
assert isinstance(self.request, HttpRequest)
250249

251-
if isinstance(plugin, AnonymousUserOIDCPluginProtocol):
250+
# shortcut for "anonymous users" where the OIDC authentication *does* happen,
251+
# but no actual Django user instance is created.
252+
if isinstance(plugin, AnonymousUserOIDCPlugin):
252253
return plugin.get_or_create_user(
253254
access_token, id_token, payload, self.request
254255
)
255256

256-
return super().get_or_create_user(access_token, id_token, payload)
257+
user = super().get_or_create_user(access_token, id_token, payload)
258+
if user is not None:
259+
assert isinstance(user, self.UserModel)
260+
return user

mozilla_django_oidc_db/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
settings that are still defined in the django settings layer.
77
"""
88

9-
from typing import Any, Generic, Protocol, Self, TypeVar, Unpack, overload
9+
from typing import Any, Protocol, Self, TypeVar, Unpack, overload
1010

1111
from django.core.exceptions import (
1212
BadRequest,
@@ -70,7 +70,7 @@ class SettingsHolder(Protocol[T]):
7070
def get_settings(self, attr: str, *args: T) -> T: ...
7171

7272

73-
class DynamicSettingKwargs(TypedDict, Generic[T], total=False):
73+
class DynamicSettingKwargs[T](TypedDict, total=False):
7474
default: T
7575

7676

mozilla_django_oidc_db/forms.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ def get_endpoints_from_discovery(cls, base_url: str):
5656
return endpoints
5757

5858
def clean(self):
59-
cleaned_data = super().clean()
59+
super().clean()
6060

61-
discovery_endpoint = cleaned_data.get("oidc_op_discovery_endpoint")
61+
discovery_endpoint = self.cleaned_data.get("oidc_op_discovery_endpoint")
6262

6363
# Derive the endpoints from the discovery endpoint
6464
if discovery_endpoint:
6565
try:
6666
endpoints = self.get_endpoints_from_discovery(discovery_endpoint)
67-
cleaned_data.update(**endpoints)
67+
self.cleaned_data.update(**endpoints)
6868
except (
6969
requests.exceptions.RequestException,
7070
json.decoder.JSONDecodeError,
@@ -80,7 +80,7 @@ def clean(self):
8080
# Verify that the required endpoints were derived from the
8181
# discovery endpoint
8282
for field in self.required_endpoints:
83-
if not cleaned_data.get(field):
83+
if not self.cleaned_data.get(field):
8484
self.add_error(field, _("This field is required."))
8585

86-
return cleaned_data
86+
return self.cleaned_data

mozilla_django_oidc_db/middleware.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ def process_request(self, request):
6767

6868
return super().process_request(request)
6969

70+
# we can't use cached_property, because a middleware instance exists for the whole
71+
# duration of the django server life cycle, and the relevant config can change
72+
# between requests. See ``process_request``.
7073
@property
71-
def exempt_urls(self):
74+
def exempt_urls(self): # pyright: ignore[reportIncompatibleVariableOverride]
7275
# In many cases, the OIDC_AUTHENTICATION_CALLBACK_URL will be the generic
7376
# callback handler and already be part of super().exempt_urls. However, this is
7477
# not a given, and consumers might have implemented different callback handlers,

0 commit comments

Comments
 (0)