diff --git a/passbook/policies/http.py b/passbook/policies/http.py new file mode 100644 index 000000000..f8c668060 --- /dev/null +++ b/passbook/policies/http.py @@ -0,0 +1,44 @@ +"""policy http response""" +from typing import Any, Dict, Optional + +from django.http.request import HttpRequest +from django.template.response import TemplateResponse +from django.utils.translation import gettext as _ + +from passbook.core.models import PASSBOOK_USER_DEBUG +from passbook.policies.types import PolicyResult + + +class AccessDeniedResponse(TemplateResponse): + """Response used for access denied messages. Can optionally show an error message, + and if the user is a superuser or has user_debug enabled, shows a policy result.""" + + title: str + + error_message: Optional[str] = None + policy_result: Optional[PolicyResult] = None + + def __init__(self, request: HttpRequest) -> None: + # For some reason pyright complains about keyword argument usage here + # pyright: reportGeneralTypeIssues=false + super().__init__(request=request, template="policies/denied.html") + self.title = _("Access denied") + + def resolve_context( + self, context: Optional[Dict[str, Any]] + ) -> Optional[Dict[str, Any]]: + if not context: + context = {} + context["title"] = self.title + if self.error_message: + context["error"] = self.error_message + # Only show policy result if user is authenticated and + # either superuser or has PASSBOOK_USER_DEBUG set + if self.policy_result: + if self._request.user and self._request.user.is_authenticated: + if ( + self._request.user.is_superuser + or self._request.user.attributes.get(PASSBOOK_USER_DEBUG, False) + ): + context["policy_result"] = self.policy_result + return context diff --git a/passbook/policies/mixins.py b/passbook/policies/mixins.py index ff513a7b3..755e7fb87 100644 --- a/passbook/policies/mixins.py +++ b/passbook/policies/mixins.py @@ -5,16 +5,13 @@ from django.contrib import messages from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.views import redirect_to_login from django.http import HttpRequest, HttpResponse -from django.shortcuts import redirect from django.utils.translation import gettext as _ from structlog import get_logger from passbook.core.models import Application, Provider, User -from passbook.flows.views import ( - SESSION_KEY_APPLICATION_PRE, - SESSION_KEY_DENIED_POLICY_RESULT, -) +from passbook.flows.views import SESSION_KEY_APPLICATION_PRE from passbook.policies.engine import PolicyEngine +from passbook.policies.http import AccessDeniedResponse from passbook.policies.types import PolicyResult LOGGER = get_logger() @@ -31,6 +28,9 @@ class PolicyAccessMixin(BaseMixin, AccessMixin): Provider functions to check application access, etc""" def handle_no_permission(self, application: Optional[Application] = None): + """User has no access and is not authenticated, so we remember the application + they try to access and redirect to the login URL. The application is saved to show + a hint on the Identification Stage what the user should login for.""" if application: self.request.session[SESSION_KEY_APPLICATION_PRE] = application return redirect_to_login( @@ -43,10 +43,10 @@ class PolicyAccessMixin(BaseMixin, AccessMixin): self, result: Optional[PolicyResult] = None ) -> HttpResponse: """Function called when user has no permissions but is authenticated""" + response = AccessDeniedResponse(self.request) if result: - self.request.session[SESSION_KEY_DENIED_POLICY_RESULT] = result - # TODO: Remove this URL and render the view instead - return redirect("passbook_flows:denied") + response.policy_result = result + return response def provider_to_application(self, provider: Provider) -> Application: """Lookup application assigned to provider, throw error if no application assigned""" diff --git a/passbook/flows/templates/flows/denied.html b/passbook/policies/templates/policies/denied.html similarity index 100% rename from passbook/flows/templates/flows/denied.html rename to passbook/policies/templates/policies/denied.html