Merge branch 'main' into meoward/3549-dependabot-updates

This commit is contained in:
Matt-Spence 2025-03-24 17:12:40 -04:00 committed by GitHub
commit e5c8c342fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 447 additions and 77 deletions

View file

@ -880,6 +880,7 @@ class Domain(TimeStampedModel, DomainHelper):
which inturn call this function)
Will throw error if contact type is not the same as expectType
Raises ValueError if expected type doesn't match the contact type"""
if expectedType != contact.contact_type:
raise ValueError("Cannot set a contact with a different contact type, expected type was %s" % expectedType)
@ -892,7 +893,6 @@ class Domain(TimeStampedModel, DomainHelper):
duplicate_contacts = PublicContact.objects.exclude(registry_id=contact.registry_id).filter(
domain=self, contact_type=contact.contact_type
)
# if no record exists with this contact type
# make contact in registry, duplicate and errors handled there
errorCode = self._make_contact_in_registry(contact)
@ -971,6 +971,24 @@ class Domain(TimeStampedModel, DomainHelper):
logger.info("making technical contact")
self._set_singleton_contact(contact, expectedType=contact.ContactTypeChoices.TECHNICAL)
def print_contact_info_epp(self, contact: PublicContact):
"""Prints registry data for this PublicContact for easier debugging"""
results = self._request_contact_info(contact, get_result_as_dict=True)
logger.info("---------------------")
logger.info(f"EPP info for {contact.contact_type}:")
logger.info("---------------------")
for key, value in results.items():
logger.info(f"{key}: {value}")
def print_all_domain_contact_info_epp(self):
"""Prints registry data for this domains security, registrant, technical, and administrative contacts."""
logger.info(f"Contact info for {self}:")
logger.info("=====================")
contacts = [self.security_contact, self.registrant_contact, self.technical_contact, self.administrative_contact]
for contact in contacts:
if contact:
self.print_contact_info_epp(contact)
def is_active(self) -> bool:
"""Currently just returns if the state is created,
because then it should be live, theoretically.
@ -1351,10 +1369,14 @@ class Domain(TimeStampedModel, DomainHelper):
)
return street_dict
def _request_contact_info(self, contact: PublicContact):
def _request_contact_info(self, contact: PublicContact, get_result_as_dict=False):
"""Grabs the resultant contact information in epp for this public contact
by using the InfoContact command.
Returns a commands.InfoContactResultData object, or a dict if get_result_as_dict is True."""
try:
req = commands.InfoContact(id=contact.registry_id)
return registry.send(req, cleaned=True).res_data[0]
result = registry.send(req, cleaned=True).res_data[0]
return result if not get_result_as_dict else vars(result)
except RegistryError as error:
logger.error(
"Registry threw error for contact id %s contact type is %s, error code is\n %s full error is %s", # noqa
@ -1674,22 +1696,26 @@ class Domain(TimeStampedModel, DomainHelper):
return help_text
def _disclose_fields(self, contact: PublicContact):
"""creates a disclose object that can be added to a contact Create using
"""creates a disclose object that can be added to a contact Create or Update using
.disclose= <this function> on the command before sending.
if item is security email then make sure email is visible"""
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
# You can find each enum here:
# https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32
DF = epp.DiscloseField
fields = {DF.EMAIL}
all_disclose_fields = {field for field in DF}
disclose_args = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}}
hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
disclose = is_security and contact.email not in hidden_security_emails
# Delete after testing on other devices
logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose)
# Will only disclose DF.EMAIL if its not the default
return epp.Disclose(
flag=disclose,
fields=fields,
)
fields_to_remove = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT}
if contact.contact_type == contact.ContactTypeChoices.SECURITY:
if contact.email not in DefaultEmail.get_all_emails():
fields_to_remove.add(DF.EMAIL)
elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE:
fields_to_remove.update({DF.EMAIL, DF.VOICE, DF.ADDR})
disclose_args["fields"].difference_update(fields_to_remove) # type: ignore
logger.debug("Updated domain contact %s to disclose: %s", contact.email, disclose_args.get("flag"))
return epp.Disclose(**disclose_args) # type: ignore
def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore
return epp.PostalInfo( # type: ignore

View file

@ -1,3 +1,4 @@
import logging
from datetime import datetime
from random import choices
from string import ascii_uppercase, ascii_lowercase, digits
@ -9,6 +10,9 @@ from registrar.utility.enums import DefaultEmail
from .utility.time_stamped_model import TimeStampedModel
logger = logging.getLogger(__name__)
def get_id():
"""Generate a 16 character registry ID with a low probability of collision."""
day = datetime.today().strftime("%A")[:2]
@ -92,15 +96,14 @@ class PublicContact(TimeStampedModel):
return cls(
contact_type=PublicContact.ContactTypeChoices.REGISTRANT,
registry_id=get_id(),
name="CSD/CB Attn: Cameron Dixon",
name="CSD/CB Attn: .gov TLD",
org="Cybersecurity and Infrastructure Security Agency",
street1="CISA NGR STOP 0645",
street2="1110 N. Glebe Rd.",
street1="1110 N. Glebe Rd",
city="Arlington",
sp="VA",
pc="20598-0645",
pc="22201",
cc="US",
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
voice="+1.8882820870",
pw="thisisnotapassword",
)
@ -110,14 +113,14 @@ class PublicContact(TimeStampedModel):
return cls(
contact_type=PublicContact.ContactTypeChoices.ADMINISTRATIVE,
registry_id=get_id(),
name="Program Manager",
name="CSD/CB Attn: .gov TLD",
org="Cybersecurity and Infrastructure Security Agency",
street1="4200 Wilson Blvd.",
street1="1110 N. Glebe Rd",
city="Arlington",
sp="VA",
pc="22201",
cc="US",
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
voice="+1.8882820870",
pw="thisisnotapassword",
)
@ -127,14 +130,14 @@ class PublicContact(TimeStampedModel):
return cls(
contact_type=PublicContact.ContactTypeChoices.TECHNICAL,
registry_id=get_id(),
name="Registry Customer Service",
name="CSD/CB Attn: .gov TLD",
org="Cybersecurity and Infrastructure Security Agency",
street1="4200 Wilson Blvd.",
street1="1110 N. Glebe Rd",
city="Arlington",
sp="VA",
pc="22201",
cc="US",
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
voice="+1.8882820870",
pw="thisisnotapassword",
)
@ -144,14 +147,14 @@ class PublicContact(TimeStampedModel):
return cls(
contact_type=PublicContact.ContactTypeChoices.SECURITY,
registry_id=get_id(),
name="Registry Customer Service",
name="CSD/CB Attn: .gov TLD",
org="Cybersecurity and Infrastructure Security Agency",
street1="4200 Wilson Blvd.",
street1="1110 N. Glebe Rd",
city="Arlington",
sp="VA",
pc="22201",
cc="US",
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
voice="+1.8882820870",
pw="thisisnotapassword",
)

View file

@ -40,7 +40,7 @@
<button
type="submit"
class="usa-button"
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov"%}Add security email{% else %}Save{% endif %}</button>
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov" or form.security_email.value == "help@get.gov"%}Add security email{% else %}Save{% endif %}</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -0,0 +1,34 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %}
An update was made to your .gov organization.
ORGANIZATION: {{ portfolio }}
UPDATED BY: {{ editor.email }}
UPDATED ON: {{ date }}
INFORMATION UPDATED: {{ updated_info }}
You can view this update in the .gov registrar <https://manage.get.gov>.
----------------------------------------------------------------
WHY DID YOU RECEIVE THIS EMAIL?
You're listed as an admin for {{ portfolio }}, so you'll receive a
notification whenever changes are made to that .gov organization.
If you have questions or concerns, reach out to the person who made the change or reply
to this email.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for using a .gov
domain.
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Learn about .gov <https://get.gov>
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency
(CISA) <https://cisa.gov/>
{% endautoescape %}

View file

@ -0,0 +1 @@
An update was made to your .gov organization

View file

@ -1444,10 +1444,8 @@ class MockEppLib(TestCase):
],
)
mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData(
"defaultTech", "dotgov@cisa.dhs.gov"
)
mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultSec", "dotgov@cisa.dhs.gov")
mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultTech", "help@get.gov")
mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultSec", "help@get.gov")
mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("securityContact", "security@mail.gov")
mockTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData("technicalContact", "tech@mail.gov")
mockAdministrativeContact = InfoDomainWithContacts.dummyInfoContactResultData("adminContact", "admin@mail.gov")
@ -1962,14 +1960,23 @@ class MockEppLib(TestCase):
self.mockedSendFunction = self.mockSendPatch.start()
self.mockedSendFunction.side_effect = self.mockSend
def _convertPublicContactToEpp(self, contact: PublicContact, disclose_email=False, createContact=True):
def _convertPublicContactToEpp(
self,
contact: PublicContact,
disclose=False,
createContact=True,
disclose_fields=None,
disclose_types=None,
):
DF = common.DiscloseField
fields = {DF.EMAIL}
if disclose_fields is None:
fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT, DF.EMAIL}
disclose_fields = {field for field in DF} - fields
di = common.Disclose(
flag=disclose_email,
fields=fields,
)
if disclose_types is None:
disclose_types = {DF.ADDR: "loc"}
di = common.Disclose(flag=disclose, fields=disclose_fields, types=disclose_types)
# check docs here looks like we may have more than one address field but
addr = common.ContactAddr(

View file

@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import patch, MagicMock, ANY
from datetime import date
from registrar.models.domain import Domain
from registrar.models.portfolio import Portfolio
@ -21,6 +21,7 @@ from registrar.utility.email_invitations import (
send_portfolio_invitation_remove_email,
send_portfolio_member_permission_remove_email,
send_portfolio_member_permission_update_email,
send_portfolio_update_emails_to_portfolio_admins,
)
from api.tests.common import less_console_noise_decorator
@ -1261,3 +1262,60 @@ class SendDomainManagerRemovalEmailsToManagersTests(unittest.TestCase):
self.assertTrue(result) # No emails sent, but also no failures
mock_filter.assert_called_once_with(domain=self.domain)
class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase):
"""Unit tests for send_portfolio_update_emails_to_portfolio_admins function."""
def setUp(self):
"""Set up test data."""
self.email = "removed.admin@example.com"
self.requestor_email = "requestor@example.com"
self.portfolio = MagicMock(spec=Portfolio, name="Portfolio")
self.portfolio.organization_name = "Test Organization"
# Mock portfolio admin users
self.admin_user1 = MagicMock(spec=User)
self.admin_user1.email = "admin1@example.com"
self.admin_user2 = MagicMock(spec=User)
self.admin_user2.email = "admin2@example.com"
self.portfolio_admin1 = MagicMock(spec=UserPortfolioPermission)
self.portfolio_admin1.user = self.admin_user1
self.portfolio_admin1.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.portfolio_admin2 = MagicMock(spec=UserPortfolioPermission)
self.portfolio_admin2.user = self.admin_user2
self.portfolio_admin2.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
@patch("registrar.utility.email_invitations.send_templated_email")
@patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
def test_send_portfolio_update_emails_to_portfolio_admins(self, mock_filter, mock_send_templated_email):
"""Test send_portfolio_update_emails_to_portfolio_admins sends templated email."""
# Mock data
editor = self.admin_user1
updated_page = "Organization"
mock_filter.return_value = [self.portfolio_admin1, self.portfolio_admin2]
mock_send_templated_email.return_value = None # No exception means success
# Call function
result = send_portfolio_update_emails_to_portfolio_admins(editor, self.portfolio, updated_page)
mock_filter.assert_called_once_with(
portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
mock_send_templated_email.assert_any_call(
"emails/portfolio_org_update_notification.txt",
"emails/portfolio_org_update_notification_subject.txt",
to_address=self.admin_user1.email,
context=ANY,
)
mock_send_templated_email.assert_any_call(
"emails/portfolio_org_update_notification.txt",
"emails/portfolio_org_update_notification_subject.txt",
to_address=self.admin_user2.email,
context=ANY,
)
self.assertTrue(result)

View file

@ -722,6 +722,9 @@ class TestRegistrantContacts(MockEppLib):
self.domain, _ = Domain.objects.get_or_create(name="security.gov")
# Creates a domain with an associated contact
self.domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov")
DF = common.DiscloseField
excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT}
self.all_disclose_fields = {field for field in DF} - excluded_disclose_fields
def tearDown(self):
super().tearDown()
@ -758,7 +761,9 @@ class TestRegistrantContacts(MockEppLib):
contact_type=PublicContact.ContactTypeChoices.SECURITY,
).registry_id
expectedSecContact.registry_id = id
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False)
expectedCreateCommand = self._convertPublicContactToEpp(
expectedSecContact, disclose=False, disclose_fields=self.all_disclose_fields
)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")],
@ -788,7 +793,7 @@ class TestRegistrantContacts(MockEppLib):
# self.domain.security_contact=expectedSecContact
expectedSecContact.save()
# no longer the default email it should be disclosed
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True)
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose=False)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")],
@ -813,7 +818,9 @@ class TestRegistrantContacts(MockEppLib):
security_contact.registry_id = "fail"
security_contact.save()
self.domain.security_contact = security_contact
expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False)
expectedCreateCommand = self._convertPublicContactToEpp(
security_contact, disclose=False, disclose_fields=self.all_disclose_fields
)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[common.DomainContact(contact=security_contact.registry_id, type="security")],
@ -846,7 +853,7 @@ class TestRegistrantContacts(MockEppLib):
new_contact.registry_id = "fail"
new_contact.email = ""
self.domain.security_contact = new_contact
firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True)
firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose=False)
updateDomainAddCall = commands.UpdateDomain(
name=self.domain.name,
add=[common.DomainContact(contact=old_contact.registry_id, type="security")],
@ -856,7 +863,7 @@ class TestRegistrantContacts(MockEppLib):
PublicContact.get_default_security().email,
)
# this one triggers the fail
secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose_email=True)
secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose=False)
updateDomainRemCall = commands.UpdateDomain(
name=self.domain.name,
rem=[common.DomainContact(contact=old_contact.registry_id, type="security")],
@ -864,7 +871,9 @@ class TestRegistrantContacts(MockEppLib):
defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id
default_security = PublicContact.get_default_security()
default_security.registry_id = defaultSecID
createDefaultContact = self._convertPublicContactToEpp(default_security, disclose_email=False)
createDefaultContact = self._convertPublicContactToEpp(
default_security, disclose=False, disclose_fields=self.all_disclose_fields
)
updateDomainWDefault = commands.UpdateDomain(
name=self.domain.name,
add=[common.DomainContact(contact=defaultSecID, type="security")],
@ -892,15 +901,15 @@ class TestRegistrantContacts(MockEppLib):
security_contact.email = "originalUserEmail@gmail.com"
security_contact.registry_id = "fail"
security_contact.save()
expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True)
expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose=False)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[common.DomainContact(contact=security_contact.registry_id, type="security")],
)
security_contact.email = "changedEmail@email.com"
security_contact.save()
expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True)
updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False)
expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose=False)
updateContact = self._convertPublicContactToEpp(security_contact, disclose=False, createContact=False)
expected_calls = [
call(expectedCreateCommand, cleaned=True),
call(expectedUpdateDomain, cleaned=True),
@ -988,9 +997,23 @@ class TestRegistrantContacts(MockEppLib):
for contact in contacts:
expected_contact = contact[0]
actual_contact = contact[1]
is_security = expected_contact.contact_type == "security"
expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security)
# Should only be disclosed if the type is security, as the email is valid
if expected_contact.contact_type == PublicContact.ContactTypeChoices.SECURITY:
disclose_fields = self.all_disclose_fields - {"email"}
expectedCreateCommand = self._convertPublicContactToEpp(
expected_contact, disclose=False, disclose_fields=disclose_fields
)
elif expected_contact.contact_type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
disclose_fields = self.all_disclose_fields - {"email", "voice", "addr"}
expectedCreateCommand = self._convertPublicContactToEpp(
expected_contact,
disclose=False,
disclose_fields=disclose_fields,
disclose_types={"addr": "loc"},
)
else:
expectedCreateCommand = self._convertPublicContactToEpp(
expected_contact, disclose=False, disclose_fields=self.all_disclose_fields
)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# The emails should match on both items
self.assertEqual(expected_contact.email, actual_contact.email)
@ -999,23 +1022,24 @@ class TestRegistrantContacts(MockEppLib):
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
dummy_contact = domain.get_default_security_contact()
test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__
test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__
test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose=False).__dict__
test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose=False).__dict__
# Separated for linter
disclose_email_field = {common.DiscloseField.EMAIL}
disclose_email_field = self.all_disclose_fields - {common.DiscloseField.EMAIL}
DF = common.DiscloseField
expected_disclose = {
"auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
"disclose": common.Disclose(flag=True, fields=disclose_email_field, types=None),
"email": "dotgov@cisa.dhs.gov",
"disclose": common.Disclose(flag=False, fields=disclose_email_field, types={DF.ADDR: "loc"}),
"email": "help@get.gov",
"extensions": [],
"fax": None,
"id": "ThIq2NcRIDN7PauO",
"ident": None,
"notify_email": None,
"postal_info": common.PostalInfo(
name="Registry Customer Service",
name="CSD/CB Attn: .gov TLD",
addr=common.ContactAddr(
street=["4200 Wilson Blvd.", None, None],
street=["1110 N. Glebe Rd", None, None],
city="Arlington",
pc="22201",
cc="US",
@ -1030,17 +1054,17 @@ class TestRegistrantContacts(MockEppLib):
# Separated for linter
expected_not_disclose = {
"auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
"disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None),
"email": "dotgov@cisa.dhs.gov",
"disclose": common.Disclose(flag=False, fields=disclose_email_field, types={DF.ADDR: "loc"}),
"email": "help@get.gov",
"extensions": [],
"fax": None,
"id": "ThrECENCHI76PGLh",
"ident": None,
"notify_email": None,
"postal_info": common.PostalInfo(
name="Registry Customer Service",
name="CSD/CB Attn: .gov TLD",
addr=common.ContactAddr(
street=["4200 Wilson Blvd.", None, None],
street=["1110 N. Glebe Rd", None, None],
city="Arlington",
pc="22201",
cc="US",
@ -1058,6 +1082,39 @@ class TestRegistrantContacts(MockEppLib):
self.assertEqual(test_disclose, expected_disclose)
self.assertEqual(test_not_disclose, expected_not_disclose)
@less_console_noise_decorator
def test_convert_public_contact_with_custom_fields(self):
"""Test converting a contact with custom disclosure fields."""
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
dummy_contact = domain.get_default_administrative_contact()
DF = common.DiscloseField
# Create contact with multiple disclosure fields
result = self._convertPublicContactToEpp(
dummy_contact,
disclose=True,
disclose_fields={DF.EMAIL, DF.VOICE, DF.ADDR},
disclose_types={},
)
self.assertEqual(result.disclose.flag, True)
self.assertEqual(result.disclose.fields, {DF.EMAIL, DF.VOICE, DF.ADDR})
self.assertEqual(result.disclose.types, {})
@less_console_noise_decorator
def test_convert_public_contact_with_empty_fields(self):
"""Test converting a contact with empty disclosure fields."""
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
dummy_contact = domain.get_default_security_contact()
DF = common.DiscloseField
# Create contact with empty fields list
result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={DF.EMAIL})
# Verify disclosure settings
self.assertEqual(result.disclose.flag, True)
self.assertEqual(result.disclose.fields, {DF.EMAIL})
self.assertEqual(result.disclose.types, {DF.ADDR: "loc"})
def test_not_disclosed_on_default_security_contact(self):
"""
Scenario: Registrant creates a new domain with no security email
@ -1071,7 +1128,9 @@ class TestRegistrantContacts(MockEppLib):
expectedSecContact.domain = domain
expectedSecContact.registry_id = "defaultSec"
domain.security_contact = expectedSecContact
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False)
expectedCreateCommand = self._convertPublicContactToEpp(
expectedSecContact, disclose=False, disclose_fields=self.all_disclose_fields
)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# Confirm that we are getting a default email
self.assertEqual(domain.security_contact.email, expectedSecContact.email)
@ -1089,7 +1148,9 @@ class TestRegistrantContacts(MockEppLib):
expectedTechContact.domain = domain
expectedTechContact.registry_id = "defaultTech"
domain.technical_contact = expectedTechContact
expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False)
expectedCreateCommand = self._convertPublicContactToEpp(
expectedTechContact, disclose=False, disclose_fields=self.all_disclose_fields
)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# Confirm that we are getting a default email
self.assertEqual(domain.technical_contact.email, expectedTechContact.email)
@ -1108,7 +1169,7 @@ class TestRegistrantContacts(MockEppLib):
expectedSecContact.domain = domain
expectedSecContact.email = "security@mail.gov"
domain.security_contact = expectedSecContact
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True)
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose=False)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# Confirm that we are getting the desired email
self.assertEqual(domain.security_contact.email, expectedSecContact.email)

View file

@ -376,8 +376,8 @@ class TestPortfolio(WebTest):
self.assertContains(page, "Non-Federal Agency")
@less_console_noise_decorator
def test_domain_org_name_address_form(self):
"""Submitting changes works on the org name address page."""
def test_org_form_invalid_update(self):
"""Organization form will not redirect on invalid formsets."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
portfolio_additional_permissions = [
@ -398,11 +398,79 @@ class TestPortfolio(WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result_page = portfolio_org_name_page.form.submit()
# Form will not validate with missing required field (zipcode)
self.assertEqual(success_result_page.status_code, 200)
self.assertContains(success_result_page, "6 Downing st")
self.assertContains(success_result_page, "London")
@less_console_noise_decorator
def test_org_form_valid_update(self):
"""Organization form will redirect on valid formsets."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
self.portfolio.address_line1 = "1600 Penn Ave"
self.portfolio.save()
portfolio_org_name_page = self.app.get(reverse("organization"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# Form validates and redirects with all required fields
portfolio_org_name_page.form["address_line1"] = "6 Downing st"
portfolio_org_name_page.form["city"] = "London"
portfolio_org_name_page.form["zipcode"] = "11111"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result_page = portfolio_org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 302)
@boto3_mocking.patching
@less_console_noise_decorator
@patch("registrar.views.portfolios.send_portfolio_update_emails_to_portfolio_admins")
def test_org_update_sends_admin_email(self, mock_send_organization_update_email):
"""Updating organization information emails organization admin."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
self.admin, _ = User.objects.get_or_create(
email="mayor@igorville.com", first_name="Hello", last_name="World"
)
portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
portfolio_permission_admin, _ = UserPortfolioPermission.objects.get_or_create(
user=self.admin,
portfolio=self.portfolio,
additional_permissions=portfolio_additional_permissions,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
self.portfolio.address_line1 = "1600 Penn Ave"
self.portfolio.save()
portfolio_org_name_page = self.app.get(reverse("organization"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
portfolio_org_name_page.form["address_line1"] = "6 Downing st"
portfolio_org_name_page.form["city"] = "London"
portfolio_org_name_page.form["zipcode"] = "11111"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result_page = portfolio_org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 302)
# Verify that the notification emails were sent to domain manager
mock_send_organization_update_email.assert_called_once()
@less_console_noise_decorator
def test_portfolio_in_session_when_organization_feature_active(self):
"""When organization_feature flag is true and user has a portfolio,

View file

@ -740,7 +740,7 @@ class DomainExport(BaseExport):
domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
security_contact_email = model.get("security_contact_email")
invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
invalid_emails = DefaultEmail.get_all_emails()
if (
not security_contact_email
or not isinstance(security_contact_email, str)

View file

@ -280,6 +280,56 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i
return all_admin_emails_sent
def send_portfolio_update_emails_to_portfolio_admins(editor, portfolio, updated_page):
"""
Sends an email notification to all portfolio admin when portfolio organization is updated.
Raises exceptions for validation or email-sending issues.
Args:
editor (User): The user editing the portfolio organization.
portfolio (Portfolio): The portfolio object whose organization information is changed.
Returns:
Boolean indicating if all messages were sent successfully.
Raises:
MissingEmailError: If the requestor has no email associated with their account.
EmailSendingError: If there is an error while sending the email.
"""
all_emails_sent = True
# Get each portfolio admin from list
user_portfolio_permissions = UserPortfolioPermission.objects.filter(
portfolio=portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
for user_portfolio_permission in user_portfolio_permissions:
# Send email to each portfolio_admin
user = user_portfolio_permission.user
try:
send_templated_email(
"emails/portfolio_org_update_notification.txt",
"emails/portfolio_org_update_notification_subject.txt",
to_address=user.email,
context={
"requested_user": user,
"portfolio": portfolio,
"editor": editor,
"portfolio_admin": user,
"date": date.today(),
"updated_info": updated_page,
},
)
except EmailSendingError:
logger.warning(
"Could not send email organization admin notification to %s " "for portfolio: %s",
user.email,
portfolio,
exc_info=True,
)
all_emails_sent = False
return all_emails_sent
def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission):
"""
Sends an email notification to a portfolio member when their permissions are updated.

View file

@ -29,18 +29,26 @@ class LogCode(Enum):
DEFAULT = 5
class DefaultEmail(Enum):
class DefaultEmail(StrEnum):
"""Stores the string values of default emails
Overview of emails:
- PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov"
- PUBLIC_CONTACT_DEFAULT: "help@get.gov"
- OLD_PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov"
- LEGACY_DEFAULT: "registrar@dotgov.gov"
- HELP_EMAIL: "help@get.gov"
"""
PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov"
PUBLIC_CONTACT_DEFAULT = "help@get.gov"
# We used to use this email for default public contacts.
# This is retained for data correctness, but it will be phased out.
# help@get.gov is the current email that we use for these now.
OLD_PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov"
LEGACY_DEFAULT = "registrar@dotgov.gov"
@classmethod
def get_all_emails(cls):
return [email for email in cls]
class DefaultUserValues(StrEnum):
"""Stores default values for a default user.

View file

@ -402,7 +402,7 @@ class DomainView(DomainBaseView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
default_emails = DefaultEmail.get_all_emails()
context["hidden_security_emails"] = default_emails
@ -460,7 +460,7 @@ class DomainRenewalView(DomainBaseView):
context = super().get_context_data(**kwargs)
default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
default_emails = DefaultEmail.get_all_emails()
context["hidden_security_emails"] = default_emails
@ -1170,7 +1170,7 @@ class DomainSecurityEmailView(DomainFormBaseView):
initial = super().get_initial()
security_contact = self.object.security_contact
invalid_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
invalid_emails = DefaultEmail.get_all_emails()
if security_contact is None or security_contact.email in invalid_emails:
initial["security_email"] = None
return initial

View file

@ -36,6 +36,7 @@ from registrar.utility.email_invitations import (
send_portfolio_invitation_remove_email,
send_portfolio_member_permission_remove_email,
send_portfolio_member_permission_update_email,
send_portfolio_update_emails_to_portfolio_admins,
)
from registrar.utility.errors import MissingEmailError
from registrar.utility.enums import DefaultUserValues
@ -930,6 +931,20 @@ class PortfolioOrganizationView(DetailView, FormMixin):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
user = request.user
try:
if not send_portfolio_update_emails_to_portfolio_admins(
editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization"
):
messages.warning(self.request, "Could not send email notification to all organization admins.")
except Exception as e:
messages.error(
request,
f"An unexpected error occurred: {str(e)}. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True)
return None
return self.form_valid(form)
else:
return self.form_invalid(form)
@ -982,6 +997,45 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin):
form = self.get_form()
return self.render_to_response(self.get_context_data(form=form))
def post(self, request, *args, **kwargs):
"""Handle POST requests to process form submission."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
user = request.user
try:
if not send_portfolio_update_emails_to_portfolio_admins(
editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official"
):
messages.warning(self.request, "Could not send email notification to all organization admins.")
except Exception as e:
messages.error(
request,
f"An unexpected error occurred: {str(e)}. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True)
return None
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""Handle the case when the form is valid."""
self.object = form.save(commit=False)
self.object.creator = self.request.user
self.object.save()
messages.success(self.request, "The senior official information for this portfolio has been updated.")
return super().form_valid(form)
def form_invalid(self, form):
"""Handle the case when the form is invalid."""
return self.render_to_response(self.get_context_data(form=form))
def get_success_url(self):
"""Redirect to the overview page for the portfolio."""
return reverse("senior-official")
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
class PortfolioMembersView(View):