diff --git a/passbook/core/models.py b/passbook/core/models.py index d895bc318..530c08b60 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -304,7 +304,9 @@ class PropertyMapping(UUIDModel): form = "" objects = InheritanceManager() - def evaluate(self, user: User, request: HttpRequest, **kwargs) -> Any: + def evaluate( + self, user: Optional[User], request: Optional[HttpRequest], **kwargs + ) -> Any: """Evaluate `self.expression` using `**kwargs` as Context.""" try: expression = NATIVE_ENVIRONMENT.from_string(self.expression) diff --git a/passbook/factors/otp/views.py b/passbook/factors/otp/views.py index 22a3c23ca..1ff3da602 100644 --- a/passbook/factors/otp/views.py +++ b/passbook/factors/otp/views.py @@ -19,7 +19,7 @@ from structlog import get_logger from passbook.audit.models import Event, EventAction from passbook.factors.otp.forms import OTPSetupForm from passbook.factors.otp.utils import otpauth_url -from passbook.lib.boilerplate import NeverCacheMixin +from passbook.lib.mixins import NeverCacheMixin from passbook.lib.config import CONFIG OTP_SESSION_KEY = "passbook_factors_otp_key" diff --git a/passbook/lib/boilerplate.py b/passbook/lib/boilerplate.py deleted file mode 100644 index 38c12800f..000000000 --- a/passbook/lib/boilerplate.py +++ /dev/null @@ -1,12 +0,0 @@ -"""passbook django boilerplate code""" -from django.utils.decorators import method_decorator -from django.views.decorators.cache import never_cache - - -class NeverCacheMixin: - """Use never_cache as mixin for CBV""" - - @method_decorator(never_cache) - def dispatch(self, *args, **kwargs): - """Use never_cache as mixin for CBV""" - return super().dispatch(*args, **kwargs) diff --git a/passbook/lib/mixins.py b/passbook/lib/mixins.py index c83b0676a..4c7a02837 100644 --- a/passbook/lib/mixins.py +++ b/passbook/lib/mixins.py @@ -1,4 +1,5 @@ """passbook util mixins""" +from django.views.decorators.cache import never_cache from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt @@ -10,3 +11,12 @@ class CSRFExemptMixin: def dispatch(self, *args, **kwargs): """wrapper to apply @csrf_exempt to CBV""" return super().dispatch(*args, **kwargs) + + +class NeverCacheMixin: + """Use never_cache as mixin for CBV""" + + @method_decorator(never_cache) + def dispatch(self, *args, **kwargs): + """Use never_cache as mixin for CBV""" + return super().dispatch(*args, **kwargs) diff --git a/passbook/policies/struct.py b/passbook/policies/struct.py index 126602690..99bb890cc 100644 --- a/passbook/policies/struct.py +++ b/passbook/policies/struct.py @@ -1,7 +1,7 @@ """policy structures""" from __future__ import annotations -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Tuple from django.db.models import Model from django.http import HttpRequest @@ -27,8 +27,8 @@ class PolicyRequest: class PolicyResult: """Small data-class to hold policy results""" - passing: bool = False - messages: List[str] = [] + passing: bool + messages: Tuple[str] def __init__(self, passing: bool, *messages: str): self.passing = passing diff --git a/passbook/providers/oidc/lib.py b/passbook/providers/oidc/lib.py index 73bb953d4..334607afa 100644 --- a/passbook/providers/oidc/lib.py +++ b/passbook/providers/oidc/lib.py @@ -15,24 +15,32 @@ from passbook.policies.engine import PolicyEngine LOGGER = get_logger() +def client_related_provider(client: Client) -> Optional[Provider]: + """Lookup related Application from Client""" + # because oidc_provider is also used by app_gw, we can't be + # sure an OpenIDPRovider instance exists. hence we look through all related models + # and choose the one that inherits from Provider, which is guaranteed to + # have the application property + collector = Collector(using="default") + collector.collect([client]) + for _, related in collector.data.items(): + related_object = next(iter(related)) + if isinstance(related_object, Provider): + return related_object + return None + + def check_permissions( request: HttpRequest, user: User, client: Client ) -> Optional[HttpResponse]: """Check permissions, used for https://django-oidc-provider.readthedocs.io/en/latest/ sections/settings.html#oidc-after-userlogin-hook""" + provider = client_related_provider(client) + if not provider: + return redirect("passbook_providers_oauth:oauth2-permission-denied") try: - # because oidc_provider is also used by app_gw, we can't be - # sure an OpenIDPRovider instance exists. hence we look through all related models - # and choose the one that inherits from Provider, which is guaranteed to - # have the application property - collector = Collector(using="default") - collector.collect([client]) - for _, related in collector.data.items(): - related_object = next(iter(related)) - if isinstance(related_object, Provider): - application = related_object.application - break + application = provider.application except Application.DoesNotExist: return redirect("passbook_providers_oauth:oauth2-permission-denied") LOGGER.debug( diff --git a/passbook/providers/saml/views.py b/passbook/providers/saml/views.py index a065e1805..2675dfbeb 100644 --- a/passbook/providers/saml/views.py +++ b/passbook/providers/saml/views.py @@ -42,7 +42,11 @@ class AccessRequiredView(AccessMixin, View): application = get_object_or_404( Application, slug=self.kwargs["application"] ) - self._provider = get_object_or_404(SAMLProvider, pk=application.provider_id) + provider: SAMLProvider = get_object_or_404( + SAMLProvider, pk=application.provider_id + ) + self._provider = provider + return self._provider return self._provider def _has_access(self) -> bool: diff --git a/passbook/sources/oauth/views/core.py b/passbook/sources/oauth/views/core.py index fded716a8..0ac48a28b 100644 --- a/passbook/sources/oauth/views/core.py +++ b/passbook/sources/oauth/views/core.py @@ -1,4 +1,5 @@ """Core OAauth Views""" +from typing import Callable, Optional from django.conf import settings from django.contrib import messages @@ -23,7 +24,7 @@ LOGGER = get_logger() class OAuthClientMixin: "Mixin for getting OAuth client for a source." - client_class = None + client_class: Optional[Callable] = None def get_client(self, source): "Get instance of the OAuth client for this source." diff --git a/passbook/sources/saml/models.py b/passbook/sources/saml/models.py index ae4cb6c6b..273380382 100644 --- a/passbook/sources/saml/models.py +++ b/passbook/sources/saml/models.py @@ -21,13 +21,15 @@ class SAMLSource(Source): @property def login_button(self): - url = reverse_lazy("passbook_sources_saml:login", kwargs={"source": self.slug}) + url = reverse_lazy( + "passbook_sources_saml:login", kwargs={"source_slug": self.slug} + ) return url, "", self.name @property def additional_info(self): metadata_url = reverse_lazy( - "passbook_sources_saml:metadata", kwargs={"source": self} + "passbook_sources_saml:metadata", kwargs={"source_slug": self} ) return f'Metadata Download' diff --git a/passbook/sources/saml/urls.py b/passbook/sources/saml/urls.py index d5e5ad04d..22b0a8fd2 100644 --- a/passbook/sources/saml/urls.py +++ b/passbook/sources/saml/urls.py @@ -4,8 +4,8 @@ from django.urls import path from passbook.sources.saml.views import ACSView, InitiateView, MetadataView, SLOView urlpatterns = [ - path("/", InitiateView.as_view(), name="login"), - path("/acs/", ACSView.as_view(), name="acs"), - path("/slo/", SLOView.as_view(), name="slo"), - path("/metadata/", MetadataView.as_view(), name="metadata"), + path("/", InitiateView.as_view(), name="login"), + path("/acs/", ACSView.as_view(), name="acs"), + path("/slo/", SLOView.as_view(), name="slo"), + path("/metadata/", MetadataView.as_view(), name="metadata"), ] diff --git a/passbook/sources/saml/views.py b/passbook/sources/saml/views.py index 5610f060f..18a07f5ba 100644 --- a/passbook/sources/saml/views.py +++ b/passbook/sources/saml/views.py @@ -24,9 +24,9 @@ from passbook.sources.saml.xml_render import get_authnrequest_xml class InitiateView(View): """Get the Form with SAML Request, which sends us to the IDP""" - def get(self, request: HttpRequest, source: str) -> HttpResponse: + def get(self, request: HttpRequest, source_slug: str) -> HttpResponse: """Replies with an XHTML SSO Request.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source) + source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) if not source.enabled: raise Http404 sso_destination = request.GET.get("next", None) @@ -56,9 +56,9 @@ class InitiateView(View): class ACSView(View): """AssertionConsumerService, consume assertion and log user in""" - def post(self, request: HttpRequest, source: str) -> HttpResponse: + def post(self, request: HttpRequest, source_slug: str) -> HttpResponse: """Handles a POSTed SSO Assertion and logs the user in.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source) + source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) if not source.enabled: raise Http404 # sso_session = request.POST.get('RelayState', None) @@ -74,9 +74,9 @@ class ACSView(View): class SLOView(View): """Single-Logout-View""" - def dispatch(self, request: HttpRequest, source: str) -> HttpResponse: + def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse: """Replies with an XHTML SSO Request.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source) + source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) if not source.enabled: raise Http404 logout(request) @@ -93,9 +93,9 @@ class SLOView(View): class MetadataView(View): """Return XML Metadata for IDP""" - def dispatch(self, request: HttpRequest, source: str) -> HttpResponse: + def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse: """Replies with the XML Metadata SPSSODescriptor.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source) + source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) entity_id = get_entity_id(request, source) return render_xml( request, diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000..95021473a --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,3 @@ +{ + "reportMissingTypeStubs": false +}