From 083deb724be0a8cd8ae93549cacc31681cc7f946 Mon Sep 17 00:00:00 2001 From: Gus Brodman Date: Mon, 30 Mar 2026 15:17:51 -0400 Subject: [PATCH] Default to skipping optional domain RDAP events 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 f29ebc77099..30af9b84d41 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 static int provideRdapResultSetMaxSize() { 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 44fcf074497..c9446bb472b 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.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 @@ record HistoryTimeAndRegistrar(DateTime modificationTime, String registrarId) {} @Nullable String rdapTosStaticUrl; + @Inject + @Config("rdapIncludeOptionalHistoryResults") + boolean rdapIncludeOptionalHistoryResults; + @Inject @RequestServerName String serverName; @Inject RdapAuthorization rdapAuthorization; @Inject Clock clock; @@ -328,9 +334,27 @@ RdapDomain createRdapDomain(Domain domain, OutputDataType outputDataType) { .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 @@ private static ImmutableMap getLastHistory * 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 @@ private ImmutableList makeOptionalEvents(EppResource resource) { 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 8151d213fbf..e1450c74849 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 @@ void beforeEach() { 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 @@ void testDomain_full() { .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 @@ void testDomain_logged_out() { .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 2c56fd1b4a4..161f19d1cb6 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 8a7257e7eb7..45411b5fd39 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 00000000000..17458837624 --- /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 2a3447a9ca8..c089ecd53cb 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 00000000000..e1c7ea3b633 --- /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" + } + ] + } + ] +}