providers/*: use PolicyAccessMixin to simplify

This commit is contained in:
Jens Langhammer 2020-07-01 23:18:10 +02:00
parent 310b31a8b7
commit cc0b8164b0
5 changed files with 38 additions and 51 deletions

View File

@ -1,4 +1,6 @@
"""passbook access helper classes"""
from typing import Optional
from django.contrib import messages
from django.http import HttpRequest
from django.utils.translation import gettext as _
@ -11,13 +13,16 @@ from passbook.policies.types import PolicyResult
LOGGER = get_logger()
class AccessMixin:
class BaseMixin:
"""Base Mixin class, used to annotate View Member variables"""
request: HttpRequest
class PolicyAccessMixin(BaseMixin):
"""Mixin class for usage in Authorization views.
Provider functions to check application access, etc"""
# request is set by view but since this Mixin has no base class
request: HttpRequest = None
def provider_to_application(self, provider: Provider) -> Application:
"""Lookup application assigned to provider, throw error if no application assigned"""
try:
@ -32,9 +37,20 @@ class AccessMixin:
)
raise exc
def user_has_access(self, application: Application, user: User) -> PolicyResult:
def user_has_access(
self, application: Application, user: Optional[User] = None
) -> PolicyResult:
"""Check if user has access to application."""
LOGGER.debug("Checking permissions", user=user, application=application)
policy_engine = PolicyEngine(application, user, self.request)
user = user or self.request.user
policy_engine = PolicyEngine(
application, user or self.request.user, self.request
)
policy_engine.build()
return policy_engine.result
result = policy_engine.result
LOGGER.debug(
"AccessMixin user_has_access", user=user, app=application, result=result,
)
if not result.passing:
for message in result.messages:
messages.error(self.request, _(message))
return result

View File

@ -1,5 +1,4 @@
"""passbook OAuth2 Views"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
@ -11,7 +10,6 @@ from structlog import get_logger
from passbook.audit.models import Event, EventAction
from passbook.core.models import Application
from passbook.core.views.access import AccessMixin
from passbook.flows.models import in_memory_stage
from passbook.flows.planner import (
PLAN_CONTEXT_APPLICATION,
@ -21,6 +19,7 @@ from passbook.flows.planner import (
from passbook.flows.stage import StageView
from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.urls import redirect_with_qs
from passbook.policies.mixins import PolicyAccessMixin
from passbook.providers.oauth.models import OAuth2Provider
from passbook.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
@ -38,7 +37,7 @@ PLAN_CONTEXT_NONCE = "nonce"
PLAN_CONTEXT_SCOPE_DESCRIPTION = "scope_descriptions"
class AuthorizationFlowInitView(AccessMixin, LoginRequiredMixin, View):
class AuthorizationFlowInitView(PolicyAccessMixin, LoginRequiredMixin, View):
"""OAuth2 Flow initializer, checks access to application and starts flow"""
# pylint: disable=unused-argument
@ -51,10 +50,8 @@ class AuthorizationFlowInitView(AccessMixin, LoginRequiredMixin, View):
except Application.DoesNotExist:
return redirect("passbook_providers_oauth:oauth2-permission-denied")
# Check permissions
result = self.user_has_access(application, request.user)
result = self.user_has_access(application)
if not result.passing:
for policy_message in result.messages:
messages.error(request, policy_message)
return redirect("passbook_providers_oauth:oauth2-permission-denied")
# Regardless, we start the planner and return to it
planner = FlowPlanner(provider.authorization_flow)

View File

@ -1,5 +1,4 @@
"""passbook OIDC Views"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, reverse
@ -11,7 +10,6 @@ from oidc_provider.views import AuthorizeView
from structlog import get_logger
from passbook.core.models import Application
from passbook.core.views.access import AccessMixin
from passbook.flows.models import in_memory_stage
from passbook.flows.planner import (
PLAN_CONTEXT_APPLICATION,
@ -22,6 +20,7 @@ from passbook.flows.planner import (
from passbook.flows.stage import StageView
from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.urls import redirect_with_qs
from passbook.policies.mixins import PolicyAccessMixin
from passbook.providers.oidc.models import OpenIDProvider
from passbook.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
@ -31,7 +30,7 @@ PLAN_CONTEXT_PARAMS = "params"
PLAN_CONTEXT_SCOPES = "scopes"
class AuthorizationFlowInitView(AccessMixin, LoginRequiredMixin, View):
class AuthorizationFlowInitView(PolicyAccessMixin, LoginRequiredMixin, View):
"""OIDC Flow initializer, checks access to application and starts flow"""
# pylint: disable=unused-argument
@ -44,10 +43,8 @@ class AuthorizationFlowInitView(AccessMixin, LoginRequiredMixin, View):
except Application.DoesNotExist:
return redirect("passbook_providers_oauth:oauth2-permission-denied")
# Check permissions
result = self.user_has_access(application, request.user)
result = self.user_has_access(application)
if not result.passing:
for policy_message in result.messages:
messages.error(request, policy_message)
return redirect("passbook_providers_oauth:oauth2-permission-denied")
# Extract params so we can save them in the plan context
endpoint = AuthorizeEndpoint(request)

View File

@ -1,7 +1,6 @@
"""passbook SAML IDP Views"""
from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.core.validators import URLValidator
@ -9,7 +8,6 @@ from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.utils.translation import gettext as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from signxml.util import strip_pem_header
@ -28,7 +26,7 @@ from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.template import render_to_string
from passbook.lib.utils.urls import redirect_with_qs
from passbook.lib.views import bad_request_message
from passbook.policies.engine import PolicyEngine
from passbook.policies.mixins import PolicyAccessMixin
from passbook.providers.saml.exceptions import CannotHandleAssertion
from passbook.providers.saml.models import SAMLBindings, SAMLProvider
from passbook.providers.saml.processors.types import SAMLResponseParams
@ -42,34 +40,13 @@ SESSION_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_PARAMS = "SAMLParams"
class SAMLAccessMixin:
"""SAML base access mixin, checks access to an application based on its policies"""
request: HttpRequest
application: Application
provider: SAMLProvider
def _has_access(self) -> bool:
"""Check if user has access to application, add an error if not"""
policy_engine = PolicyEngine(self.application, self.request.user, self.request)
policy_engine.build()
result = policy_engine.result
LOGGER.debug(
"SAMLFlowInit _has_access",
user=self.request.user,
app=self.application,
result=result,
)
if not result.passing:
for message in result.messages:
messages.error(self.request, _(message))
return result.passing
class SAMLSSOView(LoginRequiredMixin, SAMLAccessMixin, View):
class SAMLSSOView(LoginRequiredMixin, PolicyAccessMixin, View):
""""SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler."""
application: Application
provider: SAMLProvider
def dispatch(
self, request: HttpRequest, *args, application_slug: str, **kwargs
) -> HttpResponse:
@ -77,7 +54,7 @@ class SAMLSSOView(LoginRequiredMixin, SAMLAccessMixin, View):
self.provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=self.application.provider_id
)
if not self._has_access():
if not self.user_has_access(self.application):
raise PermissionDenied()
# Call the method handler, which checks the SAML Request
method_response = super().dispatch(request, *args, application_slug, **kwargs)

View File

@ -6,12 +6,12 @@ from django.shortcuts import get_object_or_404
from django.views import View
from passbook.core.models import Application
from passbook.core.views.access import AccessMixin
from passbook.policies.mixins import PolicyAccessMixin
from passbook.providers.samlv2.saml.constants import SESSION_KEY
from passbook.providers.samlv2.saml.parser import SAMLRequest
class BaseSAMLView(AccessMixin, View):
class BaseSAMLView(PolicyAccessMixin, View):
"""Base SAML View to resolve app_slug"""
application: Application