portfolio member permissions

This commit is contained in:
David Kennedy 2025-02-12 09:53:55 -05:00
parent e4021b76c1
commit 908e71e3ae
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
7 changed files with 90 additions and 228 deletions

View file

@ -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)

View file

@ -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,

View file

@ -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,

View file

@ -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."""

View file

@ -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

View file

@ -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()

View file

@ -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`.
"""