diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py
index 6674521c8..8a21e28e2 100644
--- a/src/registrar/context_processors.py
+++ b/src/registrar/context_processors.py
@@ -67,6 +67,8 @@ def portfolio_permissions(request):
"has_edit_request_portfolio_permission": False,
"has_view_suborganization_portfolio_permission": False,
"has_edit_suborganization_portfolio_permission": False,
+ "has_view_members_portfolio_permission": False,
+ "has_edit_members_portfolio_permission": False,
"portfolio": None,
"has_organization_feature_flag": False,
}
@@ -86,6 +88,8 @@ def portfolio_permissions(request):
"has_any_requests_portfolio_permission": request.user.has_any_requests_portfolio_permission(
portfolio
),
+ "has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
+ "has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
"portfolio": portfolio,
"has_organization_feature_flag": True,
}
diff --git a/src/registrar/migrations/0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more.py b/src/registrar/migrations/0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more.py
index adc00644e..c14a70ab0 100644
--- a/src/registrar/migrations/0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more.py
+++ b/src/registrar/migrations/0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.10 on 2024-09-05 23:39
+# Generated by Django 4.2.10 on 2024-09-04 21:29
import django.contrib.postgres.fields
from django.db import migrations, models
@@ -19,9 +19,10 @@ class Migration(migrations.Migration):
choices=[
("view_all_domains", "View all domains and domain reports"),
("view_managed_domains", "View managed domains"),
- ("view_member", "View members"),
- ("edit_member", "Create and edit members"),
+ ("view_members", "View members"),
+ ("edit_members", "Create and edit members"),
("view_all_requests", "View all requests"),
+ ("view_created_requests", "View created requests"),
("edit_requests", "Create and edit requests"),
("view_portfolio", "View organization"),
("edit_portfolio", "Edit organization"),
@@ -44,9 +45,10 @@ class Migration(migrations.Migration):
choices=[
("view_all_domains", "View all domains and domain reports"),
("view_managed_domains", "View managed domains"),
- ("view_member", "View members"),
- ("edit_member", "Create and edit members"),
+ ("view_members", "View members"),
+ ("edit_members", "Create and edit members"),
("view_all_requests", "View all requests"),
+ ("view_created_requests", "View created requests"),
("edit_requests", "Create and edit requests"),
("view_portfolio", "View organization"),
("edit_portfolio", "Edit organization"),
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 5d7c119bb..4427cc9c2 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -197,6 +197,41 @@ class User(AbstractUser):
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
+ def has_domain_requests_portfolio_permission(self, portfolio):
+ # BEGIN
+ # Note code below is to add organization_request feature
+ request = HttpRequest()
+ request.user = self
+ has_organization_requests_flag = flag_is_active(request, "organization_requests")
+ if not has_organization_requests_flag:
+ return False
+ # END
+ return self._has_portfolio_permission(
+ portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
+ ) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
+
+ def has_view_members_portfolio_permission(self, portfolio):
+ # BEGIN
+ # Note code below is to add organization_request feature
+ request = HttpRequest()
+ request.user = self
+ has_organization_members_flag = flag_is_active(request, "organization_members")
+ if not has_organization_members_flag:
+ return False
+ # END
+ return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
+
+ def has_edit_members_portfolio_permission(self, portfolio):
+ # BEGIN
+ # Note code below is to add organization_request feature
+ request = HttpRequest()
+ request.user = self
+ has_organization_members_flag = flag_is_active(request, "organization_members")
+ if not has_organization_members_flag:
+ return False
+ # END
+ return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
+
def has_view_all_domains_portfolio_permission(self, portfolio):
"""Determines if the current user can view all available domains in a given portfolio"""
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py
index bf1c3e566..0c2487df3 100644
--- a/src/registrar/models/user_portfolio_permission.py
+++ b/src/registrar/models/user_portfolio_permission.py
@@ -16,8 +16,8 @@ class UserPortfolioPermission(TimeStampedModel):
PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
- UserPortfolioPermissionChoices.VIEW_MEMBER,
- UserPortfolioPermissionChoices.EDIT_MEMBER,
+ UserPortfolioPermissionChoices.VIEW_MEMBERS,
+ UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
@@ -28,7 +28,7 @@ class UserPortfolioPermission(TimeStampedModel):
],
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
- UserPortfolioPermissionChoices.VIEW_MEMBER,
+ UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
# Domain: field specific permissions
diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py
index d87f981c7..7f34221fd 100644
--- a/src/registrar/models/utility/portfolio_helper.py
+++ b/src/registrar/models/utility/portfolio_helper.py
@@ -17,8 +17,8 @@ class UserPortfolioPermissionChoices(models.TextChoices):
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
- VIEW_MEMBER = "view_member", "View members"
- EDIT_MEMBER = "edit_member", "Create and edit members"
+ VIEW_MEMBERS = "view_members", "View members"
+ EDIT_MEMBERS = "edit_members", "Create and edit members"
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
diff --git a/src/registrar/templates/includes/header_extended.html b/src/registrar/templates/includes/header_extended.html
index 976ca2291..b5b7ae7e6 100644
--- a/src/registrar/templates/includes/header_extended.html
+++ b/src/registrar/templates/includes/header_extended.html
@@ -46,11 +46,11 @@
Domains
-
+
@@ -92,11 +92,13 @@
+ {% if has_view_members_portfolio_permission %}
Members
+ {% endif %}
{% url 'organization' as url %}
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 2b36fac44..dc6b5f319 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -1603,6 +1603,7 @@ class TestUser(TestCase):
self.assertFalse(self.user.has_contact_info())
@less_console_noise_decorator
+ @override_flag("organization_requests", active=True)
def test_has_portfolio_permission(self):
"""
0. Returns False when user does not have a permission
@@ -1624,7 +1625,10 @@ class TestUser(TestCase):
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
portfolio=portfolio,
user=self.user,
- additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
+ additional_permissions=[
+ UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
+ UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
+ ],
)
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py
index 8eec5e9c8..07d393973 100644
--- a/src/registrar/tests/test_views_portfolio.py
+++ b/src/registrar/tests/test_views_portfolio.py
@@ -233,6 +233,7 @@ class TestPortfolio(WebTest):
self.assertContains(response, 'for="id_city"')
@less_console_noise_decorator
+ @override_flag("organization_requests", active=True)
def test_accessible_pages_when_user_does_not_have_permission(self):
"""Tests which pages are accessible when user does not have portfolio permissions"""
self.app.set_user(self.user.username)
@@ -283,6 +284,7 @@ class TestPortfolio(WebTest):
self.assertEquals(domain_request_page.status_code, 403)
@less_console_noise_decorator
+ @override_flag("organization_requests", active=True)
def test_accessible_pages_when_user_does_not_have_role(self):
"""Test that admin / memmber roles are associated with the right access"""
self.app.set_user(self.user.username)
@@ -536,6 +538,102 @@ class TestPortfolio(WebTest):
self.assertContains(response, "Domain name")
permission.delete()
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ @override_flag("organization_requests", active=False)
+ def test_organization_requests_waffle_flag_off_hides_nav_link_and_restricts_permission(self):
+ """Setting the organization_requests waffle off hides the nav link and restricts access to the requests page"""
+ self.app.set_user(self.user.username)
+
+ UserPortfolioPermission.objects.get_or_create(
+ user=self.user,
+ portfolio=self.portfolio,
+ additional_permissions=[
+ UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
+ UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
+ UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
+ UserPortfolioPermissionChoices.EDIT_REQUESTS,
+ ],
+ )
+
+ home = self.app.get(reverse("home")).follow()
+
+ self.assertContains(home, "Hotel California")
+ self.assertNotContains(home, "Domain requests")
+
+ domain_requests = self.app.get(reverse("domain-requests"), expect_errors=True)
+ self.assertEqual(domain_requests.status_code, 403)
+
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ @override_flag("organization_requests", active=True)
+ def test_organization_requests_waffle_flag_on_shows_nav_link_and_allows_permission(self):
+ """Setting the organization_requests waffle on shows the nav link and allows access to the requests page"""
+ self.app.set_user(self.user.username)
+
+ UserPortfolioPermission.objects.get_or_create(
+ user=self.user,
+ portfolio=self.portfolio,
+ additional_permissions=[
+ UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
+ UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
+ UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
+ UserPortfolioPermissionChoices.EDIT_REQUESTS,
+ ],
+ )
+
+ home = self.app.get(reverse("home")).follow()
+
+ self.assertContains(home, "Hotel California")
+ self.assertContains(home, "Domain requests")
+
+ domain_requests = self.app.get(reverse("domain-requests"))
+ self.assertEqual(domain_requests.status_code, 200)
+
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ @override_flag("organization_members", active=False)
+ def test_organization_members_waffle_flag_off_hides_nav_link(self):
+ """Setting the organization_members waffle off hides the nav link"""
+ self.app.set_user(self.user.username)
+
+ UserPortfolioPermission.objects.get_or_create(
+ user=self.user,
+ portfolio=self.portfolio,
+ additional_permissions=[
+ UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
+ UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
+ UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
+ UserPortfolioPermissionChoices.EDIT_REQUESTS,
+ ],
+ )
+
+ home = self.app.get(reverse("home")).follow()
+
+ self.assertContains(home, "Hotel California")
+ self.assertNotContains(home, "Members")
+
+ @less_console_noise_decorator
+ @override_flag("organization_feature", active=True)
+ @override_flag("organization_members", active=True)
+ def test_organization_members_waffle_flag_on_shows_nav_link(self):
+ """Setting the organization_members waffle on shows the nav link"""
+ self.app.set_user(self.user.username)
+
+ UserPortfolioPermission.objects.get_or_create(
+ user=self.user,
+ portfolio=self.portfolio,
+ additional_permissions=[
+ UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
+ UserPortfolioPermissionChoices.VIEW_MEMBERS,
+ ],
+ )
+
+ home = self.app.get(reverse("home")).follow()
+
+ self.assertContains(home, "Hotel California")
+ self.assertContains(home, "Members")
+
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_portfolio_domain_requests_page_when_user_has_no_permissions(self):
diff --git a/src/registrar/views/utility/__init__.py b/src/registrar/views/utility/__init__.py
index 7219f4358..7e4e19085 100644
--- a/src/registrar/views/utility/__init__.py
+++ b/src/registrar/views/utility/__init__.py
@@ -7,5 +7,6 @@ from .permission_views import (
DomainRequestPermissionWithdrawView,
DomainInvitationPermissionDeleteView,
DomainRequestWizardPermissionView,
+ 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 24483b6ef..54eba727f 100644
--- a/src/registrar/views/utility/mixins.py
+++ b/src/registrar/views/utility/mixins.py
@@ -454,3 +454,20 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
return False
return super().has_permission()
+
+
+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):
+ 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 0ff7d1676..e7031cf0d 100644
--- a/src/registrar/views/utility/permission_views.py
+++ b/src/registrar/views/utility/permission_views.py
@@ -18,6 +18,7 @@ from .mixins import (
UserDeleteDomainRolePermission,
UserProfilePermission,
PortfolioBasePermission,
+ PortfolioMembersPermission,
)
import logging
@@ -229,3 +230,11 @@ class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, P
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
+
+
+class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC):
+ """Abstract base view for portfolio domain request views that enforces permissions.
+
+ This abstract view cannot be instantiated. Actual views must specify
+ `template_name`.
+ """