From 9f3dfec118d25064e37140a7b5112ded966c048a Mon Sep 17 00:00:00 2001 From: gbrodman Date: Thu, 2 Apr 2026 15:12:17 -0400 Subject: [PATCH] Default to skipping optional domain RDAP events (#2995) We don't need these, so there's no point in adding database load (but we leave the option to include them in the future). Note that these are, and were, only ever included for domains so we don't need to worry about hosts. --- .../registry/config/RegistryConfig.java | 13 ++ .../registry/rdap/RdapJsonFormatter.java | 58 ++++--- .../registry/rdap/RdapJsonFormatterTest.java | 19 ++- .../registry/rdap/rdap_domain_deleted.json | 5 - .../registry/rdap/rdapjson_domain_full.json | 5 - .../rdapjson_domain_full_with_history.json | 144 ++++++++++++++++++ .../rdap/rdapjson_domain_logged_out.json | 5 - ...apjson_domain_logged_out_with_history.json | 140 +++++++++++++++++ 8 files changed, 342 insertions(+), 47 deletions(-) create mode 100644 core/src/test/resources/google/registry/rdap/rdapjson_domain_full_with_history.json create mode 100644 core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out_with_history.json diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index f29ebc770..30af9b84d 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -988,6 +988,19 @@ public final class RegistryConfig { return 100; } + /** + * Whether to include optional RDAP history results in domain responses. + * + *

The RDAP Response Profile (Feb 2024) section 2.3 specifies that while registration and + * expiration events are required, other types are optional. In an effort to reduce database + * load, we (by default) omit the optional events. + */ + @Provides + @Config("rdapIncludeOptionalHistoryResults") + public static boolean provideRdapIncludeOptionalHistoryResults() { + return false; + } + /** * Maximum QPS for the Google Cloud Monitoring V3 (aka Stackdriver) API. The QPS limit can be * adjusted by contacting Cloud Support. diff --git a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java index 44fcf0744..c9446bb47 100644 --- a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java +++ b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java @@ -20,6 +20,8 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static google.registry.model.EppResourceUtils.isLinked; import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm; +import static google.registry.util.DateTimeUtils.toDateTime; +import static google.registry.util.DateTimeUtils.toInstant; import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.annotations.VisibleForTesting; @@ -114,6 +116,10 @@ public class RdapJsonFormatter { @Nullable String rdapTosStaticUrl; + @Inject + @Config("rdapIncludeOptionalHistoryResults") + boolean rdapIncludeOptionalHistoryResults; + @Inject @RequestServerName String serverName; @Inject RdapAuthorization rdapAuthorization; @Inject Clock clock; @@ -328,9 +334,27 @@ public class RdapJsonFormatter { .setEventAction(EventAction.LAST_UPDATE_OF_RDAP_DATABASE) .setEventDate(getRequestTime()) .build()); + // RDAP Response Profile section 2.3.2.2: + // "The event of eventAction type last changed MUST be omitted if the domain has not been + // updated since it was created." While it is possible for the domain to be changed out of band + // (i.e. without updating lastEppUpdateTime), that can only happen for domains that have already + // been modified in some way. As a result, we can ignore those cases here. + if (domain.getLastEppUpdateTime() != null + && domain.getLastEppUpdateTime().isAfter(toInstant(domain.getCreationTime()))) { + // Creates an RDAP event object as defined by RFC 9083 + builder + .eventsBuilder() + .add( + Event.builder() + .setEventAction(EventAction.LAST_CHANGED) + .setEventDate(toDateTime(domain.getLastEppUpdateTime())) + .build()); + } // RDAP Response Profile section 2.3.2 discusses optional events. We add some of those // here. We also add a few others we find interesting. - builder.eventsBuilder().addAll(makeOptionalEvents(domain)); + if (rdapIncludeOptionalHistoryResults) { + builder.eventsBuilder().addAll(makeOptionalEvents(domain)); + } // RDAP Response Profile section 2.4.1: // The domain object in the RDAP response MUST contain an entity with the Registrar role. // @@ -756,14 +780,9 @@ public class RdapJsonFormatter { * that we don't need to load HistoryEntries for "summary" responses). */ private ImmutableList makeOptionalEvents(EppResource resource) { + ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); ImmutableMap lastHistoryOfType = getLastHistoryByType(resource); - ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); - DateTime creationTime = resource.getCreationTime(); - DateTime lastChangeTime = - resource.getLastEppUpdateDateTime() == null - ? creationTime - : resource.getLastEppUpdateDateTime(); // The order of the elements is stable - it's the order in which the enum elements are defined // in EventAction for (EventAction rdapEventAction : EventAction.values()) { @@ -772,34 +791,11 @@ public class RdapJsonFormatter { if (historyTimeAndRegistrar == null) { continue; } - DateTime modificationTime = historyTimeAndRegistrar.modificationTime(); - // We will ignore all events that happened before the "creation time", since these events are - // from a "previous incarnation of the domain" (for a domain that was owned by someone, - // deleted, and then bought by someone else) - if (modificationTime.isBefore(creationTime)) { - continue; - } eventsBuilder.add( Event.builder() .setEventAction(rdapEventAction) .setEventActor(historyTimeAndRegistrar.registrarId()) - .setEventDate(modificationTime) - .build()); - // The last change time might not be the lastEppUpdateTime, since some changes happen without - // any EPP update (for example, by the passage of time). - if (modificationTime.isAfter(lastChangeTime) && modificationTime.isBefore(getRequestTime())) { - lastChangeTime = modificationTime; - } - } - // RDAP Response Profile section 2.3.2.2: - // The event of eventAction type last changed MUST be omitted if the domain name has not been - // updated since it was created - if (lastChangeTime.isAfter(creationTime)) { - // Creates an RDAP event object as defined by RFC 9083 - eventsBuilder.add( - Event.builder() - .setEventAction(EventAction.LAST_CHANGED) - .setEventDate(lastChangeTime) + .setEventDate(historyTimeAndRegistrar.modificationTime()) .build()); } return eventsBuilder.build(); diff --git a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java index 8151d213f..e1450c748 100644 --- a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java +++ b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java @@ -143,7 +143,7 @@ class RdapJsonFormatterTest { makeDomain("cat.みんな", hostIpv4, hostIpv6, registrar) .asBuilder() .setCreationTimeForTest(clock.nowUtc().minusMonths(4)) - .setLastEppUpdateTime(clock.nowUtc().minusMonths(3)) + .setLastEppUpdateTime(clock.nowUtc().minusMonths(1)) .build()); domainNoNameserversNoTransfers = persistResource( @@ -307,6 +307,14 @@ class RdapJsonFormatterTest { .isEqualTo(loadJson("rdapjson_domain_full.json")); } + @Test + void testDomain_full_withHistory() { + rdapJsonFormatter.rdapIncludeOptionalHistoryResults = true; + assertAboutJson() + .that(rdapJsonFormatter.createRdapDomain(domainFull, OutputDataType.FULL).toJson()) + .isEqualTo(loadJson("rdapjson_domain_full_with_history.json")); + } + @Test void testDomain_summary() { assertAboutJson() @@ -333,6 +341,15 @@ class RdapJsonFormatterTest { .isEqualTo(loadJson("rdapjson_domain_logged_out.json")); } + @Test + void testDomain_logged_out_withHistory() { + rdapJsonFormatter.rdapAuthorization = RdapAuthorization.PUBLIC_AUTHORIZATION; + rdapJsonFormatter.rdapIncludeOptionalHistoryResults = true; + assertAboutJson() + .that(rdapJsonFormatter.createRdapDomain(domainFull, OutputDataType.FULL).toJson()) + .isEqualTo(loadJson("rdapjson_domain_logged_out_with_history.json")); + } + @Test void testDomain_noNameserversNoTransfers() { assertAboutJson() diff --git a/core/src/test/resources/google/registry/rdap/rdap_domain_deleted.json b/core/src/test/resources/google/registry/rdap/rdap_domain_deleted.json index 2c56fd1b4..161f19d1c 100644 --- a/core/src/test/resources/google/registry/rdap/rdap_domain_deleted.json +++ b/core/src/test/resources/google/registry/rdap/rdap_domain_deleted.json @@ -42,11 +42,6 @@ "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" }, - { - "eventAction": "deletion", - "eventActor": "evilregistrar", - "eventDate": "1999-07-01T00:00:00.000Z" - }, { "eventAction": "last changed", "eventDate": "2009-05-29T20:13:00.000Z" diff --git a/core/src/test/resources/google/registry/rdap/rdapjson_domain_full.json b/core/src/test/resources/google/registry/rdap/rdapjson_domain_full.json index 8a7257e7e..45411b5fd 100644 --- a/core/src/test/resources/google/registry/rdap/rdapjson_domain_full.json +++ b/core/src/test/resources/google/registry/rdap/rdapjson_domain_full.json @@ -37,11 +37,6 @@ "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" }, - { - "eventAction": "transfer", - "eventActor": "unicoderegistrar", - "eventDate": "1999-12-01T00:00:00.000Z" - }, { "eventAction": "last changed", "eventDate": "1999-12-01T00:00:00.000Z" diff --git a/core/src/test/resources/google/registry/rdap/rdapjson_domain_full_with_history.json b/core/src/test/resources/google/registry/rdap/rdapjson_domain_full_with_history.json new file mode 100644 index 000000000..174588376 --- /dev/null +++ b/core/src/test/resources/google/registry/rdap/rdapjson_domain_full_with_history.json @@ -0,0 +1,144 @@ +{ + "objectClassName" : "domain", + "handle" : "F-Q9JYB4C", + "ldhName" : "cat.xn--q9jyb4c", + "unicodeName" : "cat.みんな", + "status" : + [ + "client delete prohibited", + "client renew prohibited", + "client transfer prohibited", + "server update prohibited" + ], + "links" : + [ + { + "rel" : "self", + "href" : "https://example.tld/rdap/domain/cat.xn--q9jyb4c", + "type" : "application/rdap+json" + }, + { + "rel" : "related", + "href" : "https://rdap.example.com/withSlash/domain/cat.xn--q9jyb4c", + "type" : "application/rdap+json" + } + ], + "events": [ + { + "eventAction": "registration", + "eventActor": "unicoderegistrar", + "eventDate": "1999-09-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": "1999-12-01T00:00:00.000Z" + }, + { + "eventAction": "transfer", + "eventActor": "unicoderegistrar", + "eventDate": "1999-12-01T00:00:00.000Z" + } + ], + "nameservers" : + [ + { + "objectClassName" : "nameserver", + "handle" : "2-ROID", + "ldhName" : "ns1.cat.xn--q9jyb4c", + "unicodeName" : "ns1.cat.みんな", + "links" : [ + { + "rel" : "self", + "href" : "https://example.tld/rdap/nameserver/ns1.cat.xn--q9jyb4c", + "type" : "application/rdap+json" + } + ], + "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" : "4-ROID", + "ldhName" : "ns2.cat.xn--q9jyb4c", + "unicodeName" : "ns2.cat.みんな", + "links" : [ + { + "rel" : "self", + "href" : "https://example.tld/rdap/nameserver/ns2.cat.xn--q9jyb4c", + "type" : "application/rdap+json" + } + ], + "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" + }, + { + "rel": "about", + "href": "http://my.fake.url", + "type": "text/html", + "value": "https://rdap.example.com/withSlash/" + } + ], + "publicIds" : + [ + { + "type" : "IANA Registrar ID", + "identifier" : "1" + } + ], + "vcardArray" : + [ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", "みんな"] + ] + ], + "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" + } + ] + } + ] +} diff --git a/core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out.json b/core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out.json index 2a3447a9c..c089ecd53 100644 --- a/core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out.json +++ b/core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out.json @@ -37,11 +37,6 @@ "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" }, - { - "eventAction": "transfer", - "eventActor": "unicoderegistrar", - "eventDate": "1999-12-01T00:00:00.000Z" - }, { "eventAction": "last changed", "eventDate": "1999-12-01T00:00:00.000Z" diff --git a/core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out_with_history.json b/core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out_with_history.json new file mode 100644 index 000000000..e1c7ea3b6 --- /dev/null +++ b/core/src/test/resources/google/registry/rdap/rdapjson_domain_logged_out_with_history.json @@ -0,0 +1,140 @@ +{ + "objectClassName": "domain", + "handle": "F-Q9JYB4C", + "ldhName": "cat.xn--q9jyb4c", + "unicodeName": "cat.みんな", + "status": + [ + "client delete prohibited", + "client renew prohibited", + "client transfer prohibited", + "server update prohibited" + ], + "links": + [ + { + "rel": "self", + "href": "https://example.tld/rdap/domain/cat.xn--q9jyb4c", + "type": "application/rdap+json" + }, + { + "href": "https://rdap.example.com/withSlash/domain/cat.xn--q9jyb4c", + "type": "application/rdap+json", + "rel": "related" + } + ], + "events": [ + { + "eventAction": "registration", + "eventActor": "unicoderegistrar", + "eventDate": "1999-09-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": "1999-12-01T00:00:00.000Z" + }, + { + "eventAction": "transfer", + "eventActor": "unicoderegistrar", + "eventDate": "1999-12-01T00:00:00.000Z" + } + ], + "nameservers": [ + { + "objectClassName": "nameserver", + "handle": "2-ROID", + "ldhName": "ns1.cat.xn--q9jyb4c", + "unicodeName": "ns1.cat.みんな", + "links": [ + { + "rel": "self", + "href": "https://example.tld/rdap/nameserver/ns1.cat.xn--q9jyb4c", + "type": "application/rdap+json" + } + ], + "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": "4-ROID", + "ldhName": "ns2.cat.xn--q9jyb4c", + "unicodeName": "ns2.cat.みんな", + "links": [ + { + "rel": "self", + "href": "https://example.tld/rdap/nameserver/ns2.cat.xn--q9jyb4c", + "type": "application/rdap+json" + } + ], + "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, + "dsData": [{"algorithm":2,"digest":"DEADFACE","digestType":3,"keyTag":1}], + "zoneSigned": true + }, + "entities": [ + { + "objectClassName": "entity", + "handle": "1", + "roles": ["registrar"], + "links": [ + { + "rel": "self", + "href": "https://example.tld/rdap/entity/1", + "type": "application/rdap+json" + }, + { + "rel": "about", + "href": "http://my.fake.url", + "type": "text/html", + "value": "https://rdap.example.com/withSlash/" + } + ], + "publicIds": [ + { + "type": "IANA Registrar ID", + "identifier": "1" + } + ], + "vcardArray": + [ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", "みんな"] + ] + ], + "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" + } + ] + } + ] +}