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