This commit is contained in:
David Kennedy 2025-02-11 23:47:55 -05:00
parent 6269cc56e3
commit 40737cbcf7
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
8 changed files with 49 additions and 56 deletions

View file

@ -10,8 +10,10 @@ IS_STAFF = "is_staff"
IS_DOMAIN_MANAGER = "is_domain_manager" IS_DOMAIN_MANAGER = "is_domain_manager"
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator" IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain" IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
IS_PORTFOLIO_MEMBER = "is_portfolio_member"
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager" 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" IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member"
HAS_PORTFOLIO_DOMAINS_ANY_PERM = "has_portfolio_domains_any_perm"
HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all" 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_ANY_PERM = "has_portfolio_domain_requests_any_perm"
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all" HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all"
@ -97,11 +99,21 @@ def _user_has_permission(user, request, rules, **kwargs):
has_permission = _can_access_other_user_domains(request, domain_id) has_permission = _can_access_other_user_domains(request, domain_id)
conditions_met.append(has_permission) conditions_met.append(has_permission)
if not any(conditions_met) and IS_PORTFOLIO_MEMBER in rules:
has_permission = user.is_org_user(request)
conditions_met.append(has_permission)
if not any(conditions_met) and HAS_PORTFOLIO_DOMAINS_VIEW_ALL in rules: if not any(conditions_met) and HAS_PORTFOLIO_DOMAINS_VIEW_ALL in rules:
domain_id = kwargs.get("domain_pk") domain_id = kwargs.get("domain_pk")
has_permission = _can_access_domain_via_portfolio_view_all_domains(request, domain_id) has_permission = _can_access_domain_via_portfolio_view_all_domains(request, domain_id)
conditions_met.append(has_permission) conditions_met.append(has_permission)
if not any(conditions_met) and HAS_PORTFOLIO_DOMAINS_ANY_PERM in rules:
has_permission = user.is_org_user(request) and user.has_any_domains_portfolio_permission(
request.session.get("portfolio")
)
conditions_met.append(has_permission)
if not any(conditions_met) and IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER in rules: if not any(conditions_met) and IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER in rules:
domain_id = kwargs.get("domain_pk") domain_id = kwargs.get("domain_pk")
has_permission = _is_domain_manager(user, domain_id) and _is_portfolio_member(request) has_permission = _is_domain_manager(user, domain_id) and _is_portfolio_member(request)

View file

@ -1,6 +1,6 @@
<li class="usa-sidenav__item"> <li class="usa-sidenav__item">
{% if url_name %} {% if url_name %}
{% url url_name pk=domain.id as url %} {% url url_name domain_pk=domain.id as url %}
{% endif %} {% endif %}
<a href="{{ url }}" <a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %} {% if request.path == url %}class="usa-current"{% endif %}

View file

