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" + } + ] + } + ] +}