diff --git a/core/src/main/java/google/registry/beam/rde/RdePipeline.java b/core/src/main/java/google/registry/beam/rde/RdePipeline.java index a44d030ea..8c32b8623 100644 --- a/core/src/main/java/google/registry/beam/rde/RdePipeline.java +++ b/core/src/main/java/google/registry/beam/rde/RdePipeline.java @@ -466,13 +466,10 @@ public class RdePipeline implements Serializable { // Contacts and hosts are only deposited in RDE, not BRDA. if (pendingDeposit.mode() == RdeMode.FULL) { HashSet contacts = new HashSet<>(); - contacts.add(domain.getAdminContact().getKey()); - contacts.add(domain.getTechContact().getKey()); - domain.getRegistrant().ifPresent(r -> contacts.add(r.getKey())); - // Billing contact is not mandatory. - if (domain.getBillingContact() != null) { - contacts.add(domain.getBillingContact().getKey()); - } + domain.getAdminContact().ifPresent(c -> contacts.add(c.getKey())); + domain.getTechContact().ifPresent(c -> contacts.add(c.getKey())); + domain.getRegistrant().ifPresent(c -> contacts.add(c.getKey())); + domain.getBillingContact().ifPresent(c -> contacts.add(c.getKey())); referencedContactCounter.inc(contacts.size()); contacts.forEach( contactRepoId -> diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java index 721463b66..c853a8208 100644 --- a/core/src/main/java/google/registry/model/domain/DomainBase.java +++ b/core/src/main/java/google/registry/model/domain/DomainBase.java @@ -46,6 +46,7 @@ import google.registry.model.EppResource; import google.registry.model.EppResource.ResourceWithTransferData; import google.registry.model.billing.BillingRecurrence; import google.registry.model.contact.Contact; +import google.registry.model.domain.DesignatedContact.Type; import google.registry.model.domain.launch.LaunchNotice; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.secdns.DomainDsData; @@ -131,10 +132,10 @@ public class DomainBase extends EppResource @Expose @Transient Set> nsHosts; /** Contacts. */ - @Expose VKey adminContact; + @Expose @Nullable VKey adminContact; - @Expose VKey billingContact; - @Expose VKey techContact; + @Expose @Nullable VKey billingContact; + @Expose @Nullable VKey techContact; @Expose @Nullable VKey registrantContact; /** Authorization info (aka transfer secret) of the domain. */ @@ -589,24 +590,32 @@ public class DomainBase extends EppResource return Optional.ofNullable(registrantContact); } - public VKey getAdminContact() { - return adminContact; + public Optional> getAdminContact() { + return Optional.ofNullable(adminContact); } - public VKey getBillingContact() { - return billingContact; + public Optional> getBillingContact() { + return Optional.ofNullable(billingContact); } - public VKey getTechContact() { - return techContact; + public Optional> getTechContact() { + return Optional.ofNullable(techContact); } - /** Associated contacts for the domain (other than registrant). */ + /** + * Associated contacts for the domain (other than registrant). + * + *

Note: This can be an empty set if no contacts are present for the domain. + */ public ImmutableSet getContacts() { return getAllContacts(false); } - /** Gets all associated contacts for the domain, including the registrant. */ + /** + * Gets all associated contacts for the domain, including the registrant. + * + *

Note: This can be an empty set if no contacts are present for the domain. + */ public ImmutableSet getAllContacts() { return getAllContacts(true); } @@ -615,7 +624,11 @@ public class DomainBase extends EppResource return authInfo; } - /** Returns all referenced contacts from this domain. */ + /** + * Returns all referenced contacts from this domain. + * + *

Note: This can be an empty set if no contacts are present for the domain. + */ public ImmutableSet> getReferencedContacts() { return nullToEmptyImmutableCopy(getAllContacts(true)).stream() .map(DesignatedContact::getContactKey) @@ -625,18 +638,12 @@ public class DomainBase extends EppResource private ImmutableSet getAllContacts(boolean includeRegistrant) { ImmutableSet.Builder builder = new ImmutableSet.Builder<>(); - if (includeRegistrant && registrantContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact)); - } - if (adminContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact)); - } - if (billingContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact)); - } - if (techContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact)); + if (includeRegistrant) { + getRegistrant().ifPresent(c -> builder.add(DesignatedContact.create(Type.REGISTRANT, c))); } + getAdminContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.ADMIN, c))); + getBillingContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.BILLING, c))); + getTechContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.TECH, c))); return builder.build(); } @@ -652,11 +659,13 @@ public class DomainBase extends EppResource */ void setContactFields(Set contacts, boolean includeRegistrant) { // Set the individual contact fields. - billingContact = techContact = adminContact = null; + billingContact = null; + techContact = null; + adminContact = null; if (includeRegistrant) { registrantContact = null; } - HashSet contactsDiscovered = new HashSet<>(); + HashSet contactsDiscovered = new HashSet<>(); for (DesignatedContact contact : contacts) { checkArgument( !contactsDiscovered.contains(contact.getType()), @@ -687,7 +696,7 @@ public class DomainBase extends EppResource /** Predicate to determine if a given {@link DesignatedContact} is the registrant. */ static final Predicate IS_REGISTRANT = - (DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type); + (DesignatedContact contact) -> Type.REGISTRANT.equals(contact.type); /** An override of {@link EppResource#asBuilder} with tighter typing. */ @Override diff --git a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java index ef7fcbcf6..29ebca3ea 100644 --- a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java +++ b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java @@ -381,8 +381,11 @@ public class RdapJsonFormatter { () -> ImmutableSet.copyOf(replicaTm().loadByKeys(domain.getNameservers()).values())); // Load the registrant and other contacts and add them to the data. + ImmutableSet> contacts = domain.getReferencedContacts(); ImmutableMap, Contact> loadedContacts = - replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(domain.getReferencedContacts())); + contacts.isEmpty() + ? ImmutableMap.of() + : replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(contacts)); // RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses redaction of // fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc. @@ -544,7 +547,8 @@ public class RdapJsonFormatter { // TODO(mcilwain): Once the RDAP profile is fully updated for minimum registration data set, // we will want to not include non-existent contacts at all, rather than // pretending they exist and just showing REDACTED info. This is especially - // important for authorized flows, where you wouldn't expect to see redaction. + // important for authorized flows, where you wouldn't expect to see redaction + // (although no one actually has access to authorized flows yet). boolean isAuthorized = contact.isPresent() && rdapAuthorization.isAuthorizedForRegistrar( diff --git a/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java index 5137c2378..a8efbf582 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java @@ -235,6 +235,19 @@ class DomainInfoFlowTest extends ResourceFlowTestCase { doSuccessfulTest("domain_info_response_no_registrant.xml", false); } + @Test + void testSuccess_noContacts() throws Exception { + persistTestEntities(false); + domain = + persistResource( + domain + .asBuilder() + .setRegistrant(Optional.empty()) + .setContacts(ImmutableSet.of()) + .build()); + doSuccessfulTest("domain_info_response_no_contacts.xml", false); + } + @Test void testSuccess_clTridNotSpecified() throws Exception { setEppInput("domain_info_no_cltrid.xml"); diff --git a/core/src/test/java/google/registry/model/domain/DomainTest.java b/core/src/test/java/google/registry/model/domain/DomainTest.java index 1307bffb1..a6a22f0d7 100644 --- a/core/src/test/java/google/registry/model/domain/DomainTest.java +++ b/core/src/test/java/google/registry/model/domain/DomainTest.java @@ -1023,16 +1023,16 @@ public class DomainTest { DesignatedContact.create(Type.TECH, contact4Key)), true); assertThat(domain.getRegistrant()).hasValue(contact1Key); - assertThat(domain.getAdminContact()).isEqualTo(contact2Key); - assertThat(domain.getBillingContact()).isEqualTo(contact3Key); - assertThat(domain.getTechContact()).isEqualTo(contact4Key); + assertThat(domain.getAdminContact()).hasValue(contact2Key); + assertThat(domain.getBillingContact()).hasValue(contact3Key); + assertThat(domain.getTechContact()).hasValue(contact4Key); // Make sure everything gets nulled out. domain.setContactFields(ImmutableSet.of(), true); assertThat(domain.getRegistrant()).isEmpty(); - assertThat(domain.getAdminContact()).isNull(); - assertThat(domain.getBillingContact()).isNull(); - assertThat(domain.getTechContact()).isNull(); + assertThat(domain.getAdminContact()).isEmpty(); + assertThat(domain.getBillingContact()).isEmpty(); + assertThat(domain.getTechContact()).isEmpty(); // Make sure that changes don't affect the registrant unless requested. domain.setContactFields( @@ -1043,15 +1043,15 @@ public class DomainTest { DesignatedContact.create(Type.TECH, contact4Key)), false); assertThat(domain.getRegistrant()).isEmpty(); - assertThat(domain.getAdminContact()).isEqualTo(contact2Key); - assertThat(domain.getBillingContact()).isEqualTo(contact3Key); - assertThat(domain.getTechContact()).isEqualTo(contact4Key); + assertThat(domain.getAdminContact()).hasValue(contact2Key); + assertThat(domain.getBillingContact()).hasValue(contact3Key); + assertThat(domain.getTechContact()).hasValue(contact4Key); domain = domain.asBuilder().setRegistrant(Optional.of(contact1Key)).build(); domain.setContactFields(ImmutableSet.of(), false); assertThat(domain.getRegistrant()).hasValue(contact1Key); - assertThat(domain.getAdminContact()).isNull(); - assertThat(domain.getBillingContact()).isNull(); - assertThat(domain.getTechContact()).isNull(); + assertThat(domain.getAdminContact()).isEmpty(); + assertThat(domain.getBillingContact()).isEmpty(); + assertThat(domain.getTechContact()).isEmpty(); } @Test diff --git a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java index b834fb0a4..5ad7b262e 100644 --- a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java +++ b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java @@ -27,6 +27,7 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPo import static google.registry.testing.GsonSubject.assertAboutJson; import static org.mockito.Mockito.verify; +import com.google.common.collect.ImmutableSet; import com.google.gson.JsonObject; import google.registry.model.contact.Contact; import google.registry.model.domain.Domain; @@ -292,6 +293,20 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json"); } + @Test + void testValidDomain_notLoggedIn_contactsShowRedacted_whenNoContactsExist() { + // Even though the domain has no contacts, it still shows a full set of REDACTED fields through + // RDAP. + persistResource( + loadByForeignKey(Domain.class, "cat.lol", clock.nowUtc()) + .get() + .asBuilder() + .setRegistrant(Optional.empty()) + .setContacts(ImmutableSet.of()) + .build()); + assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_exist_with_remark.json"); + } + @Test void testValidDomain_loggedInAsOtherRegistrar_noContacts() { login("idnregistrar"); diff --git a/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java b/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java index 54f334fe1..9bf33cec5 100644 --- a/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java +++ b/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java @@ -307,6 +307,25 @@ class DomainWhoisResponseTest { .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_registrant.txt"), 1)); } + @Test + void getPlainTextOutputTest_noContacts() { + DomainWhoisResponse domainWhoisResponse = + new DomainWhoisResponse( + domain + .asBuilder() + .setRegistrant(Optional.empty()) + .setContacts(ImmutableSet.of()) + .build(), + false, + "Please contact registrar", + clock.nowUtc()); + assertThat( + domainWhoisResponse.getResponse( + false, + "Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested.")) + .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_contacts.txt"), 1)); + } + @Test void getPlainTextOutputTest_registrarAbuseInfoMissing() { persistResource(abuseContact.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build()); diff --git a/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_contacts.xml b/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_contacts.xml new file mode 100644 index 000000000..5b64419b8 --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_contacts.xml @@ -0,0 +1,35 @@ + + + + Command completed successfully + + + + example.tld + %ROID% + + + ns1.example.tld + ns1.example.net + + ns1.example.tld + ns2.example.tld + NewRegistrar + TheRegistrar + 1999-04-03T22:00:00.0Z + NewRegistrar + 1999-12-03T09:00:00.0Z + 2005-04-03T22:00:00.0Z + 2000-04-08T09:00:00.0Z + + 2fooBAR + + + + + ABC-12345 + server-trid + + + diff --git a/core/src/test/resources/google/registry/rdap/rdap_domain_no_contacts_exist_with_remark.json b/core/src/test/resources/google/registry/rdap/rdap_domain_no_contacts_exist_with_remark.json new file mode 100644 index 000000000..dac53076c --- /dev/null +++ b/core/src/test/resources/google/registry/rdap/rdap_domain_no_contacts_exist_with_remark.json @@ -0,0 +1,263 @@ +{ + "rdapConformance": [ + "rdap_level_0", + "icann_rdap_response_profile_0", + "icann_rdap_technical_implementation_guide_0" + ], + "objectClassName": "domain", + "handle": "%DOMAIN_HANDLE_1%", + "ldhName": "%DOMAIN_PUNYCODE_NAME_1%", + "status": [ + "client delete prohibited", + "client renew prohibited", + "client transfer prohibited", + "server update prohibited" + ], + "links": [ + { + "href": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "self" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related" + } + ], + "events": [ + { + "eventAction": "registration", + "eventActor": "TheRegistrar", + "eventDate": "1997-01-01T00:00:00.000Z" + }, + { + "eventAction": "expiration", + "eventDate": "2110-10-08T00:44:59.000Z" + }, + { + "eventAction": "last update of RDAP database", + "eventDate": "2000-01-01T00:00:00.000Z" + }, + { + "eventAction": "last changed", + "eventDate": "2009-05-29T20:13:00.000Z" + } + ], + "nameservers": [ + { + "objectClassName": "nameserver", + "handle": "%NAMESERVER_HANDLE_1%", + "ldhName": "%NAMESERVER_NAME_1%", + "links": [ + { + "href": "https://example.tld/rdap/nameserver/%NAMESERVER_NAME_1%", + "type": "application/rdap+json", + "rel": "self" + } + ], + "remarks": [ + { + "title": "Incomplete Data", + "type": "object truncated due to unexplainable reasons", + "description": ["Summary data only. For complete data, send a specific query for the object."] + } + ] + }, + { + "objectClassName": "nameserver", + "handle": "%NAMESERVER_HANDLE_2%", + "ldhName": "%NAMESERVER_NAME_2%", + "links": [ + { + "href": "https://example.tld/rdap/nameserver/%NAMESERVER_NAME_2%", + "type": "application/rdap+json", + "rel": "self" + } + ], + "remarks": [ + { + "title": "Incomplete Data", + "type": "object truncated due to unexplainable reasons", + "description": ["Summary data only. For complete data, send a specific query for the object."] + } + ] + } + ], + "secureDNS" : { + "delegationSigned": true, + "zoneSigned":true, + "dsData":[ + {"algorithm":2,"digest":"DEADFACE","digestType":3,"keyTag":1} + ] + }, + "entities": [ + { + "objectClassName" : "entity", + "handle" : "1", + "roles" : ["registrar"], + "links" : [ + { + "rel" : "self", + "href" : "https://example.tld/rdap/entity/1", + "type" : "application/rdap+json" + } + ], + "publicIds" : [ + { + "type" : "IANA Registrar ID", + "identifier" : "1" + } + ], + "vcardArray" : [ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", "%REGISTRAR_FULL_NAME_1%"] + ] + ], + "entities" : [ + { + "objectClassName":"entity", + "roles":["abuse"], + "status":["active"], + "vcardArray": [ + "vcard", + [ + ["version",{},"text","4.0"], + ["fn",{},"text","Jake Doe"], + ["tel",{"type":["voice"]},"uri","tel:+1.2125551216"], + ["tel",{"type":["fax"]},"uri","tel:+1.2125551216"], + ["email",{},"text","jakedoe@example.com"] + ] + ] + } + ], + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + }, + + { + "objectClassName":"entity", + "handle":"", + "remarks":[ + { + "title":"REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description":[ + "Some of the data in this object has been removed.", + "Contact personal data is visible only to the owning registrar." + ], + "links":[ + { + "href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", + "rel":"alternate", + "type":"text/html" + } + ] + }, + { + "title":"EMAIL REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description":[ + "Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name." + ] + } + ], + "roles":["registrant"], + "vcardArray":[ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", ""] + ] + ] + }, + + { + "objectClassName": "entity", + "handle": "", + "roles":["administrative"], + "remarks": [ + { + "title":"REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description": [ + "Some of the data in this object has been removed.", + "Contact personal data is visible only to the owning registrar." + ], + "links":[ + { + "href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", + "rel":"alternate", + "type":"text/html" + } + ] + }, + { + "title":"EMAIL REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description": [ + "Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name." + ] + } + ], + "vcardArray":[ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", ""] + ] + ] + }, + + { + "objectClassName":"entity", + "handle":"", + "remarks":[ + { + "title":"REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description":[ + "Some of the data in this object has been removed.", + "Contact personal data is visible only to the owning registrar." + ], + "links":[ + { + "href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", + "rel":"alternate", + "type":"text/html" + } + ] + }, + { + "description":[ + "Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name." + ], + "title":"EMAIL REDACTED FOR PRIVACY", + "type":"object redacted due to authorization" + } + ], + "roles": ["technical"], + "vcardArray": [ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", ""] + ] + ] + } + ] +} diff --git a/core/src/test/resources/google/registry/whois/whois_domain_no_contacts.txt b/core/src/test/resources/google/registry/whois/whois_domain_no_contacts.txt new file mode 100644 index 000000000..b839efcfe --- /dev/null +++ b/core/src/test/resources/google/registry/whois/whois_domain_no_contacts.txt @@ -0,0 +1,28 @@ +Domain Name: example.tld +Registry Domain ID: 3-TLD +Registrar WHOIS Server: whois.nic.fakewhois.example +Registrar URL: http://my.fake.url +Updated Date: 2009-05-29T20:13:00Z +Creation Date: 2000-10-08T00:45:00Z +Registry Expiry Date: 2010-10-08T00:44:59Z +Registrar: New Registrar +Registrar IANA ID: 5555555 +Registrar Abuse Contact Email: jakedoe@theregistrar.com +Registrar Abuse Contact Phone: +1.2125551216 +Domain Status: addPeriod https://icann.org/epp#addPeriod +Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited +Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited +Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited +Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited +Domain Status: transferPeriod https://icann.org/epp#transferPeriod +Name Server: ns01.exampleregistrar.tld +Name Server: ns02.exampleregistrar.tld +DNSSEC: signedDelegation +URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/ +>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<< + +For more information on Whois status codes, please visit https://icann.org/epp + +Doodle Disclaimer +I exist so that carriage return +in disclaimer can be tested.