Attention!
++ You are making changes to a registrant’s domain. When finished making changes, close this tab and inform the registrant of your updates. +
+- Back to manage your domains + Back to manage your domains
- + {% endif %} {# messages block is under the back breadcrumb link #} {% if messages %} {% for message in messages %} diff --git a/src/registrar/templates/domain_your_contact_information.html b/src/registrar/templates/domain_your_contact_information.html index e18deeb50..81c62584c 100644 --- a/src/registrar/templates/domain_your_contact_information.html +++ b/src/registrar/templates/domain_your_contact_information.html @@ -7,7 +7,8 @@Domain contact information
-If you’d like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here won’t affect your Login.gov account information.
+If you’d like us to use a different name, email, or phone number you can make those changes below. Updating your contact information here will update the contact information for all domains in your account. However, it won’t affect your Login.gov account information. +
{% include "includes/required_fields.html" %} diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index c4a2772b0..940183646 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -518,3 +518,11 @@ def multiple_unalphabetical_domain_objects( application = mock.create_full_dummy_domain_object(domain_type, object_name) applications.append(application) return applications + + +def generic_domain_object(domain_type, object_name): + """Returns a generic domain object of + domain_type 'application', 'information', or 'invitation'""" + mock = AuditedAdminMockData() + application = mock.create_full_dummy_domain_object(domain_type, object_name) + return application diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 2b347287d..2bf12b48c 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2,8 +2,10 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite from contextlib import ExitStack from django.contrib import messages +from django.urls import reverse from registrar.admin import ( + DomainAdmin, DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, @@ -15,15 +17,17 @@ from registrar.models import ( Domain, User, DomainInvitation, + Domain, ) from .common import ( completed_application, + generic_domain_object, mock_user, create_superuser, create_user, multiple_unalphabetical_domain_objects, ) - +from django.contrib.sessions.backends.db import SessionStore from django.contrib.auth import get_user_model from unittest.mock import patch @@ -928,3 +932,129 @@ class AuditedAdminTest(TestCase): DomainInformation.objects.all().delete() DomainApplication.objects.all().delete() DomainInvitation.objects.all().delete() + + +class DomainSessionVariableTest(TestCase): + """Test cases for session variables in Django Admin""" + + def setUp(self): + self.factory = RequestFactory() + self.admin = DomainAdmin(Domain, None) + self.client = Client(HTTP_HOST="localhost:8080") + + def test_session_vars_set_correctly(self): + """Checks if session variables are being set correctly""" + + p = "adminpass" + self.client.login(username="superuser", password=p) + + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) + + def test_session_vars_set_correctly_hardcoded_domain(self): + """Checks if session variables are being set correctly""" + + p = "adminpass" + self.client.login(username="superuser", password=p) + + dummy_domain_information: Domain = generic_domain_object( + "information", "session" + ) + dummy_domain_information.domain.pk = 1 + + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + self.populate_session_values(request, dummy_domain_information.domain) + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], 1) + + def test_session_variables_reset_correctly(self): + """Checks if incorrect session variables get overridden""" + + p = "adminpass" + self.client.login(username="superuser", password=p) + + dummy_domain_information = generic_domain_object("information", "session") + request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) + + self.populate_session_values( + request, dummy_domain_information.domain, preload_bad_data=True + ) + + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual( + request.session["analyst_action_location"], + dummy_domain_information.domain.pk, + ) + + def test_session_variables_retain_information(self): + """Checks to see if session variables retain old information""" + + p = "adminpass" + self.client.login(username="superuser", password=p) + + dummy_domain_information_list = multiple_unalphabetical_domain_objects( + "information" + ) + for item in dummy_domain_information_list: + request = self.get_factory_post_edit_domain(item.domain.pk) + self.populate_session_values(request, item.domain) + + self.assertEqual(request.session["analyst_action"], "edit") + self.assertEqual(request.session["analyst_action_location"], item.domain.pk) + + def test_session_variables_concurrent_requests(self): + """Simulates two requests at once""" + + p = "adminpass" + self.client.login(username="superuser", password=p) + + info_first = generic_domain_object("information", "session") + info_second = generic_domain_object("information", "session2") + + request_first = self.get_factory_post_edit_domain(info_first.domain.pk) + request_second = self.get_factory_post_edit_domain(info_second.domain.pk) + + self.populate_session_values(request_first, info_first.domain, True) + self.populate_session_values(request_second, info_second.domain, True) + + # Check if anything got nulled out + self.assertNotEqual(request_first.session["analyst_action"], None) + self.assertNotEqual(request_second.session["analyst_action"], None) + self.assertNotEqual(request_first.session["analyst_action_location"], None) + self.assertNotEqual(request_second.session["analyst_action_location"], None) + + # Check if they are both the same action 'type' + self.assertEqual(request_first.session["analyst_action"], "edit") + self.assertEqual(request_second.session["analyst_action"], "edit") + + # Check their locations, and ensure they aren't the same across both + self.assertNotEqual( + request_first.session["analyst_action_location"], + request_second.session["analyst_action_location"], + ) + + def populate_session_values(self, request, domain_object, preload_bad_data=False): + """Boilerplate for creating mock sessions""" + request.user = self.client + request.session = SessionStore() + request.session.create() + if preload_bad_data: + request.session["analyst_action"] = "invalid" + request.session["analyst_action_location"] = "bad location" + self.admin.response_change(request, domain_object) + + def get_factory_post_edit_domain(self, primary_key): + """Posts to registrar domain change + with the edit domain button 'clicked', + then returns the factory object""" + return self.factory.post( + reverse("admin:registrar_domain_change", args=(primary_key,)), + {"_edit_domain": "true"}, + follow=True, + ) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 6a33ec994..f945bc443 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -79,6 +79,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin): messages.success( self.request, "The organization name and mailing address has been updated." ) + # superclass has the redirect return super().form_valid(form) @@ -121,6 +122,7 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): messages.success( self.request, "The authorizing official for this domain has been updated." ) + # superclass has the redirect return super().form_valid(form) @@ -187,6 +189,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): messages.success( self.request, "The name servers for this domain have been updated." ) + # superclass has the redirect return super().form_valid(formset) @@ -227,6 +230,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin): messages.success( self.request, "Your contact information for this domain has been updated." ) + # superclass has the redirect return super().form_valid(form) @@ -272,6 +276,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): messages.success( self.request, "The security email for this domain have been updated." ) + # superclass has the redirect return redirect(self.get_success_url()) @@ -347,6 +352,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin): messages.success( self.request, f"Invited {email_address} to this domain." ) + return redirect(self.get_success_url()) def form_valid(self, form): @@ -368,6 +374,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin): pass messages.success(self.request, f"Added user {requested_email}.") + return redirect(self.get_success_url()) diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 363709a21..fd58b3475 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -2,7 +2,16 @@ from django.contrib.auth.mixins import PermissionRequiredMixin -from registrar.models import UserDomainRole, DomainApplication, DomainInvitation +from registrar.models import ( + DomainApplication, + DomainInvitation, + DomainInformation, + UserDomainRole, +) +import logging + + +logger = logging.getLogger(__name__) class PermissionsLoginMixin(PermissionRequiredMixin): @@ -25,27 +34,80 @@ class DomainPermission(PermissionsLoginMixin): up from the domain's primary key in self.kwargs["pk"] """ - # ticket 806 - # if self.request.user is staff or admin and - # domain.application__status = 'approved' or 'rejected' or 'action needed' - # return True - if not self.request.user.is_authenticated: return False - # user needs to have a role on the domain - if not UserDomainRole.objects.filter( - user=self.request.user, domain__id=self.kwargs["pk"] - ).exists(): - return False - - # The user has an ineligible flag if self.request.user.is_restricted(): return False + pk = self.kwargs["pk"] + # If pk is none then something went very wrong... + if pk is None: + raise ValueError("Primary key is None") + + if self.can_access_other_user_domains(pk): + return True + + # user needs to have a role on the domain + if not UserDomainRole.objects.filter( + user=self.request.user, domain__id=pk + ).exists(): + return False + # if we need to check more about the nature of role, do it here. return True + def can_access_other_user_domains(self, pk): + """Checks to see if an authorized user (staff or superuser) + can access a domain that they did not create or was invited to. + """ + + # Check if the user is permissioned... + user_is_analyst_or_superuser = ( + self.request.user.is_staff or self.request.user.is_superuser + ) + + if not user_is_analyst_or_superuser: + return False + + # Check if the user is attempting a valid edit action. + # In other words, if the analyst/admin did not click + # the 'Manage Domain' button in /admin, + # then they cannot access this page. + session = self.request.session + can_do_action = ( + "analyst_action" in session + and "analyst_action_location" in session + and session["analyst_action_location"] == pk + ) + + if not can_do_action: + return False + + # Analysts may manage domains, when they are in these statuses: + valid_domain_statuses = [ + DomainApplication.APPROVED, + DomainApplication.IN_REVIEW, + DomainApplication.REJECTED, + DomainApplication.ACTION_NEEDED, + # Edge case - some domains do not have + # a status or DomainInformation... aka a status of 'None'. + # It is necessary to access those to correct errors. + None, + ] + + requested_domain = None + if DomainInformation.objects.filter(id=pk).exists(): + requested_domain = DomainInformation.objects.get(id=pk) + + if requested_domain.domain_application.status not in valid_domain_statuses: + return False + + # Valid session keys exist, + # the user is permissioned, + # and it is in a valid status + return True + class DomainApplicationPermission(PermissionsLoginMixin): diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 0ef4ff4e5..417ee8417 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -3,7 +3,6 @@ import abc # abstract base class from django.views.generic import DetailView, DeleteView, TemplateView - from registrar.models import Domain, DomainApplication, DomainInvitation from .mixins import ( @@ -12,6 +11,9 @@ from .mixins import ( DomainInvitationPermission, ApplicationWizardPermission, ) +import logging + +logger = logging.getLogger(__name__) class DomainPermissionView(DomainPermission, DetailView, abc.ABC): @@ -27,6 +29,22 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC): # variable name in template context for the model object context_object_name = "domain" + # Adds context information for user permissions + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + user = self.request.user + context["is_analyst_or_superuser"] = user.is_staff or user.is_superuser + # Stored in a variable for the linter + action = "analyst_action" + action_location = "analyst_action_location" + # Flag to see if an analyst is attempting to make edits + if action in self.request.session: + context[action] = self.request.session[action] + if action_location in self.request.session: + context[action_location] = self.request.session[action_location] + + return context + # Abstract property enforces NotImplementedError on an attribute. @property @abc.abstractmethod