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