1
0
mirror of https://github.com/google/nomulus synced 2026-02-08 05:50:24 +00:00

Remove contact entities from RDAP entirely when they don't exist in DB (#2497)

This is consistent with how other registries are handling RDAP and is also consistent
with overall behavior in WHOIS and domain info flows as implemented in my previous
PRs #2477 and #2490.
This commit is contained in:
Ben McIlwain
2024-07-18 15:33:52 -04:00
committed by GitHub
parent 0241937dee
commit c4e5bc913e
9 changed files with 274 additions and 186 deletions

View File

@@ -76,7 +76,7 @@ public class RdapEntityAction extends RdapActionBase {
// they are global, and might have different roles for different domains.
if (contact.isPresent() && isAuthorized(contact.get())) {
return rdapJsonFormatter.createRdapContactEntity(
contact, ImmutableSet.of(), OutputDataType.FULL);
contact.get(), ImmutableSet.of(), OutputDataType.FULL);
}
}

View File

@@ -461,7 +461,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
.entitySearchResultsBuilder()
.add(
rdapJsonFormatter.createRdapContactEntity(
Optional.of(contact), ImmutableSet.of(), outputDataType));
contact, ImmutableSet.of(), outputDataType));
newCursor =
Optional.of(
CONTACT_CURSOR_PREFIX

View File

@@ -75,7 +75,6 @@ import java.net.InetAddress;
import java.net.URI;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@@ -251,16 +250,6 @@ public class RdapJsonFormatter {
private static final Ordering<DesignatedContact> DESIGNATED_CONTACT_ORDERING =
Ordering.natural().onResultOf(DesignatedContact::getType);
/**
* The list of RDAP contact roles that are required to be present on each domain.
*
* <p>Per RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and
* MAY have others. We also have the BILLING role in our system but it isn't required and is only
* listed if actually present.
*/
private static final ImmutableSet<Role> REQUIRED_CONTACT_ROLES =
ImmutableSet.of(Role.REGISTRANT, Role.ADMIN, Role.TECH);
/** Creates the TOS notice that is added to every reply. */
Notice createTosNotice() {
String linkValue = makeRdapServletRelativeUrl("help", RdapHelpAction.TOS_PATH);
@@ -403,7 +392,6 @@ public class RdapJsonFormatter {
// Convert the contact entities to RDAP output contacts (this also converts the contact types
// to RDAP roles).
Set<RdapContactEntity> rdapContacts = new LinkedHashSet<>();
for (VKey<Contact> contactKey : contactsToRoles.keySet()) {
Set<Role> roles =
contactsToRoles.get(contactKey).stream()
@@ -412,22 +400,13 @@ public class RdapJsonFormatter {
if (roles.isEmpty()) {
continue;
}
rdapContacts.add(
createRdapContactEntity(
Optional.ofNullable(loadedContacts.get(contactKey)), roles, OutputDataType.INTERNAL));
builder
.entitiesBuilder()
.add(
createRdapContactEntity(
loadedContacts.get(contactKey), roles, OutputDataType.INTERNAL));
}
// Loop through all required contact roles and fill in placeholder REDACTED info for any
// required ones that are missing, i.e. because of minimum registration data set.
for (Role role : REQUIRED_CONTACT_ROLES) {
if (rdapContacts.stream().noneMatch(c -> c.roles().contains(role))) {
rdapContacts.add(
createRdapContactEntity(
Optional.empty(), ImmutableSet.of(role), OutputDataType.INTERNAL));
}
}
builder.entitiesBuilder().addAll(rdapContacts);
// Add the nameservers to the data; the load was kicked off above for efficiency.
// RDAP Response Profile 2.9: we MUST have the nameservers
for (Host host : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts)) {
@@ -537,28 +516,20 @@ public class RdapJsonFormatter {
* @param outputDataType whether to generate full or summary data
*/
RdapContactEntity createRdapContactEntity(
Optional<Contact> contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
Contact contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
RdapContactEntity.Builder contactBuilder = RdapContactEntity.builder();
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses censoring of
// fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc.
//
// 2.8 allows for unredacted output for authorized people.
// 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
// (although no one actually has access to authorized flows yet).
boolean isAuthorized =
contact.isPresent()
&& rdapAuthorization.isAuthorizedForRegistrar(
contact.get().getCurrentSponsorRegistrarId());
rdapAuthorization.isAuthorizedForRegistrar(contact.getCurrentSponsorRegistrarId());
VcardArray.Builder vcardBuilder = VcardArray.builder();
if (isAuthorized) {
fillRdapContactEntityWhenAuthorized(
contactBuilder, vcardBuilder, contact.get(), outputDataType);
fillRdapContactEntityWhenAuthorized(contactBuilder, vcardBuilder, contact, outputDataType);
} else {
// GTLD Registration Data Temp Spec 17may18, Appendix A, 2.3, 2.4 and RDAP Response Profile
// 2.7.4.1, 2.7.4.2 - the following fields must be redacted:

View File

@@ -276,27 +276,23 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
}
@Test
void testValidDomain_notLoggedIn_noContacts() {
assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json");
void testValidDomain_notLoggedIn_redactsAllContactInfo() {
assertProperResponseForCatLol("cat.lol", "rdap_domain_redacted_contacts_with_remark.json");
}
@Test
void testValidDomain_notLoggedIn_contactsShowRedacted_evenWhenRegistrantDoesntExist() {
// Even though the registrant is empty on this domain, it still shows a full set of REDACTED
// fields through RDAP.
void testValidDomain_notLoggedIn_showsNoRegistrant_whenRegistrantDoesntExist() {
persistResource(
loadByForeignKey(Domain.class, "cat.lol", clock.nowUtc())
.get()
.asBuilder()
.setRegistrant(Optional.empty())
.build());
assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json");
assertProperResponseForCatLol("cat.lol", "rdap_domain_no_registrant_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.
void testValidDomain_notLoggedIn_containsNoContactEntities_whenNoContactsExist() {
persistResource(
loadByForeignKey(Domain.class, "cat.lol", clock.nowUtc())
.get()
@@ -308,24 +304,38 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
}
@Test
void testValidDomain_loggedInAsOtherRegistrar_noContacts() {
void testValidDomain_loggedIn_containsNoContactEntities_whenNoContactsExist() {
login("evilregistrar");
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_redactsAllContactInfo() {
login("idnregistrar");
assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json");
assertProperResponseForCatLol("cat.lol", "rdap_domain_redacted_contacts_with_remark.json");
}
@Test
void testUpperCase_ignored() {
assertProperResponseForCatLol("CaT.lOl", "rdap_domain_no_contacts_with_remark.json");
assertProperResponseForCatLol("CaT.lOl", "rdap_domain_redacted_contacts_with_remark.json");
}
@Test
void testTrailingDot_ignored() {
assertProperResponseForCatLol("cat.lol.", "rdap_domain_no_contacts_with_remark.json");
assertProperResponseForCatLol("cat.lol.", "rdap_domain_redacted_contacts_with_remark.json");
}
@Test
void testQueryParameter_ignored() {
assertProperResponseForCatLol("cat.lol?key=value", "rdap_domain_no_contacts_with_remark.json");
assertProperResponseForCatLol(
"cat.lol?key=value", "rdap_domain_redacted_contacts_with_remark.json");
}
@Test

View File

@@ -750,7 +750,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
void testDomainMatch_found_loggedInAsOtherRegistrar() {
login("otherregistrar");
runSuccessfulTestWithCatLol(
RequestType.NAME, "cat.lol", "rdap_domain_no_contacts_with_remark.json");
RequestType.NAME, "cat.lol", "rdap_domain_redacted_contacts_with_remark.json");
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -782,7 +782,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.addRegistrar("St. John Chrysostom")
.addNameserver("ns1.cat.lol", "8-ROID")
.addNameserver("ns2.external.tld", "1F-ROID")
.load("rdap_domain_no_contacts_with_remark.json"));
.load("rdap_domain_redacted_contacts_with_remark.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -826,7 +826,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "35-ROID")
.addNameserver("ns2.cat.2.test", "37-ROID")
.load("rdap_domain_no_contacts_with_remark.json"));
.load("rdap_domain_redacted_contacts_with_remark.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -840,7 +840,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "35-ROID")
.addNameserver("ns2.cat.2.test", "37-ROID")
.load("rdap_domain_no_contacts_with_remark.json"));
.load("rdap_domain_redacted_contacts_with_remark.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -1378,7 +1378,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "35-ROID")
.addNameserver("ns2.cat.2.test", "37-ROID")
.load("rdap_domain_no_contacts_with_remark.json"));
.load("rdap_domain_redacted_contacts_with_remark.json"));
verifyMetrics(SearchType.BY_NAMESERVER_NAME, 1, 1);
}
@@ -1392,7 +1392,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "35-ROID")
.addNameserver("ns2.cat.2.test", "37-ROID")
.load("rdap_domain_no_contacts_with_remark.json"));
.load("rdap_domain_redacted_contacts_with_remark.json"));
verifyMetrics(SearchType.BY_NAMESERVER_NAME, 1, 1);
}
@@ -1675,7 +1675,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
runSuccessfulTestWithCatLol(
RequestType.NS_IP,
"bad:f00d:cafe:0:0:0:15:beef",
"rdap_domain_no_contacts_with_remark.json");
"rdap_domain_redacted_contacts_with_remark.json");
verifyMetrics(SearchType.BY_NAMESERVER_ADDRESS, 1, 1);
}

View File

@@ -52,7 +52,6 @@ import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase;
import google.registry.rdap.RdapObjectClasses.TopLevelReplyObject;
import google.registry.testing.FakeClock;
import google.registry.testing.FullFieldsTestEntityHelper;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -373,7 +372,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactRegistrant),
contactRegistrant,
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.FULL)
.toJson())
@@ -386,7 +385,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactRegistrant),
contactRegistrant,
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.SUMMARY)
.toJson())
@@ -400,7 +399,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactRegistrant),
contactRegistrant,
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.FULL)
.toJson())
@@ -420,7 +419,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactRegistrant),
contactRegistrant,
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.FULL)
.toJson())
@@ -433,9 +432,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactAdmin),
ImmutableSet.of(RdapEntity.Role.ADMIN),
OutputDataType.FULL)
contactAdmin, ImmutableSet.of(RdapEntity.Role.ADMIN), OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_admincontact.json"));
}
@@ -446,9 +443,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactTech),
ImmutableSet.of(RdapEntity.Role.TECH),
OutputDataType.FULL)
contactTech, ImmutableSet.of(RdapEntity.Role.TECH), OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_techcontact.json"));
}
@@ -458,8 +453,7 @@ class RdapJsonFormatterTest {
assertAboutJson()
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactTech), ImmutableSet.of(), OutputDataType.FULL)
.createRdapContactEntity(contactTech, ImmutableSet.of(), OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_rolelesscontact.json"));
}
@@ -469,8 +463,7 @@ class RdapJsonFormatterTest {
assertAboutJson()
.that(
rdapJsonFormatter
.createRdapContactEntity(
Optional.of(contactNotLinked), ImmutableSet.of(), OutputDataType.FULL)
.createRdapContactEntity(contactNotLinked, ImmutableSet.of(), OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_unlinkedcontact.json"));
}

View File

@@ -147,117 +147,6 @@
"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", ""]
]
]
}
]
}

View File

@@ -0,0 +1,225 @@
{
"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": "",
"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", ""]
]
]
}
]
}