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""" """passbook access helper classes"""
from typing import Optional
from django.contrib import messages from django.contrib import messages
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -11,13 +13,16 @@ from passbook.policies.types import PolicyResult
LOGGER = get_logger() 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. """Mixin class for usage in Authorization views.
Provider functions to check application access, etc""" 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: def provider_to_application(self, provider: Provider) -> Application:
"""Lookup application assigned to provider, throw error if no application assigned""" """Lookup application assigned to provider, throw error if no application assigned"""
try: try:
@ -32,9 +37,20 @@ class AccessMixin:
) )
raise exc 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.""" """Check if user has access to application."""
LOGGER.debug("Checking permissions", user=user, application=application) user = user or self.request.user
policy_engine = PolicyEngine(application, user, self.request) policy_engine = PolicyEngine(
application, user or self.request.user, self.request
)
policy_engine.build() 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""" """passbook OAuth2 Views"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect 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.audit.models import Event, EventAction
from passbook.core.models import Application from passbook.core.models import Application
from passbook.core.views.access import AccessMixin
from passbook.flows.models import in_memory_stage from passbook.flows.models import in_memory_stage
from passbook.flows.planner import ( from passbook.flows.planner import (
PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_APPLICATION,
@ -21,6 +19,7 @@ from passbook.flows.planner import (
from passbook.flows.stage import StageView from passbook.flows.stage import StageView
from passbook.flows.views import SESSION_KEY_PLAN from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.urls import redirect_with_qs from passbook.lib.utils.urls import redirect_with_qs
from passbook.policies.mixins import PolicyAccessMixin
from passbook.providers.oauth.models import OAuth2Provider from passbook.providers.oauth.models import OAuth2Provider
from passbook.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE from passbook.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
@ -38,7 +37,7 @@ PLAN_CONTEXT_NONCE = "nonce"
PLAN_CONTEXT_SCOPE_DESCRIPTION = "scope_descriptions" 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""" """OAuth2 Flow initializer, checks access to application and starts flow"""
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -51,10 +50,8 @@ class AuthorizationFlowInitView(AccessMixin, LoginRequiredMixin, View):
except Application.DoesNotExist: except Application.DoesNotExist:
return redirect("passbook_providers_oauth:oauth2-permission-denied") return redirect("passbook_providers_oauth:oauth2-permission-denied")
# Check permissions # Check permissions
result = self.user_has_access(application, request.user) result = self.user_has_access(application)
if not result.passing: if not result.passing:
for policy_message in result.messages:
messages.error(request, policy_message)
return redirect("passbook_providers_oauth:oauth2-permission-denied") return redirect("passbook_providers_oauth:oauth2-permission-denied")
# Regardless, we start the planner and return to it # Regardless, we start the planner and return to it
planner = FlowPlanner(provider.authorization_flow) planner = FlowPlanner(provider.authorization_flow)

View File

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

View File

@ -1,7 +1,6 @@
"""passbook SAML IDP Views""" """passbook SAML IDP Views"""
from typing import Optional from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.validators import URLValidator 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.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext as _
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from signxml.util import strip_pem_header 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.template import render_to_string
from passbook.lib.utils.urls import redirect_with_qs from passbook.lib.utils.urls import redirect_with_qs
from passbook.lib.views import bad_request_message 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.exceptions import CannotHandleAssertion
from passbook.providers.saml.models import SAMLBindings, SAMLProvider from passbook.providers.saml.models import SAMLBindings, SAMLProvider
from passbook.providers.saml.processors.types import SAMLResponseParams from passbook.providers.saml.processors.types import SAMLResponseParams
@ -42,34 +40,13 @@ SESSION_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_PARAMS = "SAMLParams" SESSION_KEY_PARAMS = "SAMLParams"
class SAMLAccessMixin: class SAMLSSOView(LoginRequiredMixin, PolicyAccessMixin, View):
"""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):
""""SAML SSO Base View, which plans a flow and injects our final stage. """"SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler.""" Calls get/post handler."""
application: Application
provider: SAMLProvider
def dispatch( def dispatch(
self, request: HttpRequest, *args, application_slug: str, **kwargs self, request: HttpRequest, *args, application_slug: str, **kwargs
) -> HttpResponse: ) -> HttpResponse:
@ -77,7 +54,7 @@ class SAMLSSOView(LoginRequiredMixin, SAMLAccessMixin, View):
self.provider: SAMLProvider = get_object_or_404( self.provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=self.application.provider_id SAMLProvider, pk=self.application.provider_id
) )
if not self._has_access(): if not self.user_has_access(self.application):
raise PermissionDenied() raise PermissionDenied()
# Call the method handler, which checks the SAML Request # Call the method handler, which checks the SAML Request
method_response = super().dispatch(request, *args, application_slug, **kwargs) 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 django.views import View
from passbook.core.models import Application 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.constants import SESSION_KEY
from passbook.providers.samlv2.saml.parser import SAMLRequest from passbook.providers.samlv2.saml.parser import SAMLRequest
class BaseSAMLView(AccessMixin, View): class BaseSAMLView(PolicyAccessMixin, View):
"""Base SAML View to resolve app_slug""" """Base SAML View to resolve app_slug"""
application: Application application: Application