From 3f2ceb81e8c95f41f4ec036e04ebc50f24b00202 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 11 Feb 2025 11:38:45 -0500 Subject: [PATCH] decorators on domain views --- src/registrar/config/urls.py | 6 ++- src/registrar/decorators.py | 63 ++++++++++++++++++---- src/registrar/registrar_middleware.py | 12 ++--- src/registrar/views/domain.py | 25 ++++++++- src/registrar/views/index.py | 1 + src/registrar/views/utility/error_views.py | 2 +- 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 27ad01dc8..cf440256e 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -297,7 +297,11 @@ urlpatterns = [ name="todo", ), path("domain/", views.DomainView.as_view(), name="domain"), - path("domain//prototype-dns", views.PrototypeDomainDNSRecordView.as_view(), name="prototype-domain-dns"), + path( + "domain//prototype-dns", + views.PrototypeDomainDNSRecordView.as_view(), + name="prototype-domain-dns", + ), path("domain//users", views.DomainUsersView.as_view(), name="domain-users"), path( "domain//dns", diff --git a/src/registrar/decorators.py b/src/registrar/decorators.py index 177dfab62..ff8da3eed 100644 --- a/src/registrar/decorators.py +++ b/src/registrar/decorators.py @@ -1,7 +1,7 @@ import functools from django.core.exceptions import PermissionDenied from django.utils.decorators import method_decorator -from registrar.models import DomainInformation, DomainRequest, UserDomainRole +from registrar.models import Domain, DomainInformation, DomainRequest, UserDomainRole # Constants for clarity ALL = "all" @@ -9,6 +9,11 @@ IS_SUPERUSER = "is_superuser" IS_STAFF = "is_staff" IS_DOMAIN_MANAGER = "is_domain_manager" IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain" +IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager" +IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member" +HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all" +# HAS_PORTFOLIO_DOMAINS_VIEW_MANAGED = "has_portfolio_domains_view_managed" + def grant_access(*rules): """ @@ -28,7 +33,7 @@ def grant_access(*rules): if not _user_has_permission(request.user, request, rules, **kwargs): raise PermissionDenied return original_dispatch(self, request, *args, **kwargs) - + view.dispatch = wrapped_dispatch # replace dispatch with wrapped version return view @@ -43,11 +48,11 @@ def grant_access(*rules): if not _user_has_permission(request.user, request, rules, **kwargs): raise PermissionDenied return view(request, *args, **kwargs) - + return wrapper - + return decorator - + def _user_has_permission(user, request, rules, **kwargs): """ @@ -75,19 +80,45 @@ def _user_has_permission(user, request, rules, **kwargs): conditions_met.append(user.is_superuser) if not any(conditions_met) and IS_DOMAIN_MANAGER in rules: - domain_id = kwargs.get('domain_pk') - # Check UserDomainRole directly instead of fetching Domain - has_permission = UserDomainRole.objects.filter(user=user, domain_id=domain_id).exists() + domain_id = kwargs.get("domain_pk") + has_permission = _is_domain_manager(user, domain_id) conditions_met.append(has_permission) if not any(conditions_met) and IS_STAFF_MANAGING_DOMAIN in rules: - domain_id = kwargs.get('domain_pk') + domain_id = kwargs.get("domain_pk") has_permission = _can_access_other_user_domains(request, domain_id) conditions_met.append(has_permission) + if not any(conditions_met) and HAS_PORTFOLIO_DOMAINS_VIEW_ALL in rules: + domain_id = kwargs.get("domain_pk") + has_permission = _can_access_domain_via_portfolio_view_all_domains(request, domain_id) + conditions_met.append(has_permission) + + if not any(conditions_met) and IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER in rules: + domain_id = kwargs.get("domain_pk") + has_permission = _is_domain_manager(user, domain_id) and _is_portfolio_member(request) + conditions_met.append(has_permission) + + if not any(conditions_met) and IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER in rules: + domain_id = kwargs.get("domain_pk") + has_permission = _is_domain_manager(user, domain_id) and not _is_portfolio_member(request) + conditions_met.append(has_permission) + return any(conditions_met) +def _is_domain_manager(user, domain_pk): + """Checks to see if the user is a domain manager of the + domain with domain_pk.""" + return UserDomainRole.objects.filter(user=user, domain_id=domain_pk).exists() + + +def _is_portfolio_member(request): + """Checks to see if the user in the request is a member of the + portfolio in the request's session.""" + return request.user.is_org_user(request) + + def _can_access_other_user_domains(request, domain_pk): """Checks to see if an authorized user (staff or superuser) can access a domain that they did not create or were invited to. @@ -144,3 +175,17 @@ def _can_access_other_user_domains(request, domain_pk): # the user is permissioned, # and it is in a valid status return True + + +def _can_access_domain_via_portfolio_view_all_domains(request, domain_pk): + """Returns whether the user in the request can access the domain + via portfolio view all domains permission.""" + # NOTE: determine if in practice this ever needs to be called on its own + # or if it can be combined with view_managed_domains + portfolio = request.session.get("portfolio") + if request.user.has_view_all_domains_portfolio_permission(portfolio): + if Domain.objects.filter(id=domain_pk).exists(): + domain = Domain.objects.get(id=domain_pk) + if domain.domain_info.portfolio == portfolio: + return True + return False diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index e3dcbe788..efef79063 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -177,17 +177,17 @@ class CheckPortfolioMiddleware: class RestrictAccessMiddleware: - """ Middleware that blocks all views unless explicitly permitted """ + """Middleware that blocks all views unless explicitly permitted""" def __init__(self, get_response): self.get_response = get_response self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])] - + def __call__(self, request): # Allow requests that match LOGIN_REQUIRED_IGNORE_PATHS if any(pattern.match(request.path) for pattern in self.ignored_paths): return self.get_response(request) - + # Try to resolve the view function try: resolver_match = resolve(request.path_info) @@ -199,13 +199,13 @@ class RestrictAccessMiddleware: # Auto-allow Django's built-in admin views (but NOT custom /admin/* views) if app_name == "admin": return self.get_response(request) - + # Skip access restriction if the view explicitly allows unauthenticated access if getattr(view_func, "login_required", True) is False: return self.get_response(request) - + # Enforce explicit access fules for other views if not getattr(view_func, "has_explicit_access", False): raise PermissionDenied - return self.get_response(request) \ No newline at end of file + return self.get_response(request) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 03860dc02..d56bbb7f8 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -15,7 +15,14 @@ from django.shortcuts import redirect, render, get_object_or_404 from django.urls import reverse from django.views.generic.edit import FormMixin from django.conf import settings -from registrar.decorators import IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN, grant_access +from registrar.decorators import ( + HAS_PORTFOLIO_DOMAINS_VIEW_ALL, + IS_DOMAIN_MANAGER, + IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, + IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, + IS_STAFF_MANAGING_DOMAIN, + grant_access, +) from registrar.forms.domain import DomainSuborganizationForm, DomainRenewalForm from registrar.models import ( Domain, @@ -257,7 +264,8 @@ class DomainFormBaseView(DomainBaseView, FormMixin): exc_info=True, ) -@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) + +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN, HAS_PORTFOLIO_DOMAINS_VIEW_ALL) class DomainView(DomainBaseView): """Domain detail overview page.""" @@ -311,6 +319,7 @@ class DomainView(DomainBaseView): self.object = self.get_object() self._update_session_with_domain() + @grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainRenewalView(DomainBaseView): """Domain detail overview page.""" @@ -380,6 +389,7 @@ class DomainRenewalView(DomainBaseView): }, ) + @grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainOrgNameAddressView(DomainFormBaseView): """Organization view""" @@ -422,6 +432,7 @@ class DomainOrgNameAddressView(DomainFormBaseView): return super().has_permission() +@grant_access(IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER) class DomainSubOrganizationView(DomainFormBaseView): """Suborganization view""" @@ -468,6 +479,7 @@ class DomainSubOrganizationView(DomainFormBaseView): return super().form_valid(form) +@grant_access(IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER) class DomainSeniorOfficialView(DomainFormBaseView): """Domain senior official editing view.""" @@ -525,6 +537,7 @@ class DomainSeniorOfficialView(DomainFormBaseView): return super().has_permission() +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainDNSView(DomainBaseView): """DNS Information View.""" @@ -741,6 +754,7 @@ class PrototypeDomainDNSRecordView(DomainFormBaseView): return super().post(request) +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainNameserversView(DomainFormBaseView): """Domain nameserver editing view.""" @@ -868,6 +882,7 @@ class DomainNameserversView(DomainFormBaseView): return super().form_valid(formset) +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainDNSSECView(DomainFormBaseView): """Domain DNSSEC editing view.""" @@ -905,6 +920,7 @@ class DomainDNSSECView(DomainFormBaseView): return self.form_valid(form) +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainDsDataView(DomainFormBaseView): """Domain DNSSEC ds data editing view.""" @@ -1023,6 +1039,7 @@ class DomainDsDataView(DomainFormBaseView): return super().form_valid(formset) +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainSecurityEmailView(DomainFormBaseView): """Domain security email editing view.""" @@ -1094,6 +1111,7 @@ class DomainSecurityEmailView(DomainFormBaseView): return redirect(self.get_success_url()) +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainUsersView(DomainBaseView): """Domain managers page in the domain details.""" @@ -1189,6 +1207,7 @@ class DomainUsersView(DomainBaseView): return context +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainAddUserView(DomainFormBaseView): """Inside of a domain's user management, a form for adding users. @@ -1286,6 +1305,7 @@ class DomainAddUserView(DomainFormBaseView): messages.success(self.request, f"Added user {email}.") +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainInvitationCancelView(SuccessMessageMixin, DomainInvitationPermissionCancelView): object: DomainInvitation fields = [] @@ -1311,6 +1331,7 @@ class DomainInvitationCancelView(SuccessMessageMixin, DomainInvitationPermission return f"Canceled invitation to {self.object.email}." +@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) class DomainDeleteUserView(UserDomainRolePermissionDeleteView): """Inside of a domain's user management, a form for deleting users.""" diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index d9ff9b209..097bf2c89 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -2,6 +2,7 @@ from django.shortcuts import render from registrar.decorators import grant_access, ALL + @grant_access(ALL) def index(request): """This page is available to anyone without logging in.""" diff --git a/src/registrar/views/utility/error_views.py b/src/registrar/views/utility/error_views.py index bc6536b2b..4f9635d09 100644 --- a/src/registrar/views/utility/error_views.py +++ b/src/registrar/views/utility/error_views.py @@ -42,4 +42,4 @@ def custom_404_error_view(request, exception=None, context=None): print("this is called") if context is None: context = {} - return render(request, "404.html", context=context, status=404) \ No newline at end of file + return render(request, "404.html", context=context, status=404)