diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 7a3647582..a6c4ffd8e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -125,6 +125,8 @@ class DomainAdmin(ListHeaderAdmin): def response_change(self, request, obj): ACTION_BUTTON = "_place_client_hold" + GET_SECURITY_EMAIL="_get_security_contact" + SET_SECURITY_EMAIL="_set_security_contact" if ACTION_BUTTON in request.POST: try: obj.place_client_hold() @@ -140,6 +142,43 @@ class DomainAdmin(ListHeaderAdmin): % obj.name, ) return HttpResponseRedirect(".") + + if GET_SECURITY_EMAIL in request.POST: + try: + security_email=obj.get_security_email() + + + except Exception as err: + self.message_user(request, err, messages.ERROR) + else: + self.message_user(request, + ( + "The security email is %" + ". Thanks!" + ) + % security_email, + ) + return HttpResponseRedirect(".") + + return super().response_change(request, obj) + def response_change(self, request, obj): + ACTION_BUTTON = "_get_security_email" + + if ACTION_BUTTON in request.POST: + try: + obj.security + except Exception as err: + self.message_user(request, err, messages.ERROR) + else: + self.message_user( + request, + ( + "%s is in client hold. This domain is no longer accessible on" + " the public internet." + ) + % obj.name, + ) + return HttpResponseRedirect(".") return super().response_change(request, obj) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index a7e46f888..0edfe9e7e 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -19,7 +19,6 @@ from .utility.domain_helper import DomainHelper from .utility.time_stamped_model import TimeStampedModel from .public_contact import PublicContact - logger = logging.getLogger(__name__) @@ -273,19 +272,92 @@ class Domain(TimeStampedModel, DomainHelper): # use admin as type parameter for this contact raise NotImplementedError() + def get_default_security_contact(self): + logger.info("getting default sec contact") + contact = PublicContact.get_default_security() + contact.domain = self + return contact + def _update_domain_with_contact(self, contact:PublicContact,rem=False): + logger.info("received type %s " % contact.contact_type) + domainContact=epp.DomainContact(contact=contact.registry_id,type=contact.contact_type) + + updateDomain=commands.UpdateDomain(name=self.name, add=[domainContact] ) + if rem: + updateDomain=commands.UpdateDomain(name=self.name, rem=[domainContact] ) + logger.info("Send updated") + try: + registry.send(updateDomain, cleaned=True) + except RegistryError as e: + logger.error("Error removing old secuity contact code was %s error was %s" % (e.code, e)) @Cache def security_contact(self) -> PublicContact: """Get or set the security contact for this domain.""" - # TODO: replace this with a real implementation - contact = PublicContact.get_default_security() - contact.domain = self - contact.email = "mayor@igorville.gov" - return contact + + #get the contacts: call _get_property(contacts=True) + #if contacts exist and security contact is in the contact list + #return that contact + #else call the setter + # send the public default contact + try: + contacts=self._get_property("contacts") + except KeyError as err: + logger.info("Found a key error in security_contact get") + ## send public contact to the thingy + + ##TODO - change to get or create in db? + default= self.get_default_security_contact() + # self._cache["contacts"]=[] + # self._cache["contacts"].append({"type":"security", "contact":default}) + self.security_contact=default + return default + except Exception as e: + logger.error("found an error ") + logger.error(e) + else: + logger.info("Showing contacts") + for contact in contacts: + if isinstance(contact, dict) and "type" in contact.keys() and \ + "contact" in contact.keys() and contact["type"]=="security": + return contact["contact"] + + ##TODO -get the security contact, requires changing the implemenation below and the parser from epplib + #request=InfoContact(securityID) + #contactInfo=...send(request) + #convert info to a PublicContact + #return the info in Public conta + #TODO - below line never executes with current logic + return self.get_default_security_contact() + @security_contact.setter # type: ignore def security_contact(self, contact: PublicContact): - # TODO: replace this with a real implementation - pass + """makes the contact in the registry, + for security the public contact should have the org or registrant information + from domain information (not domain application) + and should have the security email from DomainApplication""" + print("making contact in registry") + self._make_contact_in_registry(contact=contact) + + + #create update domain command with security contact + current_security_contact=self.security_contact + if self.security_contact.email is not None: + #if there is already a security contact + domainContact=epp.DomainContact(contact=current_security_contact.registry_id,type=current_security_contact.contact_type) + updateDomain=commands.UpdateDomain(name=self.name, rem=[domainContact] ) + try: + registry.send(updateDomain, cleaned=True) + except RegistryError as e: + logger.error("Error removing old secuity contact code was %s error was %s" % (e.code, e)) + + addDomainContact=epp.DomainContact(contact=contact.registry_id,type=contact.contact_type) + updateDomainAdd=commands.UpdateDomain(name=self.name, rem=[addDomainContact] ) + try: + registry.send(updateDomainAdd, cleaned=True) + except RegistryError as e: + logger.error("Error removing old security contact code was %s error was %s" % (e.code, e)) + + @Cache def technical_contact(self) -> PublicContact: @@ -315,6 +387,11 @@ class Domain(TimeStampedModel, DomainHelper): """This domain should not be active.""" raise NotImplementedError("This is not implemented yet.") + def get_security_email(self): + logger.info("get_security_email-> getting the contact ") + secContact=self.security_contact + return secContact.email + def remove_client_hold(self): """This domain is okay to be active.""" raise NotImplementedError() @@ -380,6 +457,8 @@ class Domain(TimeStampedModel, DomainHelper): already_tried_to_create = False while True: try: + logger.info("_get_or_create_domain()-> getting info on the domain, should hit an error") + req = commands.InfoDomain(name=self.name) return registry.send(req, cleaned=True).res_data[0] except RegistryError as e: @@ -387,24 +466,84 @@ class Domain(TimeStampedModel, DomainHelper): raise e if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: # avoid infinite loop - already_tried_to_create = True - registrant = self._get_or_create_contact( - PublicContact.get_default_registrant() - ) - req = commands.CreateDomain( - name=self.name, - registrant=registrant.id, - auth_info=epp.DomainAuthInfo( - pw="2fooBAR123fooBaz" - ), # not a password - ) - registry.send(req, cleaned=True) - # no error, so go ahead and update state - self.state = Domain.State.CREATED - self.save() - else: - raise e + already_tried_to_create = True + self._make_domain_in_registry + else: + logger.error(e) + logger.error(e.code) + raise e + def _make_domain_in_registry(self): + registrant = self._get_or_create_contact( + PublicContact.get_default_registrant() + ) + + #TODO-notes no chg item for registrant in the epplib should + already_tried_to_create = True + security_contact = self._get_or_create_contact(self.get_default_security_contact()) + + req = commands.CreateDomain( + name=self.name, + registrant=registrant.id, + auth_info=epp.DomainAuthInfo( + pw="2fooBAR123fooBaz" + ), # not a password + ) + logger.info("_get_or_create_domain()-> about to send domain request") + + response=registry.send(req, cleaned=True) + logger.info("_get_or_create_domain()-> registry received create for "+self.name) + logger.info(response) + # no error, so go ahead and update state + self.state = Domain.State.CREATED + self.save() + self._update_domain_with_contact(security_contact) + def _make_contact_in_registry(self, contact: PublicContact): + """Create the contact in the registry, ignore duplicate contact errors""" + create = commands.CreateContact( + id=contact.registry_id, + postal_info=epp.PostalInfo( # type: ignore + name=contact.name, + addr=epp.ContactAddr( + street=[ + getattr(contact, street) + for street in ["street1", "street2", "street3"] + if hasattr(contact, street) + ], + city=contact.city, + pc=contact.pc, + cc=contact.cc, + sp=contact.sp, + ), + org=contact.org, + type="loc", + ), + email=contact.email, + voice=contact.voice, + fax=contact.fax, + auth_info=epp.ContactAuthInfo(pw="2fooBAR123fooBaz"), + ) + # security contacts should only show email addresses, for now + if ( + contact.contact_type + == PublicContact.ContactTypeChoices.SECURITY + ): + DF = epp.DiscloseField + create.disclose = epp.Disclose( + flag=False, + fields={DF.FAX, DF.VOICE, DF.ADDR}, + types={DF.ADDR: "loc"}, + ) + try: + registry.send(create) + return contact + except RegistryError as err: + #don't throw an error if it is just saying this is a duplicate contact + if err.code!=ErrorCode.OBJECT_EXISTS: + raise err + else: + logger.warning("Registrar tried to create duplicate contact for id %s",contact.registry_id) + def _get_or_create_contact(self, contact: PublicContact): """Try to fetch info about a contact. Create it if it does not exist.""" while True: @@ -413,41 +552,7 @@ class Domain(TimeStampedModel, DomainHelper): return registry.send(req, cleaned=True).res_data[0] except RegistryError as e: if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: - create = commands.CreateContact( - id=contact.registry_id, - postal_info=epp.PostalInfo( # type: ignore - name=contact.name, - addr=epp.ContactAddr( - street=[ - getattr(contact, street) - for street in ["street1", "street2", "street3"] - if hasattr(contact, street) - ], - city=contact.city, - pc=contact.pc, - cc=contact.cc, - sp=contact.sp, - ), - org=contact.org, - type="loc", - ), - email=contact.email, - voice=contact.voice, - fax=contact.fax, - auth_info=epp.ContactAuthInfo(pw="2fooBAR123fooBaz"), - ) - # security contacts should only show email addresses, for now - if ( - contact.contact_type - == PublicContact.ContactTypeChoices.SECURITY - ): - DF = epp.DiscloseField - create.disclose = epp.Disclose( - flag=False, - fields={DF.FAX, DF.VOICE, DF.ADDR}, - types={DF.ADDR: "loc"}, - ) - registry.send(create) + return self._make_contact_in_registry(contact=contact) else: raise e @@ -461,6 +566,7 @@ class Domain(TimeStampedModel, DomainHelper): """Contact registry for info about a domain.""" try: # get info from registry + logger.info("_fetch_cache()-> fetching from cache, should create domain") data = self._get_or_create_domain() # extract properties from response # (Ellipsis is used to mean "null") @@ -479,6 +585,7 @@ class Domain(TimeStampedModel, DomainHelper): # remove null properties (to distinguish between "a value of None" and null) cleaned = {k: v for k, v in cache.items() if v is not ...} + logger.info("_fetch_cache()-> cleaned is "+str(cleaned)) # get contact info, if there are any if ( @@ -497,6 +604,8 @@ class Domain(TimeStampedModel, DomainHelper): # extract properties from response # (Ellipsis is used to mean "null") + logger.info("_fetch_cache()->contacts are ") + logger.info(data) contact = { "id": id, "auth_info": getattr(data, "auth_info", ...), @@ -514,6 +623,7 @@ class Domain(TimeStampedModel, DomainHelper): cleaned["contacts"].append( {k: v for k, v in contact.items() if v is not ...} ) + logger.info("_fetch_cache()-> after getting contacts cleaned is "+str(cleaned)) # get nameserver info, if there are any if ( @@ -522,6 +632,7 @@ class Domain(TimeStampedModel, DomainHelper): and isinstance(cleaned["_hosts"], list) and len(cleaned["_hosts"]) ): + ##TODO- add elif in cache set it to be the old cache value, no point in removing cleaned["hosts"] = [] for name in cleaned["_hosts"]: # we do not use _get_or_create_* because we expect the object we diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 5fa89f20a..b1a947adc 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -3,6 +3,8 @@ {% block field_sets %}
+ +
{{ block.super }} {% endblock %} \ No newline at end of file diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 42ee03905..954b4649e 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -10,14 +10,14 @@ import datetime from registrar.models import Domain # add in DomainApplication, User, from unittest import skip -from epplibwrapper import commands +from epplibwrapper import commands,common from registrar.models.domain_application import DomainApplication from registrar.models.domain_information import DomainInformation from registrar.models.draft_domain import DraftDomain +from registrar.models.public_contact import PublicContact from registrar.models.user import User - -class TestDomainCache(TestCase): +class MockEppLib(TestCase): class fakedEppObject(object): """""" @@ -33,6 +33,12 @@ class TestDomainCache(TestCase): contacts=["123"], hosts=["fake.host.com"], ) + infoDomainNoContact= fakedEppObject( + "security", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[], + hosts=["fake.host.com"], + ) mockDataInfoContact = fakedEppObject( "anotherPw", cr_date=datetime.datetime(2023, 7, 25, 19, 45, 35) ) @@ -43,19 +49,33 @@ class TestDomainCache(TestCase): def mockSend(self, _request, cleaned): """""" if isinstance(_request, commands.InfoDomain): + if getattr(_request,"name",None)=="security.gov": + return MagicMock(res_data=[self.infoDomainNoContact]) return MagicMock(res_data=[self.mockDataInfoDomain]) elif isinstance(_request, commands.InfoContact): return MagicMock(res_data=[self.mockDataInfoContact]) + return MagicMock(res_data=[self.mockDataInfoHosts]) def setUp(self): """mock epp send function as this will fail locally""" - self.patcher = patch("registrar.models.domain.registry.send") - self.mockedSendFunction = self.patcher.start() + self.mockSendPatch = patch("registrar.models.domain.registry.send") + self.mockedSendFunction = self.mockSendPatch.start() self.mockedSendFunction.side_effect = self.mockSend def tearDown(self): - self.patcher.stop() + self.mockSendPatch.stop() + +class TestDomainCache(MockEppLib): + + + # def setUp(self): + # #call setup from the mock epplib + # super().setUp() + + # def tearDown(self): + # #call setup from the mock epplib + # super().tearDown() def test_cache_sets_resets(self): """Cache should be set on getter and reset on setter calls""" @@ -120,18 +140,17 @@ class TestDomainCache(TestCase): # get and check hosts is set correctly domain._get_property("hosts") self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) - + ##IS THERE AN ERROR HERE???, class TestDomainCreation(TestCase): """Rule: An approved domain application must result in a domain""" - def setUp(self): - """ - Background: - Given that a valid domain application exists - """ - - @skip("not implemented yet") + # def setUp(self): + # """ + # Background: + # Given that a valid domain application exists + # """ + def test_approved_application_creates_domain_locally(self): """ Scenario: Analyst approves a domain application @@ -139,19 +158,21 @@ class TestDomainCreation(TestCase): Then a Domain exists in the database with the same `name` But a domain object does not exist in the registry """ + patcher = patch("registrar.models.domain.Domain._get_or_create_domain") + mocked_domain_creation=patcher.start() draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") user, _ = User.objects.get_or_create() application = DomainApplication.objects.create( creator=user, requested_domain=draft_domain ) # skip using the submit method - application.status = DomainApplication.SUBMITTED - #trnasistion to approve state + application.status = DomainApplication.SUBMITTED + #transition to approve state application.approve() - - # should be an information present for this domain + # should hav information present for this domain domain = Domain.objects.get(name="igorville.gov") self.assertTrue(domain) + mocked_domain_creation.assert_not_called() @skip("not implemented yet") @@ -190,9 +211,12 @@ class TestDomainCreation(TestCase): domain.activate() domain.save() self.assertIn("ok", domain.status) + + def tearDown(self) -> None: + Domain.objects.delete() + # User.objects.delete() - -class TestRegistrantContacts(TestCase): +class TestRegistrantContacts(MockEppLib): """Rule: Registrants may modify their WHOIS data""" def setUp(self): @@ -201,9 +225,33 @@ class TestRegistrantContacts(TestCase): Given the registrant is logged in And the registrant is the admin on a domain """ - pass + super().setUp() + #mock create contact email extension + self.contactMailingAddressPatch = patch("registrar.models.domain.commands.command_extensions.CreateContactMailingAddressExtension") + self.mockCreateContactExtension=self.contactMailingAddressPatch.start() + + #mock create contact + self.createContactPatch = patch("registrar.models.domain.commands.CreateContact") + self.mockCreateContact=self.createContactPatch.start() + #mock the sending + + + self.domain,_ = Domain.objects.get_or_create(name="security.gov") + # draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") + # user, _ = User.objects.get_or_create() + + # self.application = DomainApplication.objects.create( + # creator=user, requested_domain=draft_domain + # ) + # self.application.status = DomainApplication.SUBMITTED + #transition to approve state + + def tearDown(self): + super().tearDown() + # self.contactMailingAddressPatch.stop() + # self.createContactPatch.stop() - @skip("not implemented yet") + # @skip("source code not implemented") def test_no_security_email(self): """ Scenario: Registrant has not added a security contact email @@ -212,7 +260,29 @@ class TestRegistrantContacts(TestCase): Then the domain has a valid security contact with CISA defaults And disclose flags are set to keep the email address hidden """ - raise + print(self.domain) + #get security contact + expectedSecContact=PublicContact.get_default_security() + expectedSecContact.domain=self.domain + + receivedSecContact=self.domain.security_contact + + DF = common.DiscloseField + di = common.Disclose(flag=False, fields={DF.FAX, DF.VOICE, DF.ADDR}, types={DF.ADDR: "loc"}) + + #check docs here looks like we may have more than one address field but + addr = common.ContactAddr(street=[expectedSecContact.street1,expectedSecContact.street2,expectedSecContact.street3] , city=expectedSecContact.city, pc=expectedSecContact.pc, cc=expectedSecContact.cc, sp=expectedSecContact.sp) + pi = common.PostalInfo(name=expectedSecContact.name, addr=addr, org=expectedSecContact.org, type="loc") + ai = common.ContactAuthInfo(pw='feedabee') + expectedCreateCommand=commands.CreateContact(id=expectedSecContact.registry_id, postal_info=pi, email=expectedSecContact.email, voice=expectedSecContact.voice, fax=expectedSecContact.fax, auth_info=ai, disclose=di, vat=None, ident=None, notify_email=None) + expectedUpdateDomain =commands.UpdateDomain(name=self.domain.name, add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")]) + #check that send has triggered the create command + + self.mockedSendFunction.assert_any_call(expectedCreateCommand,True) + self.mockedSendFunction.assert_any_call(expectedUpdateDomain, True) + #check that the security contact sent is the same as the one recieved + self.assertEqual(receivedSecContact,expectedSecContact) + @skip("not implemented yet") def test_user_adds_security_email(self): @@ -224,7 +294,30 @@ class TestRegistrantContacts(TestCase): And Domain sends `commands.UpdateDomain` to the registry with the newly created contact of type 'security' """ - raise + #make a security contact that is a PublicContact + expectedSecContact=PublicContact.get_default_security() + expectedSecContact.domain=self.domain + expectedSecContact.email="newEmail@fake.com" + expectedSecContact.registry_id="456" + expectedSecContact.name="Fakey McPhakerson" + self.domain.security_contact=expectedSecContact + + #check create contact sent with email + DF = common.DiscloseField + di = common.Disclose(flag=False, fields={DF.FAX, DF.VOICE, DF.ADDR}, types={DF.ADDR: "loc"}) + + addr = common.ContactAddr(street=[expectedSecContact.street1,expectedSecContact.street2,expectedSecContact.street3] , city=expectedSecContact.city, pc=expectedSecContact.pc, cc=expectedSecContact.cc, sp=expectedSecContact.sp) + pi = common.PostalInfo(name=expectedSecContact.name, addr=addr, org=expectedSecContact.org, type="loc") + ai = common.ContactAuthInfo(pw='feedabee') + + expectedCreateCommand=commands.CreateContact(id=expectedSecContact.registry_id, postal_info=pi, email=expectedSecContact.email, voice=expectedSecContact.voice, fax=expectedSecContact.fax, auth_info=ai, disclose=di, vat=None, ident=None, notify_email=None) + expectedUpdateDomain =commands.UpdateDomain(name=self.domain.name, add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")]) + + #check that send has triggered the create command for the contact + self.mockedSendFunction.assert_any_call(expectedCreateCommand, True) + ##check domain contact was updated + self.mockedSendFunction.assert_any_call(expectedUpdateDomain, True) + @skip("not implemented yet") def test_security_email_is_idempotent(self): @@ -237,6 +330,10 @@ class TestRegistrantContacts(TestCase): # implementation note: this requires seeing what happens when these are actually # sent like this, and then implementing appropriate mocks for any errors the # registry normally sends in this case + #will send epplibwrapper.errors.RegistryError with code 2302 for a duplicate contact + + #set the smae fake contact to the email + #show no errors raise @skip("not implemented yet") @@ -428,7 +525,7 @@ class TestRegistrantDNSSEC(TestCase): def test_user_adds_dns_data(self): """ Scenario: Registrant adds DNS data - ... + """ raise @@ -436,7 +533,7 @@ class TestRegistrantDNSSEC(TestCase): def test_dnssec_is_idempotent(self): """ Scenario: Registrant adds DNS data twice, due to a UI glitch - ... + """ # implementation note: this requires seeing what happens when these are actually # sent like this, and then implementing appropriate mocks for any errors the diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 6a33ec994..424c8c093 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -269,6 +269,8 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): contact.email = new_email contact.save() + ##update security email here + #call the setter messages.success( self.request, "The security email for this domain have been updated." )