@ -13,6 +13,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import redirect, render, get_object_or_404 from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.views.generic import DeleteView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django.conf import settings from django.conf import settings
from registrar.decorators import ( from registrar.decorators import (
@ -1332,10 +1333,12 @@ class DomainInvitationCancelView(SuccessMessageMixin, DomainInvitationPermission
@grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN) @grant_access(IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN)
class DomainDeleteUserView(UserDomainRolePermissionDeleteView): class DomainDeleteUserView(DeleteView):
"""Inside of a domain's user management, a form for deleting users.""" """Inside of a domain's user management, a form for deleting users."""
object: UserDomainRole # workaround for type mismatch in DeleteView object: UserDomainRole
model = UserDomainRole
context_object_name = "userdomainrole"
def get_object(self, queryset=None): def get_object(self, queryset=None):
"""Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id""" """Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id"""

View file

@ -15,20 +15,20 @@ def get_domain_requests_json(request):
If we are on the portfolio requests page, limit the response to only those requests associated with If we are on the portfolio requests page, limit the response to only those requests associated with
the given portfolio.""" the given portfolio."""
domain_request_ids = get_domain_request_ids_from_request(request) domain_request_ids = _get_domain_request_ids_from_request(request)
objects = DomainRequest.objects.filter(id__in=domain_request_ids) objects = DomainRequest.objects.filter(id__in=domain_request_ids)
unfiltered_total = objects.count() unfiltered_total = objects.count()
objects = apply_search(objects, request) objects = _apply_search(objects, request)
objects = apply_status_filter(objects, request) objects = _apply_status_filter(objects, request)
objects = apply_sorting(objects, request) objects = _apply_sorting(objects, request)
paginator = Paginator(objects, 10) paginator = Paginator(objects, 10)
page_number = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
domain_requests = [ domain_requests = [
serialize_domain_request(request, domain_request, request.user) for domain_request in page_obj.object_list _serialize_domain_request(request, domain_request, request.user) for domain_request in page_obj.object_list
] ]
return JsonResponse( return JsonResponse(
@ -44,7 +44,7 @@ def get_domain_requests_json(request):
) )
def get_domain_request_ids_from_request(request): def _get_domain_request_ids_from_request(request):
"""Get domain request ids from request. """Get domain request ids from request.
If portfolio specified, return domain request ids associated with portfolio. If portfolio specified, return domain request ids associated with portfolio.
@ -63,7 +63,7 @@ def get_domain_request_ids_from_request(request):
return domain_requests.values_list("id", flat=True) return domain_requests.values_list("id", flat=True)
def apply_search(queryset, request): def _apply_search(queryset, request):
search_term = request.GET.get("search_term") search_term = request.GET.get("search_term")
is_portfolio = request.GET.get("portfolio") is_portfolio = request.GET.get("portfolio")
@ -91,7 +91,7 @@ def apply_search(queryset, request):
return queryset return queryset
def apply_status_filter(queryset, request): def _apply_status_filter(queryset, request):
status_param = request.GET.get("status") status_param = request.GET.get("status")
if status_param: if status_param:
status_list = status_param.split(",") status_list = status_param.split(",")
@ -106,7 +106,7 @@ def apply_status_filter(queryset, request):
return queryset return queryset
def apply_sorting(queryset, request): def _apply_sorting(queryset, request):
sort_by = request.GET.get("sort_by", "id") # Default to 'id' sort_by = request.GET.get("sort_by", "id") # Default to 'id'
order = request.GET.get("order", "asc") # Default to 'asc' order = request.GET.get("order", "asc") # Default to 'asc'
@ -119,7 +119,7 @@ def apply_sorting(queryset, request):
return queryset.order_by(sort_by) return queryset.order_by(sort_by)
def serialize_domain_request(request, domain_request, user): def _serialize_domain_request(request, domain_request, user):
deletable_statuses = [ deletable_statuses = [
DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.STARTED,

View file

@ -6,7 +6,12 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.contrib import messages from django.contrib import messages
from registrar.decorators import HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM, grant_access from registrar.decorators import (
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
HAS_PORTFOLIO_DOMAINS_ANY_PERM,
IS_PORTFOLIO_MEMBER,
grant_access,
)
from registrar.forms import portfolio as portfolioForms from registrar.forms import portfolio as portfolioForms
from registrar.models import Portfolio, User from registrar.models import Portfolio, User
from registrar.models.domain import Domain from registrar.models.domain import Domain
@ -26,9 +31,7 @@ from registrar.utility.errors import MissingEmailError
from registrar.utility.enums import DefaultUserValues from registrar.utility.enums import DefaultUserValues
from registrar.views.utility.mixins import PortfolioMemberPermission from registrar.views.utility.mixins import PortfolioMemberPermission
from registrar.views.utility.permission_views import ( from registrar.views.utility.permission_views import (
PortfolioDomainsPermissionView,
PortfolioBasePermissionView, PortfolioBasePermissionView,
NoPortfolioDomainsPermissionView,
PortfolioMemberDomainsPermissionView, PortfolioMemberDomainsPermissionView,
PortfolioMemberDomainsEditPermissionView, PortfolioMemberDomainsEditPermissionView,
PortfolioMemberEditPermissionView, PortfolioMemberEditPermissionView,
@ -45,7 +48,8 @@ from registrar.views.utility.invitation_helper import get_org_membership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PortfolioDomainsView(PortfolioDomainsPermissionView, View): @grant_access(HAS_PORTFOLIO_DOMAINS_ANY_PERM)
class PortfolioDomainsView(View):
template_name = "portfolio_domains.html" template_name = "portfolio_domains.html"
@ -685,7 +689,8 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
).update(status=DomainInvitation.DomainInvitationStatus.CANCELED) ).update(status=DomainInvitation.DomainInvitationStatus.CANCELED)
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View): @grant_access(IS_PORTFOLIO_MEMBER)
class PortfolioNoDomainsView(View):
"""Some users have access to the underlying portfolio, but not any domains. """Some users have access to the underlying portfolio, but not any domains.
This is a custom view which explains that to the user - and denotes who to contact. This is a custom view which explains that to the user - and denotes who to contact.
""" """
@ -714,7 +719,8 @@ class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
return context return context
class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, View): @grant_access(IS_PORTFOLIO_MEMBER)
class PortfolioNoDomainRequestsView(View):
"""Some users have access to the underlying portfolio, but not any domain requests. """Some users have access to the underlying portfolio, but not any domain requests.
This is a custom view which explains that to the user - and denotes who to contact. This is a custom view which explains that to the user - and denotes who to contact.
""" """

View file

@ -373,23 +373,6 @@ class PortfolioBasePermission(PermissionsLoginMixin):
return self.request.user.is_org_user(self.request) return self.request.user.is_org_user(self.request)
class PortfolioDomainsPermission(PortfolioBasePermission):
"""Permission mixin that allows access to portfolio domain pages if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to 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_any_domains_portfolio_permission(portfolio):
return False
return super().has_permission()
class PortfolioMembersPermission(PortfolioBasePermission): class PortfolioMembersPermission(PortfolioBasePermission):
"""Permission mixin that allows access to portfolio members pages if user """Permission mixin that allows access to portfolio members pages if user
has access, otherwise 403""" has access, otherwise 403"""

View file

@ -10,7 +10,6 @@ from registrar.models.user_domain_role import UserDomainRole
from .mixins import ( from .mixins import (
DomainPermission, DomainPermission,
DomainInvitationPermission, DomainInvitationPermission,
PortfolioDomainsPermission,
PortfolioMemberDomainsPermission, PortfolioMemberDomainsPermission,
PortfolioMemberDomainsEditPermission, PortfolioMemberDomainsEditPermission,
PortfolioMemberEditPermission, PortfolioMemberEditPermission,
@ -74,6 +73,13 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
return False return False
def can_access_domain_via_portfolio(self, pk):
"""Most views should not allow permission to portfolio users.
If particular views allow access to the domain pages, they will need to override
this function.
"""
return False
# Abstract property enforces NotImplementedError on an attribute. # Abstract property enforces NotImplementedError on an attribute.
@property @property
@abc.abstractmethod @abc.abstractmethod
@ -142,23 +148,6 @@ class PortfolioBasePermissionView(PortfolioBasePermission, DetailView, abc.ABC):
raise NotImplementedError raise NotImplementedError
class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for portfolio domains views that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
class NoPortfolioDomainsPermissionView(PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for a user without access to the
portfolio domains views that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC): class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for portfolio members views that enforces permissions. """Abstract base view for portfolio members views that enforces permissions.