Merge pull request #1550 from cisagov/es/993-restrict-withdrawing-request

#993 Prevent restricted users from withdrawing domain application
This commit is contained in:
Erin Song 2023-12-21 09:56:14 -08:00 committed by GitHub
commit c22aff5531
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 88 additions and 8 deletions

View file

@ -114,7 +114,7 @@ class UserGroup(Group):
) )
cisa_analysts_group.save() 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: except Exception as e:
logger.error(f"Error creating analyst permissions group: {e}") logger.error(f"Error creating analyst permissions group: {e}")

View file

@ -2225,6 +2225,33 @@ class TestApplicationStatus(TestWithUser, WebTest):
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "Withdrawn") 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): def test_application_status_no_permissions(self):
"""Can't access applications without being the creator.""" """Can't access applications without being the creator."""
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)

View file

@ -13,7 +13,11 @@ from registrar.models import DomainApplication
from registrar.utility import StrEnum from registrar.utility import StrEnum
from registrar.views.utility import StepsHelper from registrar.views.utility import StepsHelper
from .utility import DomainApplicationPermissionView, ApplicationWizardPermissionView from .utility import (
DomainApplicationPermissionView,
DomainApplicationPermissionWithdrawView,
ApplicationWizardPermissionView,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -544,7 +548,7 @@ class ApplicationStatus(DomainApplicationPermissionView):
template_name = "application_status.html" template_name = "application_status.html"
class ApplicationWithdrawConfirmation(DomainApplicationPermissionView): class ApplicationWithdrawConfirmation(DomainApplicationPermissionWithdrawView):
"""This page will ask user to confirm if they want to withdraw """This page will ask user to confirm if they want to withdraw
The DomainApplicationPermissionView restricts access so that only the The DomainApplicationPermissionView restricts access so that only the
@ -554,7 +558,7 @@ class ApplicationWithdrawConfirmation(DomainApplicationPermissionView):
template_name = "application_withdraw_confirmation.html" template_name = "application_withdraw_confirmation.html"
class ApplicationWithdrawn(DomainApplicationPermissionView): class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
# this view renders no template # this view renders no template
template_name = "" template_name = ""

View file

@ -4,6 +4,7 @@ from .always_404 import always_404
from .permission_views import ( from .permission_views import (
DomainPermissionView, DomainPermissionView,
DomainApplicationPermissionView, DomainApplicationPermissionView,
DomainApplicationPermissionWithdrawView,
DomainInvitationPermissionDeleteView, DomainInvitationPermissionDeleteView,
ApplicationWizardPermissionView, ApplicationWizardPermissionView,
) )

View file

@ -26,7 +26,8 @@ class PermissionsLoginMixin(PermissionRequiredMixin):
class DomainPermission(PermissionsLoginMixin): 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): def has_permission(self):
"""Check if this user has access to this domain. """Check if this user has access to this domain.
@ -134,7 +135,8 @@ class DomainPermission(PermissionsLoginMixin):
class DomainApplicationPermission(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): def has_permission(self):
"""Check if this user has access to this domain application. """Check if this user has access to this domain application.
@ -154,9 +156,33 @@ class DomainApplicationPermission(PermissionsLoginMixin):
return True 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): 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): def has_permission(self):
"""Check if this user has permission to start or edit an application. """Check if this user has permission to start or edit an application.
@ -173,7 +199,8 @@ class ApplicationWizardPermission(PermissionsLoginMixin):
class DomainInvitationPermission(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 A user has access to a domain invitation if they have a role on the
associated domain. associated domain.

View file

@ -8,6 +8,7 @@ from registrar.models import Domain, DomainApplication, DomainInvitation
from .mixins import ( from .mixins import (
DomainPermission, DomainPermission,
DomainApplicationPermission, DomainApplicationPermission,
DomainApplicationPermissionWithdraw,
DomainInvitationPermission, DomainInvitationPermission,
ApplicationWizardPermission, ApplicationWizardPermission,
) )
@ -74,6 +75,26 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
raise NotImplementedError 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): class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC):
"""Abstract base view for the application form that enforces permissions """Abstract base view for the application form that enforces permissions