diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index 0f12a2e84..a733239c7 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -114,7 +114,7 @@ class UserGroup(Group): ) cisa_analysts_group.save() - logger.debug("CISA Analyt permissions added to group " + cisa_analysts_group.name) + logger.debug("CISA Analyst permissions added to group " + cisa_analysts_group.name) except Exception as e: logger.error(f"Error creating analyst permissions group: {e}") diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 07170d32f..ef60aa675 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2225,6 +2225,33 @@ class TestApplicationStatus(TestWithUser, WebTest): home_page = self.app.get("/") self.assertContains(home_page, "Withdrawn") + def test_application_withdraw_no_permissions(self): + """Can't withdraw applications as a restricted user.""" + self.user.status = User.RESTRICTED + self.user.save() + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + # Restricted user trying to withdraw results in 403 error + with less_console_noise(): + for url_name in [ + "application-withdraw-confirmation", + "application-withdrawn", + ]: + with self.subTest(url_name=url_name): + page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) + self.assertEqual(page.status_code, 403) + def test_application_status_no_permissions(self): """Can't access applications without being the creator.""" application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 0a6eb5b7b..41052e164 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -13,7 +13,11 @@ from registrar.models import DomainApplication from registrar.utility import StrEnum from registrar.views.utility import StepsHelper -from .utility import DomainApplicationPermissionView, ApplicationWizardPermissionView +from .utility import ( + DomainApplicationPermissionView, + DomainApplicationPermissionWithdrawView, + ApplicationWizardPermissionView, +) logger = logging.getLogger(__name__) @@ -544,7 +548,7 @@ class ApplicationStatus(DomainApplicationPermissionView): template_name = "application_status.html" -class ApplicationWithdrawConfirmation(DomainApplicationPermissionView): +class ApplicationWithdrawConfirmation(DomainApplicationPermissionWithdrawView): """This page will ask user to confirm if they want to withdraw The DomainApplicationPermissionView restricts access so that only the @@ -554,7 +558,7 @@ class ApplicationWithdrawConfirmation(DomainApplicationPermissionView): template_name = "application_withdraw_confirmation.html" -class ApplicationWithdrawn(DomainApplicationPermissionView): +class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView): # this view renders no template template_name = "" diff --git a/src/registrar/views/utility/__init__.py b/src/registrar/views/utility/__init__.py index 71d3edb91..3d1a64628 100644 --- a/src/registrar/views/utility/__init__.py +++ b/src/registrar/views/utility/__init__.py @@ -4,6 +4,7 @@ from .always_404 import always_404 from .permission_views import ( DomainPermissionView, DomainApplicationPermissionView, + DomainApplicationPermissionWithdrawView, DomainInvitationPermissionDeleteView, ApplicationWizardPermissionView, ) diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 37c0f6e98..5ff291d69 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -26,7 +26,8 @@ class PermissionsLoginMixin(PermissionRequiredMixin): class DomainPermission(PermissionsLoginMixin): - """Does the logged-in user have access to this domain?""" + """Permission mixin that redirects to domain if user has access, + otherwise 403""" def has_permission(self): """Check if this user has access to this domain. @@ -134,7 +135,8 @@ class DomainPermission(PermissionsLoginMixin): class DomainApplicationPermission(PermissionsLoginMixin): - """Does the logged-in user have access to this domain application?""" + """Permission mixin that redirects to domain application if user + has access, otherwise 403""" def has_permission(self): """Check if this user has access to this domain application. @@ -154,9 +156,33 @@ class DomainApplicationPermission(PermissionsLoginMixin): return True +class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): + + """Permission mixin that redirects to withdraw action on domain application + if user has access, otherwise 403""" + + def has_permission(self): + """Check if this user has access to withdraw this domain application.""" + if not self.request.user.is_authenticated: + return False + + # user needs to be the creator of the application + # this query is empty if there isn't a domain application with this + # id and this user as creator + if not DomainApplication.objects.filter(creator=self.request.user, id=self.kwargs["pk"]).exists(): + return False + + # Restricted users should not be able to withdraw domain requests + if self.request.user.is_restricted(): + return False + + return True + + class ApplicationWizardPermission(PermissionsLoginMixin): - """Does the logged-in user have permission to start or edit an application?""" + """Permission mixin that redirects to start or edit domain application if + user has access, otherwise 403""" def has_permission(self): """Check if this user has permission to start or edit an application. @@ -173,7 +199,8 @@ class ApplicationWizardPermission(PermissionsLoginMixin): class DomainInvitationPermission(PermissionsLoginMixin): - """Does the logged-in user have access to this domain invitation? + """Permission mixin that redirects to domain invitation if user has + access, otherwise 403" A user has access to a domain invitation if they have a role on the associated domain. diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 0e5d89ee2..1798ec79d 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -8,6 +8,7 @@ from registrar.models import Domain, DomainApplication, DomainInvitation from .mixins import ( DomainPermission, DomainApplicationPermission, + DomainApplicationPermissionWithdraw, DomainInvitationPermission, ApplicationWizardPermission, ) @@ -74,6 +75,26 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a raise NotImplementedError +class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC): + + """Abstract base view for domain application withdraw function + + This abstract view cannot be instantiated. Actual views must specify + `template_name`. + """ + + # DetailView property for what model this is viewing + model = DomainApplication + # variable name in template context for the model object + context_object_name = "domainapplication" + + # Abstract property enforces NotImplementedError on an attribute. + @property + @abc.abstractmethod + def template_name(self): + raise NotImplementedError + + class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC): """Abstract base view for the application form that enforces permissions