diff --git a/java/google/registry/env/common/default/WEB-INF/datastore-indexes.xml b/java/google/registry/env/common/default/WEB-INF/datastore-indexes.xml
index 949b3ee2d..922f1a822 100644
--- a/java/google/registry/env/common/default/WEB-INF/datastore-indexes.xml
+++ b/java/google/registry/env/common/default/WEB-INF/datastore-indexes.xml
@@ -26,6 +26,7 @@
+
@@ -71,6 +72,11 @@
+
+
+
+
+
diff --git a/java/google/registry/rdap/RdapActionBase.java b/java/google/registry/rdap/RdapActionBase.java
index 2b1dfd4be..b27a1e246 100644
--- a/java/google/registry/rdap/RdapActionBase.java
+++ b/java/google/registry/rdap/RdapActionBase.java
@@ -215,16 +215,28 @@ public abstract class RdapActionBase implements Runnable {
}
/**
- * Returns true if the EPP resource should be visible. This is true iff:
+ * Returns true if the request is authorized to see the resource.
+ *
+ *
This is true if the resource is not deleted, or the request wants to see deleted items, and
+ * is authorized to do so.
+ */
+ boolean isAuthorized(EppResource eppResource, DateTime now) {
+ return now.isBefore(eppResource.getDeletionTime())
+ || (shouldIncludeDeleted()
+ && getAuthorization()
+ .isAuthorizedForClientId(eppResource.getPersistedCurrentSponsorClientId()));
+ }
+
+ /**
+ * Returns true if the EPP resource should be visible.
+ *
+ *
This is true iff:
* 1. The resource is not deleted, or the request wants to see deleted items, and is authorized to
* do so, and:
* 2. The request did not specify a registrar to filter on, or the registrar matches.
*/
boolean shouldBeVisible(EppResource eppResource, DateTime now) {
- return (now.isBefore(eppResource.getDeletionTime())
- || (shouldIncludeDeleted()
- && getAuthorization()
- .isAuthorizedForClientId(eppResource.getPersistedCurrentSponsorClientId())))
+ return isAuthorized(eppResource, now)
&& (!registrarParam.isPresent()
|| registrarParam.get().equals(eppResource.getPersistedCurrentSponsorClientId()));
}
@@ -366,6 +378,11 @@ public abstract class RdapActionBase implements Runnable {
* @param query an already-defined query to be run; a filter on currentSponsorClientId will be
* added if appropriate
* @param now the time as of which to evaluate the query
+ * @param checkForVisibility true if the results should be checked to make sure they are visible;
+ * normally this should be equal to the shouldIncludeDeleted setting, but in cases where
+ * the query could not check deletion status (due to Datastore limitations such as the
+ * limit of one field queried for inequality, for instance), it may need to be set to true
+ * even when not including deleted records
* @return an {@link RdapResourcesAndIncompletenessWarningType} object containing the list of
* resources and an incompleteness warning flag, which is set to MIGHT_BE_INCOMPLETE iff
* any resources were excluded due to lack of visibility, and the resulting list of
@@ -373,12 +390,12 @@ public abstract class RdapActionBase implements Runnable {
* fetched enough resources
*/
RdapResourcesAndIncompletenessWarningType getMatchingResources(
- Query query, DateTime now) {
+ Query query, boolean checkForVisibility, DateTime now) {
Optional desiredRegistrar = getDesiredRegistrar();
if (desiredRegistrar.isPresent()) {
query = query.filter("currentSponsorClientId", desiredRegistrar.get());
}
- if (!shouldIncludeDeleted()) {
+ if (!checkForVisibility) {
return RdapResourcesAndIncompletenessWarningType.create(query.list());
}
// If we are including deleted resources, we need to check that we're authorized for each one.
diff --git a/java/google/registry/rdap/RdapDomainAction.java b/java/google/registry/rdap/RdapDomainAction.java
index df8f20f13..c8ae1eee4 100644
--- a/java/google/registry/rdap/RdapDomainAction.java
+++ b/java/google/registry/rdap/RdapDomainAction.java
@@ -18,6 +18,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
+import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableMap;
import google.registry.flows.EppException;
@@ -69,8 +70,10 @@ public class RdapDomainAction extends RdapActionBase {
pathSearchString, getHumanReadableObjectTypeName(), e.getMessage()));
}
// The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com.
- DomainResource domainResource = loadByForeignKey(DomainResource.class, pathSearchString, now);
- if (domainResource == null) {
+ DomainResource domainResource =
+ loadByForeignKey(
+ DomainResource.class, pathSearchString, shouldIncludeDeleted() ? START_OF_TIME : now);
+ if ((domainResource == null) || !shouldBeVisible(domainResource, now)) {
throw new NotFoundException(pathSearchString + " not found");
}
return rdapJsonFormatter.makeRdapJsonForDomain(
diff --git a/java/google/registry/rdap/RdapDomainSearchAction.java b/java/google/registry/rdap/RdapDomainSearchAction.java
index 78b0febe1..dde33bd62 100644
--- a/java/google/registry/rdap/RdapDomainSearchAction.java
+++ b/java/google/registry/rdap/RdapDomainSearchAction.java
@@ -19,7 +19,7 @@ import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
-import static google.registry.util.DateTimeUtils.END_OF_TIME;
+import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -28,7 +28,6 @@ import com.google.common.collect.Iterables;
import com.google.common.primitives.Booleans;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.cmd.Query;
-import google.registry.model.EppResourceUtils;
import google.registry.model.domain.DomainResource;
import google.registry.model.host.HostResource;
import google.registry.rdap.RdapJsonFormatter.BoilerplateType;
@@ -156,33 +155,20 @@ public class RdapDomainSearchAction extends RdapActionBase {
* characters (e.g. "ex*"), to avoid queries for all domains in the system. If the TLD is present,
* the initial string is not required (e.g. "*.tld" is valid), because the search will be
* restricted to a single TLD.
+ *
+ *
Searches which include deleted entries are effectively treated as if they have a wildcard,
+ * since the same name can return multiple results.
*/
private RdapSearchResults searchByDomainName(
final RdapSearchPattern partialStringQuery, final DateTime now) {
- // Handle queries without a wildcard -- just load by foreign key.
- if (!partialStringQuery.getHasWildcard()) {
- DomainResource domainResource =
- loadByForeignKey(DomainResource.class, partialStringQuery.getInitialString(), now);
- ImmutableList results = (domainResource == null)
- ? ImmutableList.of()
- : ImmutableList.of(domainResource);
- return makeSearchResults(results, now);
- // Handle queries with a wildcard and no initial string.
- } else if (partialStringQuery.getInitialString().isEmpty()) {
- if (partialStringQuery.getSuffix() == null) {
- throw new UnprocessableEntityException(
- "Initial search string is required for wildcard domain searches without a TLD suffix");
- }
- // Since we aren't searching on fullyQualifiedDomainName, we can perform our one allowed
- // inequality query on deletion time.
- Query query = ofy().load()
- .type(DomainResource.class)
- .filter("tld", partialStringQuery.getSuffix())
- .filter("deletionTime >", now)
- .limit(rdapResultSetMaxSize + 1);
- return makeSearchResults(query.list(), now);
- // Handle queries with a wildcard and an initial string.
- } else {
+ // Handle queries without a wildcard -- just load by foreign key. We can't do this if deleted
+ // entries are included, because there may be multiple nameservers with the same name.
+ if (!partialStringQuery.getHasWildcard() && !shouldIncludeDeleted()) {
+ return searchByDomainNameWithoutWildcard(partialStringQuery, now);
+ }
+ // Handle queries with a wildcard and initial search string. We require either a TLD or an
+ // initial string at least MIN_INITIAL_STRING_LENGTH long.
+ if (!partialStringQuery.getInitialString().isEmpty()) {
if ((partialStringQuery.getSuffix() == null)
&& (partialStringQuery.getInitialString().length()
< RdapSearchPattern.MIN_INITIAL_STRING_LENGTH)) {
@@ -192,44 +178,64 @@ public class RdapDomainSearchAction extends RdapActionBase {
+ " without a TLD suffix",
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
}
-
- // We can't query for undeleted domains as part of the query itself; that would require an
- // inequality query on deletion time, and we are already using inequality queries on
- // fullyQualifiedDomainName. So we instead pick an arbitrary limit of
- // RESULT_SET_SIZE_SCALING_FACTOR times the result set size limit, fetch up to that many, and
- // weed out all deleted domains. If there still isn't a full result set's worth of domains, we
- // give up and return just the ones we found.
- // TODO(b/31546493): Add metrics to figure out how well this works.
- List domainList = new ArrayList<>();
- Query query = ofy().load()
- .type(DomainResource.class)
- .filter("fullyQualifiedDomainName <", partialStringQuery.getNextInitialString())
- .filter("fullyQualifiedDomainName >=", partialStringQuery.getInitialString());
- if (partialStringQuery.getSuffix() != null) {
- query = query.filter("tld", partialStringQuery.getSuffix());
- }
- // Query the domains directly, rather than the foreign keys, because then we have an index on
- // TLD if we need it.
- int numFetched = 0;
- for (DomainResource domain :
- query.limit(RESULT_SET_SIZE_SCALING_FACTOR * rdapResultSetMaxSize)) {
- numFetched++;
- if (EppResourceUtils.isActive(domain, now)) {
- if (domainList.size() >= rdapResultSetMaxSize) {
- return makeSearchResults(
- ImmutableList.copyOf(domainList), IncompletenessWarningType.TRUNCATED, now);
- }
- domainList.add(domain);
- }
- }
- return makeSearchResults(
- domainList,
- ((numFetched == RESULT_SET_SIZE_SCALING_FACTOR * rdapResultSetMaxSize)
- && (domainList.size() < rdapResultSetMaxSize))
- ? IncompletenessWarningType.MIGHT_BE_INCOMPLETE
- : IncompletenessWarningType.NONE,
- now);
+ return searchByDomainNameWithInitialString(partialStringQuery, now);
}
+ if (partialStringQuery.getSuffix() == null) {
+ throw new UnprocessableEntityException(
+ "Initial search string is required for wildcard domain searches without a TLD suffix");
+ }
+ return searchByDomainNameByTld(partialStringQuery.getSuffix(), now);
+ }
+
+ /**
+ * Searches for domains by domain name without a wildcard or interest in deleted entries.
+ */
+ private RdapSearchResults searchByDomainNameWithoutWildcard(
+ final RdapSearchPattern partialStringQuery, final DateTime now) {
+ DomainResource domainResource =
+ loadByForeignKey(DomainResource.class, partialStringQuery.getInitialString(), now);
+ ImmutableList results =
+ ((domainResource == null) || !shouldBeVisible(domainResource, now))
+ ? ImmutableList.of()
+ : ImmutableList.of(domainResource);
+ return makeSearchResults(results, now);
+ }
+
+ /** Searches for domains by domain name with an initial string, wildcard and possible suffix. */
+ private RdapSearchResults searchByDomainNameWithInitialString(
+ final RdapSearchPattern partialStringQuery, final DateTime now) {
+ // We can't query for undeleted domains as part of the query itself; that would require an
+ // inequality query on deletion time, and we are already using inequality queries on
+ // fullyQualifiedDomainName. So we instead pick an arbitrary limit of
+ // RESULT_SET_SIZE_SCALING_FACTOR times the result set size limit, fetch up to that many, and
+ // weed out all deleted domains. If there still isn't a full result set's worth of domains, we
+ // give up and return just the ones we found. Don't use queryItems, because it checks that the
+ // initial string is at least a certain length, which we don't need in this case. Query the
+ // domains directly, rather than the foreign keys, because then we have an index on TLD if we
+ // need it.
+ // TODO(b/31546493): Add metrics to figure out how well this works.
+ Query query =
+ ofy()
+ .load()
+ .type(DomainResource.class)
+ .filter("fullyQualifiedDomainName <", partialStringQuery.getNextInitialString())
+ .filter("fullyQualifiedDomainName >=", partialStringQuery.getInitialString())
+ .limit(RESULT_SET_SIZE_SCALING_FACTOR * rdapResultSetMaxSize);
+ if (partialStringQuery.getSuffix() != null) {
+ query = query.filter("tld", partialStringQuery.getSuffix());
+ }
+ // Always check for visibility, because we couldn't look at the deletionTime in the query.
+ return makeSearchResults(getMatchingResources(query, true, now), now);
+ }
+
+ /** Searches for domains by domain name with a TLD suffix. */
+ private RdapSearchResults searchByDomainNameByTld(String tld, DateTime now) {
+ // Since we aren't searching on fullyQualifiedDomainName, we can perform our one allowed
+ // inequality query on deletion time.
+ Query query =
+ queryItems(
+ DomainResource.class, "tld", tld, shouldIncludeDeleted(), rdapResultSetMaxSize + 1);
+ return makeSearchResults(getMatchingResources(query, shouldIncludeDeleted(), now), now);
}
/**
@@ -237,6 +243,9 @@ public class RdapDomainSearchAction extends RdapActionBase {
*
*
This is a two-step process: get a list of host references by host name, and then look up
* domains by host reference.
+ *
+ *
The includeDeleted parameter does NOT cause deleted nameservers to be searched, only deleted
+ * domains which used to be connected to an undeleted nameserver.
*/
private RdapSearchResults searchByNameserverLdhName(
final RdapSearchPattern partialStringQuery, final DateTime now) {
@@ -259,62 +268,106 @@ public class RdapDomainSearchAction extends RdapActionBase {
*/
private Iterable> getNameserverRefsByLdhName(
final RdapSearchPattern partialStringQuery, final DateTime now) {
- // Handle queries without a wildcard; just load the host by foreign key in the usual way.
+ // Handle queries without a wildcard.
if (!partialStringQuery.getHasWildcard()) {
- Key hostKey = loadAndGetKey(
- HostResource.class, partialStringQuery.getInitialString(), now);
- if (hostKey == null) {
- return ImmutableList.of();
- } else {
- return ImmutableList.of(hostKey);
- }
- // Handle queries with a wildcard.
+ return getNameserverRefsByLdhNameWithoutWildcard(partialStringQuery, now);
+ }
+ // Handle queries with a wildcard and suffix (specifying a suprerordinate domain).
+ if (partialStringQuery.getSuffix() != null) {
+ return getNameserverRefsByLdhNameWithSuffix(partialStringQuery, now);
+ }
+ // If there's no suffix, query the host resources. Query the resources themselves, rather than
+ // the foreign key indexes, because then we have an index on fully qualified host name and
+ // deletion time, so we can check the deletion status in the query itself. The initial string
+ // must be present, to avoid querying every host in the system. This restriction is enforced by
+ // {@link queryItems}.
+ //
+ // Only return the first 1000 nameservers. This could result in an incomplete result set if
+ // a search asks for something like "ns*", but we need to enforce a limit in order to avoid
+ // arbitrarily long-running queries.
+ Query query =
+ queryItems(
+ HostResource.class,
+ "fullyQualifiedHostName",
+ partialStringQuery,
+ false, /* includeDeleted */
+ MAX_NAMESERVERS_IN_FIRST_STAGE);
+ Optional desiredRegistrar = getDesiredRegistrar();
+ if (desiredRegistrar.isPresent()) {
+ query = query.filter("currentSponsorClientId", desiredRegistrar.get());
+ }
+ return query.keys();
+ }
+
+ /** Assembles a list of {@link HostResource} keys by name when the pattern has no wildcard. */
+ private Iterable> getNameserverRefsByLdhNameWithoutWildcard(
+ final RdapSearchPattern partialStringQuery, final DateTime now) {
+ // If we need to check the sponsoring registrar, we need to load the resource rather than just
+ // the key.
+ Optional desiredRegistrar = getDesiredRegistrar();
+ if (desiredRegistrar.isPresent()) {
+ HostResource host =
+ loadByForeignKey(
+ HostResource.class,
+ partialStringQuery.getInitialString(),
+ shouldIncludeDeleted() ? START_OF_TIME : now);
+ return ((host == null)
+ || !desiredRegistrar.get().equals(host.getPersistedCurrentSponsorClientId()))
+ ? ImmutableList.of()
+ : ImmutableList.of(Key.create(host));
} else {
- // If there is a suffix, it must be a domain that we manage. That way, we can look up the
- // domain and search through the subordinate hosts. This is more efficient, and lets us permit
- // wildcard searches with no initial string.
- if (partialStringQuery.getSuffix() != null) {
- DomainResource domainResource = loadByForeignKey(
- DomainResource.class, partialStringQuery.getSuffix(), now);
- if (domainResource == null) {
- // Don't allow wildcards with suffixes which are not domains we manage. That would risk a
- // table scan in some easily foreseeable cases.
- throw new UnprocessableEntityException(
- "A suffix in a lookup by nameserver name must be an in-bailiwick domain");
- }
- ImmutableList.Builder> builder = new ImmutableList.Builder<>();
- for (String fqhn : ImmutableSortedSet.copyOf(domainResource.getSubordinateHosts())) {
- // We can't just check that the host name starts with the initial query string, because
- // then the query ns.exam*.example.com would match against nameserver ns.example.com.
- if (partialStringQuery.matches(fqhn)) {
- Key hostKey = loadAndGetKey(HostResource.class, fqhn, now);
- if (hostKey != null) {
- builder.add(hostKey);
- } else {
- logger.warningfmt("Host key unexpectedly null");
- }
+ Key hostKey =
+ loadAndGetKey(
+ HostResource.class,
+ partialStringQuery.getInitialString(),
+ shouldIncludeDeleted() ? START_OF_TIME : now);
+ return (hostKey == null) ? ImmutableList.of() : ImmutableList.of(hostKey);
+ }
+ }
+
+ /** Assembles a list of {@link HostResource} keys by name using a superordinate domain suffix. */
+ private Iterable> getNameserverRefsByLdhNameWithSuffix(
+ final RdapSearchPattern partialStringQuery, final DateTime now) {
+ // The suffix must be a domain that we manage. That way, we can look up the domain and search
+ // through the subordinate hosts. This is more efficient, and lets us permit wildcard searches
+ // with no initial string.
+ DomainResource domainResource =
+ loadByForeignKey(
+ DomainResource.class,
+ partialStringQuery.getSuffix(),
+ shouldIncludeDeleted() ? START_OF_TIME : now);
+ if (domainResource == null) {
+ // Don't allow wildcards with suffixes which are not domains we manage. That would risk a
+ // table scan in some easily foreseeable cases.
+ throw new UnprocessableEntityException(
+ "A suffix in a lookup by nameserver name must be a domain defined in the system");
+ }
+ Optional desiredRegistrar = getDesiredRegistrar();
+ ImmutableList.Builder> builder = new ImmutableList.Builder<>();
+ for (String fqhn : ImmutableSortedSet.copyOf(domainResource.getSubordinateHosts())) {
+ // We can't just check that the host name starts with the initial query string, because
+ // then the query ns.exam*.example.com would match against nameserver ns.example.com.
+ if (partialStringQuery.matches(fqhn)) {
+ if (desiredRegistrar.isPresent()) {
+ HostResource host =
+ loadByForeignKey(
+ HostResource.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : now);
+ if ((host != null)
+ && desiredRegistrar.get().equals(host.getPersistedCurrentSponsorClientId())) {
+ builder.add(Key.create(host));
+ }
+ } else {
+ Key hostKey =
+ loadAndGetKey(HostResource.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : now);
+ if (hostKey != null) {
+ builder.add(hostKey);
+ } else {
+ logger.warningfmt("Host key unexpectedly null");
}
}
- return builder.build();
- // If there's no suffix, query the host resources. Query the resources themselves, rather than
- // the foreign key indexes, because then we have an index on fully qualified host name and
- // deletion time, so we can check the deletion status in the query itself. There are no
- // pending deletes for hosts, so we can call queryUndeleted. In this case, the initial string
- // must be present, to avoid querying every host in the system. This restriction is enforced
- // by queryUndeleted().
- } else {
- // Only return the first 1000 nameservers. This could result in an incomplete result set if
- // a search asks for something like "ns*", but we need to enforce a limit in order to avoid
- // arbitrarily long-running queries.
- return queryItems(
- HostResource.class,
- "fullyQualifiedHostName",
- partialStringQuery,
- false, /* includeDeleted */
- MAX_NAMESERVERS_IN_FIRST_STAGE)
- .keys();
}
}
+ return builder.build();
}
/**
@@ -329,18 +382,24 @@ public class RdapDomainSearchAction extends RdapActionBase {
* IP. To avoid this, fetch only the first 1000 nameservers. In all normal circumstances, this
* should be orders of magnitude more than there actually are. But it could result in us missing
* some domains.
+ *
+ *