diff --git a/src/registrar/decorators.py b/src/registrar/decorators.py index 237985f02..98a37c62c 100644 --- a/src/registrar/decorators.py +++ b/src/registrar/decorators.py @@ -18,6 +18,8 @@ HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all" HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM = "has_portfolio_domain_requests_any_perm" HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all" HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT = "has_portfolio_domain_requests_edit" +HAS_PORTFOLIO_MEMBERS_ANY_PERM = "has_portfolio_members_any_perm" +HAS_PORTFOLIO_MEMBERS_EDIT = "has_portfolio_members_edit" def grant_access(*rules): @@ -142,6 +144,22 @@ def _user_has_permission(user, request, rules, **kwargs): print(has_permission) conditions_met.append(has_permission) + if not any(conditions_met) and HAS_PORTFOLIO_MEMBERS_ANY_PERM in rules: + portfolio = request.session.get("portfolio") + has_permission = user.is_org_user(request) and ( + user.has_view_members_portfolio_permission(portfolio) or + user.has_edit_members_portfolio_permission(portfolio) + ) + conditions_met.append(has_permission) + + if not any(conditions_met) and HAS_PORTFOLIO_MEMBERS_EDIT in rules: + portfolio = request.session.get("portfolio") + has_permission = ( + user.is_org_user(request) and + user.has_edit_members_portfolio_permission(portfolio) + ) + conditions_met.append(has_permission) + return any(conditions_met) diff --git a/src/registrar/views/member_domains_json.py b/src/registrar/views/member_domains_json.py index 3d24336bb..40b7bea74 100644 --- a/src/registrar/views/member_domains_json.py +++ b/src/registrar/views/member_domains_json.py @@ -4,37 +4,38 @@ from django.http import JsonResponse from django.core.paginator import Paginator from django.shortcuts import get_object_or_404 from django.views import View +from registrar.decorators import HAS_PORTFOLIO_MEMBERS_ANY_PERM, grant_access from registrar.models import UserDomainRole, Domain, DomainInformation, User from django.urls import reverse from django.db.models import Q from registrar.models.domain_invitation import DomainInvitation -from registrar.views.utility.mixins import PortfolioMemberDomainsPermission logger = logging.getLogger(__name__) -class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioMemberDomainsJson(View): def get(self, request): """Given the current request, get all domains that are associated with the portfolio, or associated with the member/invited member""" - domain_ids = self.get_domain_ids_from_request(request) + domain_ids = self._get_domain_ids_from_request(request) objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization") unfiltered_total = objects.count() - objects = self.apply_search(objects, request) - objects = self.apply_sorting(objects, request) + objects = self._apply_search(objects, request) + objects = self._apply_sorting(objects, request) - paginator = Paginator(objects, self.get_page_size(request)) + paginator = Paginator(objects, self._get_page_size(request)) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) member_id = request.GET.get("member_id") - domains = [self.serialize_domain(domain, member_id, request.user) for domain in page_obj.object_list] + domains = [self._serialize_domain(domain, member_id, request.user) for domain in page_obj.object_list] return JsonResponse( { @@ -48,7 +49,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): } ) - def get_page_size(self, request): + def _get_page_size(self, request): """Gets the page size. If member_only, need to return the entire result set every time, so need @@ -65,7 +66,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): # later return 1000 - def get_domain_ids_from_request(self, request): + def _get_domain_ids_from_request(self, request): """Get domain ids from request. request.get.email - email address of invited member @@ -100,13 +101,13 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): logger.warning("Invalid search criteria, returning empty results list") return [] - def apply_search(self, queryset, request): + def _apply_search(self, queryset, request): search_term = request.GET.get("search_term") if search_term: queryset = queryset.filter(Q(name__icontains=search_term)) return queryset - def apply_sorting(self, queryset, request): + def _apply_sorting(self, queryset, request): # Get the sorting parameters from the request sort_by = request.GET.get("sort_by", "name") order = request.GET.get("order", "asc") @@ -141,7 +142,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): return queryset - def serialize_domain(self, domain, member_id, user): + def _serialize_domain(self, domain, member_id, user): suborganization_name = None try: domain_info = domain.domain_info @@ -176,7 +177,7 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): "state": domain.state, "state_display": domain.state_display(), "get_state_help_text": domain.get_state_help_text(), - "action_url": reverse("domain", kwargs={"pk": domain.id}), + "action_url": reverse("domain", kwargs={"domain_pk": domain.id}), "action_label": ("View" if view_only else "Manage"), "svg_icon": ("visibility" if view_only else "settings"), "domain_info__sub_organization": suborganization_name, diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 29dc6a71c..ecdd24441 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -6,16 +6,17 @@ from django.contrib.postgres.aggregates import ArrayAgg from django.urls import reverse from django.views import View +from registrar.decorators import HAS_PORTFOLIO_MEMBERS_ANY_PERM, grant_access from registrar.models.domain_invitation import DomainInvitation from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from registrar.views.utility.mixins import PortfolioMembersPermission from registrar.models.utility.orm_helper import ArrayRemoveNull from django.contrib.postgres.aggregates import StringAgg -class PortfolioMembersJson(PortfolioMembersPermission, View): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioMembersJson(View): def get(self, request): """Fetch members (permissions and invitations) for the given portfolio.""" @@ -236,7 +237,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): ), # split domain_info array values into ids to form urls, and names "domain_urls": [ - reverse("domain", kwargs={"pk": domain_info.split(":")[0]}) for domain_info in domain_info_list + reverse("domain", kwargs={"domain_pk": domain_info.split(":")[0]}) for domain_info in domain_info_list ], "domain_names": [domain_info.split(":")[1] for domain_info in domain_info_list], "is_admin": is_admin, diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index dc1357585..f0c4841f1 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -5,20 +5,26 @@ from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.safestring import mark_safe +from django.views.generic import DetailView from django.contrib import messages from registrar.decorators import ( HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM, HAS_PORTFOLIO_DOMAINS_ANY_PERM, + HAS_PORTFOLIO_MEMBERS_ANY_PERM, + HAS_PORTFOLIO_MEMBERS_EDIT, IS_PORTFOLIO_MEMBER, grant_access, ) from registrar.forms import portfolio as portfolioForms -from registrar.models import Portfolio, User -from registrar.models.domain import Domain -from registrar.models.domain_invitation import DomainInvitation -from registrar.models.portfolio_invitation import PortfolioInvitation -from registrar.models.user_domain_role import UserDomainRole -from registrar.models.user_portfolio_permission import UserPortfolioPermission +from registrar.models import ( + Domain, + DomainInvitation, + Portfolio, + PortfolioInvitation, + User, + UserDomainRole, + UserPortfolioPermission +) from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.utility.email import EmailSendingError from registrar.utility.email_invitations import ( @@ -29,15 +35,6 @@ from registrar.utility.email_invitations import ( ) from registrar.utility.errors import MissingEmailError from registrar.utility.enums import DefaultUserValues -from registrar.views.utility.mixins import PortfolioMemberPermission -from registrar.views.utility.permission_views import ( - PortfolioBasePermissionView, - PortfolioMemberDomainsPermissionView, - PortfolioMemberDomainsEditPermissionView, - PortfolioMemberEditPermissionView, - PortfolioMemberPermissionView, - PortfolioMembersPermissionView, -) from django.views.generic import View from django.views.generic.edit import FormMixin from django.db import IntegrityError @@ -71,8 +68,10 @@ class PortfolioDomainRequestsView(View): return render(request, "portfolio_requests.html") -class PortfolioMemberView(PortfolioMemberPermissionView, View): - +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioMemberView(DetailView, View): + model = Portfolio + context_object_name = "portfolio" template_name = "portfolio_member.html" def get(self, request, pk): @@ -113,7 +112,8 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View): ) -class PortfolioMemberDeleteView(PortfolioMemberPermission, View): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioMemberDeleteView(View): def post(self, request, pk): """ @@ -190,8 +190,10 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View): messages.warning(self.request, "Could not send email notification to existing organization admins.") -class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): - +@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT) +class PortfolioMemberEditView(DetailView, View): + model = Portfolio + context_object_name = "portfolio" template_name = "portfolio_member_permissions.html" form_class = portfolioForms.PortfolioMemberForm @@ -265,7 +267,8 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): messages.warning(self.request, "Could not send email notification to existing organization admins.") -class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioMemberDomainsView(View): template_name = "portfolio_member_domains.html" @@ -283,8 +286,10 @@ class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View): ) -class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, View): - +@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT) +class PortfolioMemberDomainsEditView(DetailView, View): + model = Portfolio + context_object_name = "portfolio" template_name = "portfolio_member_domains_edit.html" def get(self, request, pk): @@ -393,8 +398,10 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V UserDomainRole.objects.filter(domain_id__in=removed_domain_ids, user=member).delete() -class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View): - +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioInvitedMemberView(DetailView, View): + model = Portfolio + context_object_name = "portfolio" template_name = "portfolio_member.html" # form_class = PortfolioInvitedMemberForm @@ -435,7 +442,8 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View): ) -class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioInvitedMemberDeleteView(View): def post(self, request, pk): """ @@ -478,8 +486,10 @@ class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View): messages.warning(self.request, "Could not send email notification to existing organization admins.") -class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View): - +@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT) +class PortfolioInvitedMemberEditView(DetailView, View): + model = Portfolio + context_object_name = "portfolio" template_name = "portfolio_member_permissions.html" form_class = portfolioForms.PortfolioInvitedMemberForm @@ -547,7 +557,8 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View): messages.warning(self.request, "Could not send email notification to existing organization admins.") -class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, View): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioInvitedMemberDomainsView(View): template_name = "portfolio_member_domains.html" @@ -562,9 +573,11 @@ class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, Vi }, ) +@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT) +class PortfolioInvitedMemberDomainsEditView(DetailView, View): -class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, View): - + model = Portfolio + context_object_name = "portfolio" template_name = "portfolio_member_domains_edit.html" def get(self, request, pk): @@ -749,7 +762,8 @@ class PortfolioNoDomainRequestsView(View): return context -class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin): +@grant_access(IS_PORTFOLIO_MEMBER) +class PortfolioOrganizationView(DetailView, FormMixin): """ View to handle displaying and updating the portfolio's organization details. """ @@ -811,7 +825,8 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin): return reverse("organization") -class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin): +@grant_access(IS_PORTFOLIO_MEMBER) +class PortfolioSeniorOfficialView(DetailView, FormMixin): """ View to handle displaying and updating the portfolio's senior official details. For now, this view is readonly. @@ -842,7 +857,8 @@ class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin): return self.render_to_response(self.get_context_data(form=form)) -class PortfolioMembersView(PortfolioMembersPermissionView, View): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioMembersView(View): template_name = "portfolio_members.html" @@ -851,10 +867,13 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View): return render(request, "portfolio_members.html") -class PortfolioAddMemberView(PortfolioMembersPermissionView, FormMixin): +@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) +class PortfolioAddMemberView(DetailView, FormMixin): template_name = "portfolio_members_add_new.html" form_class = portfolioForms.PortfolioNewMemberForm + model = Portfolio + context_object_name = "portfolio" def get(self, request, *args, **kwargs): """Handle GET requests to display the form.""" diff --git a/src/registrar/views/utility/__init__.py b/src/registrar/views/utility/__init__.py index f80774ef3..12fcc325d 100644 --- a/src/registrar/views/utility/__init__.py +++ b/src/registrar/views/utility/__init__.py @@ -1,7 +1,4 @@ from .steps_helper import StepsHelper from .always_404 import always_404 -from .permission_views import ( - PortfolioMembersPermission, -) from .api_views import get_senior_official_from_federal_agency_json diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 23895cb5c..1762d4900 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -202,110 +202,3 @@ class UserProfilePermission(PermissionsLoginMixin): return False return True - - -class PortfolioBasePermission(PermissionsLoginMixin): - """Permission mixin that redirects to portfolio pages if user - has access, otherwise 403""" - - def has_permission(self): - """Check if this user has access to this portfolio. - - The user is in self.request.user and the portfolio can be looked - up from the portfolio's primary key in self.kwargs["pk"] - """ - if not self.request.user.is_authenticated: - return False - - return self.request.user.is_org_user(self.request) - - -class PortfolioMembersPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio members pages if user - has access, otherwise 403""" - - def has_permission(self): - """Check if this user has access to members for this portfolio. - - The user is in self.request.user and the portfolio can be looked - up from the portfolio's primary key in self.kwargs["pk"]""" - - portfolio = self.request.session.get("portfolio") - if not self.request.user.has_view_members_portfolio_permission( - portfolio - ) and not self.request.user.has_edit_members_portfolio_permission(portfolio): - return False - - return super().has_permission() - - -class PortfolioMemberPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio member or invited member pages if user - has access, otherwise 403""" - - def has_permission(self): - """Check if this user has access to members or invited members for this portfolio. - - The user is in self.request.user and the portfolio can be looked - up from the portfolio's primary key in self.kwargs["pk"]""" - - portfolio = self.request.session.get("portfolio") - if not self.request.user.has_view_members_portfolio_permission( - portfolio - ) and not self.request.user.has_edit_members_portfolio_permission(portfolio): - return False - - return super().has_permission() - - -class PortfolioMemberEditPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio member or invited member pages if user - has access to edit, otherwise 403""" - - def has_permission(self): - """Check if this user has access to members or invited members for this portfolio. - - The user is in self.request.user and the portfolio can be looked - up from the portfolio's primary key in self.kwargs["pk"]""" - - portfolio = self.request.session.get("portfolio") - if not self.request.user.has_edit_members_portfolio_permission(portfolio): - return False - - return super().has_permission() - - -class PortfolioMemberDomainsPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio member or invited member domains pages if user - has access to edit, otherwise 403""" - - def has_permission(self): - """Check if this user has access to member or invited member domains for this portfolio. - - The user is in self.request.user and the portfolio can be looked - up from the portfolio's primary key in self.kwargs["pk"]""" - - portfolio = self.request.session.get("portfolio") - if not self.request.user.has_view_members_portfolio_permission( - portfolio - ) and not self.request.user.has_edit_members_portfolio_permission(portfolio): - return False - - return super().has_permission() - - -class PortfolioMemberDomainsEditPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio member or invited member domains edit pages if user - has access to edit, otherwise 403""" - - def has_permission(self): - """Check if this user has access to member or invited member domains for this portfolio. - - The user is in self.request.user and the portfolio can be looked - up from the portfolio's primary key in self.kwargs["pk"]""" - - portfolio = self.request.session.get("portfolio") - if not self.request.user.has_edit_members_portfolio_permission(portfolio): - return False - - return super().has_permission() diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 02a2b029b..1961e1cdd 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -7,13 +7,7 @@ from registrar.models import Portfolio from registrar.models.user import User from .mixins import ( - PortfolioMemberDomainsPermission, - PortfolioMemberDomainsEditPermission, - PortfolioMemberEditPermission, UserProfilePermission, - PortfolioBasePermission, - PortfolioMembersPermission, - PortfolioMemberPermission, ) import logging @@ -37,64 +31,3 @@ class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC): @abc.abstractmethod def template_name(self): raise NotImplementedError - - -class PortfolioBasePermissionView(PortfolioBasePermission, DetailView, abc.ABC): - """Abstract base view for portfolio views that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """ - - # DetailView property for what model this is viewing - model = Portfolio - # variable name in template context for the model object - context_object_name = "portfolio" - - # Abstract property enforces NotImplementedError on an attribute. - @property - @abc.abstractmethod - def template_name(self): - raise NotImplementedError - - -class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC): - """Abstract base view for portfolio members views that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """ - - -class PortfolioMemberPermissionView(PortfolioMemberPermission, PortfolioBasePermissionView, abc.ABC): - """Abstract base view for portfolio member views that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """ - - -class PortfolioMemberEditPermissionView(PortfolioMemberEditPermission, PortfolioBasePermissionView, abc.ABC): - """Abstract base view for portfolio member edit views that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """ - - -class PortfolioMemberDomainsPermissionView(PortfolioMemberDomainsPermission, PortfolioBasePermissionView, abc.ABC): - """Abstract base view for portfolio member domains views that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """ - - -class PortfolioMemberDomainsEditPermissionView( - PortfolioMemberDomainsEditPermission, PortfolioBasePermissionView, abc.ABC -): - """Abstract base view for portfolio member domains edit views that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """