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. + * + *

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 searchByNameserverIp( final InetAddress inetAddress, final DateTime now) { - return searchByNameserverRefs( - ofy() - .load() - .type(HostResource.class) - .filter("inetAddresses", inetAddress.getHostAddress()) - .filter("deletionTime", END_OF_TIME) - .limit(MAX_NAMESERVERS_IN_FIRST_STAGE) - .keys(), - now); + Query query = + queryItems( + HostResource.class, + "inetAddresses", + inetAddress.getHostAddress(), + false, + MAX_NAMESERVERS_IN_FIRST_STAGE); + Optional desiredRegistrar = getDesiredRegistrar(); + if (desiredRegistrar.isPresent()) { + query = query.filter("currentSponsorClientId", desiredRegistrar.get()); + } + return searchByNameserverRefs(query.keys(), now); } /** @@ -360,12 +419,14 @@ public class RdapDomainSearchAction extends RdapActionBase { int numHostKeysSearched = 0; for (List> chunk : Iterables.partition(hostKeys, 30)) { numHostKeysSearched += chunk.size(); - for (DomainResource domain : ofy().load() + Query query = ofy().load() .type(DomainResource.class) - .filter("nsHosts in", chunk) - .filter("deletionTime >", now) - .limit(rdapResultSetMaxSize + 1)) { - if (!domains.contains(domain)) { + .filter("nsHosts in", chunk); + if (!shouldIncludeDeleted()) { + query = query.filter("deletionTime >", now); + } + for (DomainResource domain : query.limit(rdapResultSetMaxSize + 1)) { + if (!domains.contains(domain) && isAuthorized(domain, now)) { if (domains.size() >= rdapResultSetMaxSize) { return makeSearchResults( ImmutableList.copyOf(domains), IncompletenessWarningType.TRUNCATED, now); @@ -387,6 +448,17 @@ public class RdapDomainSearchAction extends RdapActionBase { return makeSearchResults(domains, IncompletenessWarningType.NONE, now); } + /** Output JSON from data in an {@link RdapResourcesAndIncompletenessWarningType} object. */ + private RdapSearchResults makeSearchResults( + RdapResourcesAndIncompletenessWarningType + resourcesAndIncompletenessWarningType, + DateTime now) { + return makeSearchResults( + resourcesAndIncompletenessWarningType.resources(), + resourcesAndIncompletenessWarningType.incompletenessWarningType(), + now); + } + /** * Output JSON for a list of domains. * @@ -401,12 +473,19 @@ public class RdapDomainSearchAction extends RdapActionBase { OutputDataType outputDataType = (domains.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; RdapAuthorization authorization = getAuthorization(); - ImmutableList.Builder> jsonBuilder = new ImmutableList.Builder<>(); + List> jsonList = new ArrayList<>(); for (DomainResource domain : domains) { - jsonBuilder.add( + jsonList.add( rdapJsonFormatter.makeRdapJsonForDomain( domain, false, rdapLinkBase, rdapWhoisServer, now, outputDataType, authorization)); + if (jsonList.size() >= rdapResultSetMaxSize) { + break; + } } - return RdapSearchResults.create(jsonBuilder.build(), incompletenessWarningType); + return RdapSearchResults.create( + ImmutableList.copyOf(jsonList), + (jsonList.size() < domains.size()) + ? IncompletenessWarningType.TRUNCATED + : incompletenessWarningType); } } diff --git a/java/google/registry/rdap/RdapEntitySearchAction.java b/java/google/registry/rdap/RdapEntitySearchAction.java index f293dab87..16e70e334 100644 --- a/java/google/registry/rdap/RdapEntitySearchAction.java +++ b/java/google/registry/rdap/RdapEntitySearchAction.java @@ -175,7 +175,8 @@ public class RdapEntitySearchAction extends RdapActionBase { shouldIncludeDeleted() ? (RESULT_SET_SIZE_SCALING_FACTOR * (rdapResultSetMaxSize + 1)) : (rdapResultSetMaxSize + 1)); - return makeSearchResults(getMatchingResources(query, now), registrars, now); + return makeSearchResults( + getMatchingResources(query, shouldIncludeDeleted(), now), registrars, now); } /** @@ -226,7 +227,8 @@ public class RdapEntitySearchAction extends RdapActionBase { shouldIncludeDeleted() ? (RESULT_SET_SIZE_SCALING_FACTOR * (rdapResultSetMaxSize + 1)) : (rdapResultSetMaxSize + 1)); - return makeSearchResults(getMatchingResources(query, now), registrars, now); + return makeSearchResults( + getMatchingResources(query, shouldIncludeDeleted(), now), registrars, now); } } diff --git a/java/google/registry/rdap/RdapNameserverSearchAction.java b/java/google/registry/rdap/RdapNameserverSearchAction.java index ea26679bc..4611e31da 100644 --- a/java/google/registry/rdap/RdapNameserverSearchAction.java +++ b/java/google/registry/rdap/RdapNameserverSearchAction.java @@ -219,7 +219,7 @@ public class RdapNameserverSearchAction extends RdapActionBase { shouldIncludeDeleted() ? (RESULT_SET_SIZE_SCALING_FACTOR * (rdapResultSetMaxSize + 1)) : (rdapResultSetMaxSize + 1)); - return makeSearchResults(getMatchingResources(query, now), now); + return makeSearchResults(getMatchingResources(query, shouldIncludeDeleted(), now), now); } /** Searches for nameservers by IP address, returning a JSON array of nameserver info maps. */ @@ -234,7 +234,7 @@ public class RdapNameserverSearchAction extends RdapActionBase { shouldIncludeDeleted() ? (RESULT_SET_SIZE_SCALING_FACTOR * (rdapResultSetMaxSize + 1)) : (rdapResultSetMaxSize + 1)); - return makeSearchResults(getMatchingResources(query, now), now); + return makeSearchResults(getMatchingResources(query, shouldIncludeDeleted(), now), now); } /** diff --git a/javatests/google/registry/rdap/RdapDomainActionTest.java b/javatests/google/registry/rdap/RdapDomainActionTest.java index 822b01792..6bb26161a 100644 --- a/javatests/google/registry/rdap/RdapDomainActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainActionTest.java @@ -50,6 +50,7 @@ import google.registry.testing.InjectRule; import google.registry.ui.server.registrar.SessionUtils; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; @@ -80,6 +81,7 @@ public class RdapDomainActionTest { private final SessionUtils sessionUtils = mock(SessionUtils.class); private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); + private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private RdapDomainAction action; @@ -121,12 +123,29 @@ public class RdapDomainActionTest { registrantLol, adminContactLol, techContactLol, host1, host2, registrarLol)); // deleted domain in lol + HostResource hostDodo2 = makeAndPersistHostResource( + "ns2.dodo.lol", "bad:f00d:cafe:0:0:0:15:beef", clock.nowUtc().minusYears(2)); DomainBase domainDeleted = persistResource(makeDomainResource("dodo.lol", - registrantLol, - adminContactLol, - techContactLol, + makeAndPersistContactResource( + "5372808-ERL", + "Goblin Market", + "lol@cat.lol", + clock.nowUtc().minusYears(1), + registrarLol), + makeAndPersistContactResource( + "5372808-IRL", + "Santa Claus", + "BOFH@cat.lol", + clock.nowUtc().minusYears(2), + registrarLol), + makeAndPersistContactResource( + "5372808-TRL", + "The Raven", + "bog@cat.lol", + clock.nowUtc().minusYears(3), + registrarLol), host1, - host2, + hostDodo2, registrarLol).asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build()); // xn--q9jyb4c @@ -211,7 +230,7 @@ public class RdapDomainActionTest { HistoryEntry.Type.DOMAIN_CREATE, Period.create(1, Period.Unit.YEARS), "created", - clock.nowUtc())); + clock.nowUtc().minusYears(1))); persistResource( makeHistoryEntry( domainCatIdn, @@ -226,18 +245,36 @@ public class RdapDomainActionTest { Period.create(1, Period.Unit.YEARS), "created", clock.nowUtc())); + persistResource( + makeHistoryEntry( + domainDeleted, + HistoryEntry.Type.DOMAIN_DELETE, + Period.create(1, Period.Unit.YEARS), + "deleted", + clock.nowUtc().minusMonths(6))); action = new RdapDomainAction(); action.clock = clock; action.request = request; action.response = response; + action.registrarParam = Optional.empty(); + action.includeDeletedParam = Optional.empty(); action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapLinkBase = "https://example.com/rdap/"; action.rdapWhoisServer = null; action.sessionUtils = sessionUtils; action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); + } + + private void login(String clientId) { when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("evilregistrar"); + when(sessionUtils.getRegistrarClientId(request)).thenReturn(clientId); + } + + private void loginAsAdmin() { + when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); + when(sessionUtils.getRegistrarClientId(request)).thenReturn("irrelevant"); + action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); } private Object generateActualJson(String domainName) { @@ -251,7 +288,7 @@ public class RdapDomainActionTest { String punycodeName, String handle, String expectedOutputFile) { - return generateExpectedJson(name, punycodeName, handle, null, expectedOutputFile); + return generateExpectedJson(name, punycodeName, handle, null, null, null, expectedOutputFile); } private Object generateExpectedJson( @@ -259,25 +296,40 @@ public class RdapDomainActionTest { String punycodeName, String handle, @Nullable List contactRoids, + @Nullable List nameserverRoids, + @Nullable List nameserverNames, String expectedOutputFile) { ImmutableMap.Builder substitutionsBuilder = new ImmutableMap.Builder<>(); substitutionsBuilder.put("NAME", name); substitutionsBuilder.put("PUNYCODENAME", (punycodeName == null) ? name : punycodeName); substitutionsBuilder.put("HANDLE", handle); substitutionsBuilder.put("TYPE", "domain name"); - substitutionsBuilder.put("NAMESERVER1ROID", "8-ROID"); - substitutionsBuilder.put("NAMESERVER1NAME", "ns1.cat.lol"); - substitutionsBuilder.put("NAMESERVER1PUNYCODENAME", "ns1.cat.lol"); substitutionsBuilder.put("NAMESERVER1ADDRESS", "1.2.3.4"); - substitutionsBuilder.put("NAMESERVER2ROID", "A-ROID"); - substitutionsBuilder.put("NAMESERVER2NAME", "ns2.cat.lol"); - substitutionsBuilder.put("NAMESERVER2PUNYCODENAME", "ns2.cat.lol"); substitutionsBuilder.put("NAMESERVER2ADDRESS", "bad:f00d:cafe::15:beef"); if (contactRoids != null) { for (int i = 0; i < contactRoids.size(); i++) { substitutionsBuilder.put("CONTACT" + (i + 1) + "ROID", contactRoids.get(i)); } } + if (nameserverRoids != null) { + for (int i = 0; i < nameserverRoids.size(); i++) { + substitutionsBuilder.put("NAMESERVER" + (i + 1) + "ROID", nameserverRoids.get(i)); + } + } else { + substitutionsBuilder.put("NAMESERVER1ROID", "8-ROID"); + substitutionsBuilder.put("NAMESERVER2ROID", "A-ROID"); + } + if (nameserverNames != null) { + for (int i = 0; i < nameserverRoids.size(); i++) { + substitutionsBuilder.put("NAMESERVER" + (i + 1) + "NAME", nameserverNames.get(i)); + substitutionsBuilder.put("NAMESERVER" + (i + 1) + "PUNYCODENAME", nameserverNames.get(i)); + } + } else { + substitutionsBuilder.put("NAMESERVER1NAME", "ns1.cat.lol"); + substitutionsBuilder.put("NAMESERVER1PUNYCODENAME", "ns1.cat.lol"); + substitutionsBuilder.put("NAMESERVER2NAME", "ns2.cat.lol"); + substitutionsBuilder.put("NAMESERVER2PUNYCODENAME", "ns2.cat.lol"); + } return JSONValue.parse( loadFileWithSubstitutions( this.getClass(), expectedOutputFile, substitutionsBuilder.build())); @@ -288,9 +340,28 @@ public class RdapDomainActionTest { String punycodeName, String handle, @Nullable List contactRoids, + @Nullable List nameserverRoids, String expectedOutputFile) { - Object obj = generateExpectedJson( - name, punycodeName, handle, contactRoids, expectedOutputFile); + return generateExpectedJsonWithTopLevelEntries( + name, punycodeName, handle, contactRoids, nameserverRoids, null, expectedOutputFile); + } + private Object generateExpectedJsonWithTopLevelEntries( + String name, + String punycodeName, + String handle, + @Nullable List contactRoids, + @Nullable List nameserverRoids, + @Nullable List nameserverNames, + String expectedOutputFile) { + Object obj = + generateExpectedJson( + name, + punycodeName, + handle, + contactRoids, + nameserverRoids, + nameserverNames, + expectedOutputFile); if (obj instanceof Map) { @SuppressWarnings("unchecked") Map map = (Map) obj; @@ -331,6 +402,7 @@ public class RdapDomainActionTest { expectedOutputFile.equals("rdap_domain.json") ? ImmutableList.of("4-ROID", "6-ROID", "2-ROID") : null, + ImmutableList.of("8-ROID", "A-ROID"), expectedOutputFile)); assertThat(response.getStatus()).isEqualTo(200); } @@ -361,107 +433,114 @@ public class RdapDomainActionTest { assertThat(response.getStatus()).isEqualTo(400); } - @Test - public void testDeletedDomain_returns404() throws Exception { - assertJsonEqual( - generateActualJson("dodo.lol"), - generateExpectedJson("dodo.lol not found", null, "1", "rdap_error_404.json")); - assertThat(response.getStatus()).isEqualTo(404); - } - @Test public void testValidDomain_works() throws Exception { + login("evilregistrar"); assertProperResponseForCatLol("cat.lol", "rdap_domain.json"); } + @Test + public void testValidDomain_works_sameRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("evilregistrar"); + login("evilregistrar"); + assertProperResponseForCatLol("cat.lol", "rdap_domain.json"); + } + + @Test + public void testValidDomain_notFound_differentRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("idnregistrar"); + generateActualJson("cat.lol"); + assertThat(response.getStatus()).isEqualTo(404); + } + @Test public void testValidDomain_asAdministrator_works() throws Exception { - UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); - action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(false); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("noregistrar"); + loginAsAdmin(); assertProperResponseForCatLol("cat.lol", "rdap_domain.json"); } @Test public void testValidDomain_notLoggedIn_noContacts() throws Exception { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(false); assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts.json"); } @Test public void testValidDomain_loggedInAsOtherRegistrar_noContacts() throws Exception { - when(sessionUtils.getRegistrarClientId(request)).thenReturn("otherregistrar"); + login("idnregistrar"); assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts.json"); } @Test public void testUpperCase_ignored() throws Exception { - assertProperResponseForCatLol("CaT.lOl", "rdap_domain.json"); + assertProperResponseForCatLol("CaT.lOl", "rdap_domain_no_contacts.json"); } @Test public void testTrailingDot_ignored() throws Exception { - assertProperResponseForCatLol("cat.lol.", "rdap_domain.json"); + assertProperResponseForCatLol("cat.lol.", "rdap_domain_no_contacts.json"); } @Test public void testQueryParameter_ignored() throws Exception { - assertProperResponseForCatLol("cat.lol?key=value", "rdap_domain.json"); + assertProperResponseForCatLol("cat.lol?key=value", "rdap_domain_no_contacts.json"); } @Test public void testIdnDomain_works() throws Exception { - when(sessionUtils.getRegistrarClientId(request)).thenReturn("idnregistrar"); + login("idnregistrar"); assertJsonEqual( generateActualJson("cat.みんな"), generateExpectedJsonWithTopLevelEntries( "cat.みんな", "cat.xn--q9jyb4c", - "15-Q9JYB4C", - ImmutableList.of("11-ROID", "13-ROID", "F-ROID"), + "1D-Q9JYB4C", + ImmutableList.of("19-ROID", "1B-ROID", "17-ROID"), + ImmutableList.of("8-ROID", "A-ROID"), "rdap_domain_unicode.json")); assertThat(response.getStatus()).isEqualTo(200); } @Test public void testIdnDomainWithPercentEncoding_works() throws Exception { - when(sessionUtils.getRegistrarClientId(request)).thenReturn("idnregistrar"); + login("idnregistrar"); assertJsonEqual( generateActualJson("cat.%E3%81%BF%E3%82%93%E3%81%AA"), generateExpectedJsonWithTopLevelEntries( "cat.みんな", "cat.xn--q9jyb4c", - "15-Q9JYB4C", - ImmutableList.of("11-ROID", "13-ROID", "F-ROID"), + "1D-Q9JYB4C", + ImmutableList.of("19-ROID", "1B-ROID", "17-ROID"), + ImmutableList.of("8-ROID", "A-ROID"), "rdap_domain_unicode.json")); assertThat(response.getStatus()).isEqualTo(200); } @Test public void testPunycodeDomain_works() throws Exception { - when(sessionUtils.getRegistrarClientId(request)).thenReturn("idnregistrar"); + login("idnregistrar"); assertJsonEqual( generateActualJson("cat.xn--q9jyb4c"), generateExpectedJsonWithTopLevelEntries( "cat.みんな", "cat.xn--q9jyb4c", - "15-Q9JYB4C", - ImmutableList.of("11-ROID", "13-ROID", "F-ROID"), + "1D-Q9JYB4C", + ImmutableList.of("19-ROID", "1B-ROID", "17-ROID"), + ImmutableList.of("8-ROID", "A-ROID"), "rdap_domain_unicode.json")); assertThat(response.getStatus()).isEqualTo(200); } @Test public void testMultilevelDomain_works() throws Exception { - when(sessionUtils.getRegistrarClientId(request)).thenReturn("1tldregistrar"); + login("1tldregistrar"); assertJsonEqual( generateActualJson("cat.1.tld"), generateExpectedJsonWithTopLevelEntries( "cat.1.tld", null, - "1D-1_TLD", - ImmutableList.of("19-ROID", "1B-ROID", "17-ROID"), + "25-1_TLD", + ImmutableList.of("21-ROID", "23-ROID", "1F-ROID"), + ImmutableList.of("8-ROID", "A-ROID"), "rdap_domain.json")); assertThat(response.getStatus()).isEqualTo(200); } @@ -474,4 +553,68 @@ public class RdapDomainActionTest { generateActualJson("cat.lol"); assertThat(response.getStatus()).isEqualTo(404); } + + @Test + public void testDeletedDomain_notFound() throws Exception { + assertJsonEqual( + generateActualJson("dodo.lol"), + generateExpectedJson("dodo.lol not found", null, "1", "rdap_error_404.json")); + assertThat(response.getStatus()).isEqualTo(404); + } + + @Test + public void testDeletedDomain_notFound_includeDeletedSetFalse() throws Exception { + action.includeDeletedParam = Optional.of(true); + generateActualJson("dodo.lol"); + assertThat(response.getStatus()).isEqualTo(404); + } + + @Test + public void testDeletedDomain_notFound_notLoggedIn() throws Exception { + action.includeDeletedParam = Optional.of(true); + generateActualJson("dodo.lol"); + assertThat(response.getStatus()).isEqualTo(404); + } + + @Test + public void testDeletedDomain_notFound_loggedInAsDifferentRegistrar() throws Exception { + login("1tldregistrar"); + action.includeDeletedParam = Optional.of(true); + generateActualJson("dodo.lol"); + assertThat(response.getStatus()).isEqualTo(404); + } + + @Test + public void testDeletedDomain_works_loggedInAsCorrectRegistrar() throws Exception { + login("evilregistrar"); + action.includeDeletedParam = Optional.of(true); + assertJsonEqual( + generateActualJson("dodo.lol"), + generateExpectedJsonWithTopLevelEntries( + "dodo.lol", + null, + "15-LOL", + ImmutableList.of("11-ROID", "13-ROID", "F-ROID"), + ImmutableList.of("8-ROID", "D-ROID"), + ImmutableList.of("ns1.cat.lol", "ns2.dodo.lol"), + "rdap_domain_deleted.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testDeletedDomain_works_loggedInAsAdmin() throws Exception { + loginAsAdmin(); + action.includeDeletedParam = Optional.of(true); + assertJsonEqual( + generateActualJson("dodo.lol"), + generateExpectedJsonWithTopLevelEntries( + "dodo.lol", + null, + "15-LOL", + ImmutableList.of("11-ROID", "13-ROID", "F-ROID"), + ImmutableList.of("8-ROID", "D-ROID"), + ImmutableList.of("ns1.cat.lol", "ns2.dodo.lol"), + "rdap_domain_deleted.json")); + assertThat(response.getStatus()).isEqualTo(200); + } } diff --git a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java index 7fb48cf15..faf0be612 100644 --- a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java @@ -89,7 +89,8 @@ public class RdapDomainSearchActionTest { private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); private final SessionUtils sessionUtils = mock(SessionUtils.class); private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); - UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); + private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); + private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapDomainSearchAction action = new RdapDomainSearchAction(); @@ -104,6 +105,7 @@ public class RdapDomainSearchActionTest { private ContactResource contact3; private HostResource hostNs1CatLol; private HostResource hostNs2CatLol; + private HistoryEntry historyEntryCatLolCreate; private Map hostNameToHostMap = new HashMap<>(); enum RequestType { NONE, NAME, NS_LDH_NAME, NS_IP } @@ -246,7 +248,7 @@ public class RdapDomainSearchActionTest { registrar), hostNs1CatLol, addHostToMap(makeAndPersistHostResource( - "ns2.external.tld", "bad:f00d:cafe:0:0:0:15:beef", clock.nowUtc().minusYears(2))), + "ns2.external.tld", "bad:f00d:cafe:0:0:0:16:beef", clock.nowUtc().minusYears(2))), registrar) .asBuilder() .setCreationTimeForTest(clock.nowUtc().minusYears(3)) @@ -287,7 +289,7 @@ public class RdapDomainSearchActionTest { // cat.1.test createTld("1.test"); registrar = - persistResource(makeRegistrar("unicoderegistrar", "1.test", Registrar.State.ACTIVE)); + persistResource(makeRegistrar("multiregistrar", "1.test", Registrar.State.ACTIVE)); persistSimpleResources(makeRegistrarContacts(registrar)); domainMultipart = persistResource(makeDomainResource( "cat.1.test", @@ -319,8 +321,10 @@ public class RdapDomainSearchActionTest { .setCreationTimeForTest(clock.nowUtc().minusYears(3)) .build()); + persistResource(makeRegistrar("otherregistrar", "other", Registrar.State.ACTIVE)); + // history entries - persistResource( + historyEntryCatLolCreate = persistResource( makeHistoryEntry( domainCatLol, HistoryEntry.Type.DOMAIN_CREATE, @@ -366,8 +370,17 @@ public class RdapDomainSearchActionTest { action.rdapWhoisServer = null; action.sessionUtils = sessionUtils; action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); + } + + private void login(String clientId) { when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("evilregistrar"); + when(sessionUtils.getRegistrarClientId(request)).thenReturn(clientId); + } + + private void loginAsAdmin() { + when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); + when(sessionUtils.getRegistrarClientId(request)).thenReturn("irrelevant"); + action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); } private Object generateExpectedJsonForTwoDomains() { @@ -477,6 +490,98 @@ public class RdapDomainSearchActionTest { return new JSONObject(builder.build()); } + private void deleteCatLol() { + persistResource( + domainCatLol + .asBuilder() + .setCreationTimeForTest(clock.nowUtc().minusYears(1)) + .setDeletionTime(clock.nowUtc().minusMonths(6)) + .build()); + persistResource( + historyEntryCatLolCreate + .asBuilder() + .setModificationTime(clock.nowUtc().minusYears(1)) + .build()); + persistResource( + makeHistoryEntry( + domainCatLol, + HistoryEntry.Type.DOMAIN_DELETE, + Period.create(1, Period.Unit.YEARS), + "deleted", + clock.nowUtc().minusMonths(6))); + } + + private void createManyDomainsAndHosts( + int numActiveDomains, int numTotalDomainsPerActiveDomain, int numHosts) { + ImmutableSet.Builder> hostKeysBuilder = new ImmutableSet.Builder<>(); + ImmutableSet.Builder subordinateHostnamesBuilder = new ImmutableSet.Builder<>(); + String mainDomainName = String.format("domain%d.lol", numTotalDomainsPerActiveDomain); + for (int i = 1; i <= numHosts; i++) { + String hostName = String.format("ns%d.%s", i, mainDomainName); + subordinateHostnamesBuilder.add(hostName); + HostResource host = makeAndPersistHostResource( + hostName, String.format("5.5.%d.%d", 5 + i / 250, i % 250), clock.nowUtc().minusYears(1)); + hostKeysBuilder.add(Key.create(host)); + } + ImmutableSet> hostKeys = hostKeysBuilder.build(); + // Create all the domains at once, then persist them in parallel, for increased efficiency. + ImmutableList.Builder domainsBuilder = new ImmutableList.Builder<>(); + for (int i = 1; i <= numActiveDomains * numTotalDomainsPerActiveDomain; i++) { + String domainName = String.format("domain%d.lol", i); + DomainResource.Builder builder = + makeDomainResource( + domainName, contact1, contact2, contact3, null, null, registrar) + .asBuilder() + .setNameservers(hostKeys) + .setCreationTimeForTest(clock.nowUtc().minusYears(3)); + if (domainName.equals(mainDomainName)) { + builder.setSubordinateHosts(subordinateHostnamesBuilder.build()); + } + if (i % numTotalDomainsPerActiveDomain != 0) { + builder = builder.setDeletionTime(clock.nowUtc().minusDays(1)); + } + domainsBuilder.add(builder.build()); + } + persistResources(domainsBuilder.build()); + } + + private Object readMultiDomainFile( + String fileName, + String domainName1, + String domainHandle1, + String domainName2, + String domainHandle2, + String domainName3, + String domainHandle3, + String domainName4, + String domainHandle4) { + return JSONValue.parse(loadFileWithSubstitutions( + this.getClass(), + fileName, + new ImmutableMap.Builder() + .put("DOMAINNAME1", domainName1) + .put("DOMAINHANDLE1", domainHandle1) + .put("DOMAINNAME2", domainName2) + .put("DOMAINHANDLE2", domainHandle2) + .put("DOMAINNAME3", domainName3) + .put("DOMAINHANDLE3", domainHandle3) + .put("DOMAINNAME4", domainName4) + .put("DOMAINHANDLE4", domainHandle4) + .build())); + } + + private void checkNumberOfDomainsInResult(Object obj, int expected) { + assertThat(obj).isInstanceOf(Map.class); + + @SuppressWarnings("unchecked") + Map map = (Map) obj; + + @SuppressWarnings("unchecked") + List domains = (List) map.get("domainSearchResults"); + + assertThat(domains).hasSize(expected); + } + private void runSuccessfulTestWithCatLol( RequestType requestType, String queryString, String fileName) { runSuccessfulTest( @@ -582,26 +687,38 @@ public class RdapDomainSearchActionTest { @Test public void testDomainMatch_found() throws Exception { + login("evilregistrar"); runSuccessfulTestWithCatLol(RequestType.NAME, "cat.lol", "rdap_domain.json"); } @Test public void testDomainMatch_foundWithUpperCase() throws Exception { + login("evilregistrar"); runSuccessfulTestWithCatLol(RequestType.NAME, "CaT.lOl", "rdap_domain.json"); } + @Test + public void testDomainMatch_found_sameRegistrarRequested() throws Exception { + login("evilregistrar"); + action.registrarParam = Optional.of("evilregistrar"); + runSuccessfulTestWithCatLol(RequestType.NAME, "cat.lol", "rdap_domain.json"); + } + + @Test + public void testDomainMatch_notFound_differentRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("otherregistrar"); + runNotFoundTest(RequestType.NAME, "cat.lol", "No domains found"); + } + @Test public void testDomainMatch_found_asAdministrator() throws Exception { - UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); - action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(false); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("noregistrar"); + loginAsAdmin(); runSuccessfulTestWithCatLol(RequestType.NAME, "cat.lol", "rdap_domain.json"); } @Test public void testDomainMatch_found_loggedInAsOtherRegistrar() throws Exception { - when(sessionUtils.getRegistrarClientId(request)).thenReturn("otherregistrar"); + login("otherregistrar"); runSuccessfulTestWithCatLol( RequestType.NAME, "cat.lol", "rdap_domain_no_contacts_with_remark.json"); } @@ -618,11 +735,13 @@ public class RdapDomainSearchActionTest { @Test public void testDomainMatch_cat2_lol_found() throws Exception { + login("evilregistrar"); runSuccessfulTestWithCat2Lol(RequestType.NAME, "cat2.lol", "rdap_domain_cat2.json"); } @Test public void testDomainMatch_cat_example_found() throws Exception { + login("evilregistrar"); runSuccessfulTest( RequestType.NAME, "cat.example", @@ -705,6 +824,11 @@ public class RdapDomainSearchActionTest { assertThat(response.getStatus()).isEqualTo(200); } + @Test + public void testDomainMatch_qstar_lol_notFound() throws Exception { + runNotFoundTest(RequestType.NAME, "q*.lol", "No domains found"); + } + @Test public void testDomainMatch_star_lol_found() throws Exception { assertThat(generateActualJson(RequestType.NAME, "*.lol")) @@ -712,6 +836,20 @@ public class RdapDomainSearchActionTest { assertThat(response.getStatus()).isEqualTo(200); } + @Test + public void testDomainMatch_star_lol_found_sameRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("evilregistrar"); + assertThat(generateActualJson(RequestType.NAME, "*.lol")) + .isEqualTo(generateExpectedJsonForTwoDomains("cat2.lol", "17-LOL", "cat.lol", "C-LOL")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testDomainMatch_star_lol_notFound_differentRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("otherregistrar"); + runNotFoundTest(RequestType.NAME, "*.lol", "No domains found"); + } + @Test public void testDomainMatch_cat_star_found() throws Exception { assertThat(generateActualJson(RequestType.NAME, "cat.*")) @@ -725,8 +863,22 @@ public class RdapDomainSearchActionTest { assertThat(response.getStatus()).isEqualTo(200); } + @Test + public void testDomainMatch_cat_star_foundOne_sameRegistrarRequested() throws Exception { + login("evilregistrar"); + action.registrarParam = Optional.of("evilregistrar"); + runSuccessfulTestWithCatLol(RequestType.NAME, "cat.*", "rdap_domain.json"); + } + + @Test + public void testDomainMatch_cat_star_notFound_differentRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("otherregistrar"); + runNotFoundTest(RequestType.NAME, "cat.*", "No domains found"); + } + @Test public void testDomainMatch_cat_lstar_found() throws Exception { + login("evilregistrar"); runSuccessfulTestWithCatLol(RequestType.NAME, "cat.l*", "rdap_domain.json"); } @@ -763,6 +915,37 @@ public class RdapDomainSearchActionTest { runNotFoundTest(RequestType.NAME, "cat.lol", "No domains found"); } + @Test + public void testDomainMatchDeletedDomain_notFound_deletedNotRequested() throws Exception { + login("evilregistrar"); + persistDomainAsDeleted(domainCatLol, clock.nowUtc().minusDays(1)); + runNotFoundTest(RequestType.NAME, "cat.lol", "No domains found"); + } + + @Test + public void testDomainMatchDeletedDomain_found_loggedInAsSameRegistrar() throws Exception { + login("evilregistrar"); + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runSuccessfulTestWithCatLol(RequestType.NAME, "cat.lol", "rdap_domain_deleted.json"); + } + + @Test + public void testDomainMatchDeletedDomain_notFound_loggedInAsOtherRegistrar() throws Exception { + login("otherregistrar"); + action.includeDeletedParam = Optional.of(true); + persistDomainAsDeleted(domainCatLol, clock.nowUtc().minusDays(1)); + runNotFoundTest(RequestType.NAME, "cat.lol", "No domains found"); + } + + @Test + public void testDomainMatchDeletedDomain_found_loggedInAsAdmin() throws Exception { + loginAsAdmin(); + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runSuccessfulTestWithCatLol(RequestType.NAME, "cat.lol", "rdap_domain_deleted.json"); + } + @Test public void testDomainMatchDeletedDomainWithWildcard_notFound() throws Exception { persistDomainAsDeleted(domainCatLol, clock.nowUtc().minusDays(1)); @@ -784,77 +967,6 @@ public class RdapDomainSearchActionTest { runNotFoundTest(RequestType.NAME, "cat.lol", "No domains found"); } - private void createManyDomainsAndHosts( - int numActiveDomains, int numTotalDomainsPerActiveDomain, int numHosts) { - ImmutableSet.Builder> hostKeysBuilder = new ImmutableSet.Builder<>(); - ImmutableSet.Builder subordinateHostsBuilder = new ImmutableSet.Builder<>(); - String mainDomainName = String.format("domain%d.lol", numTotalDomainsPerActiveDomain); - for (int i = 1; i <= numHosts; i++) { - String hostName = String.format("ns%d.%s", i, mainDomainName); - subordinateHostsBuilder.add(hostName); - HostResource host = makeAndPersistHostResource( - hostName, String.format("5.5.%d.%d", 5 + i / 250, i % 250), clock.nowUtc().minusYears(1)); - hostKeysBuilder.add(Key.create(host)); - } - ImmutableSet> hostKeys = hostKeysBuilder.build(); - // Create all the domains at once, then persist them in parallel, for increased efficiency. - ImmutableList.Builder domainsBuilder = new ImmutableList.Builder<>(); - for (int i = 1; i <= numActiveDomains * numTotalDomainsPerActiveDomain; i++) { - String domainName = String.format("domain%d.lol", i); - DomainResource.Builder builder = - makeDomainResource( - domainName, contact1, contact2, contact3, null, null, registrar) - .asBuilder() - .setNameservers(hostKeys) - .setCreationTimeForTest(clock.nowUtc().minusYears(3)); - if (domainName.equals(mainDomainName)) { - builder.setSubordinateHosts(subordinateHostsBuilder.build()); - } - if (i % numTotalDomainsPerActiveDomain != 0) { - builder = builder.setDeletionTime(clock.nowUtc().minusDays(1)); - } - domainsBuilder.add(builder.build()); - } - persistResources(domainsBuilder.build()); - } - - private Object readMultiDomainFile( - String fileName, - String domainName1, - String domainHandle1, - String domainName2, - String domainHandle2, - String domainName3, - String domainHandle3, - String domainName4, - String domainHandle4) { - return JSONValue.parse(loadFileWithSubstitutions( - this.getClass(), - fileName, - new ImmutableMap.Builder() - .put("DOMAINNAME1", domainName1) - .put("DOMAINHANDLE1", domainHandle1) - .put("DOMAINNAME2", domainName2) - .put("DOMAINHANDLE2", domainHandle2) - .put("DOMAINNAME3", domainName3) - .put("DOMAINHANDLE3", domainHandle3) - .put("DOMAINNAME4", domainName4) - .put("DOMAINHANDLE4", domainHandle4) - .build())); - } - - private void checkNumberOfDomainsInResult(Object obj, int expected) { - assertThat(obj).isInstanceOf(Map.class); - - @SuppressWarnings("unchecked") - Map map = (Map) obj; - - @SuppressWarnings("unchecked") - List domains = (List) map.get("domainSearchResults"); - - assertThat(domains).hasSize(expected); - } - @Test public void testDomainMatch_manyDeletedDomains_fullResultSet() throws Exception { // There are enough domains to fill a full result set; deleted domains are ignored. @@ -972,25 +1084,54 @@ public class RdapDomainSearchActionTest { assertThat(response.getStatus()).isEqualTo(200); } + @Test + public void testNameserverMatch_foundMultiple_sameRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("TheRegistrar"); + assertThat(generateActualJson(RequestType.NS_LDH_NAME, "ns1.cat.lol")) + .isEqualTo(generateExpectedJsonForTwoDomains()); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testNameserverMatch_notFound_differentRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("otherregistrar"); + runNotFoundTest(RequestType.NS_LDH_NAME, "ns1.cat.lol", "No matching nameservers found"); + } + @Test public void testNameserverMatchWithWildcard_found() throws Exception { + login("evilregistrar"); runSuccessfulTestWithCatLol(RequestType.NS_LDH_NAME, "ns2.cat.l*", "rdap_domain.json"); } + @Test + public void testNameserverMatchWithWildcard_found_sameRegistrarRequested() throws Exception { + login("evilregistrar"); + action.registrarParam = Optional.of("TheRegistrar"); + runSuccessfulTestWithCatLol(RequestType.NS_LDH_NAME, "ns2.cat.l*", "rdap_domain.json"); + } + + @Test + public void testNameserverMatchWithWildcard_notFound_differentRegistrarRequested() + throws Exception { + action.registrarParam = Optional.of("otherregistrar"); + runNotFoundTest(RequestType.NS_LDH_NAME, "ns2.cat.l*", "No matching nameservers found"); + } + @Test public void testNameserverMatchWithWildcardAndDomainSuffix_notFound() throws Exception { runNotFoundTest(RequestType.NS_LDH_NAME, "ns5*.cat.lol", "No matching nameservers found"); } @Test - public void testNameserverMatchWithNoPrefixWildcardAndDomainSuffix_found() throws Exception { + public void testNameserverMatchWithNoPrefixAndDomainSuffix_found() throws Exception { assertThat(generateActualJson(RequestType.NS_LDH_NAME, "*.cat.lol")) .isEqualTo(generateExpectedJsonForTwoDomains()); assertThat(response.getStatus()).isEqualTo(200); } @Test - public void testNameserverMatchWithOneCharacterPrefixWildcardAndDomainSuffix_found() + public void testNameserverMatchWithOneCharacterPrefixAndDomainSuffix_found() throws Exception { assertThat(generateActualJson(RequestType.NS_LDH_NAME, "n*.cat.lol")) .isEqualTo(generateExpectedJsonForTwoDomains()); @@ -998,7 +1139,24 @@ public class RdapDomainSearchActionTest { } @Test - public void testNameserverMatchWithTwoCharacterPrefixWildcardAndDomainSuffix_found() + public void + testNameserverMatchWithOneCharacterPrefixAndDomainSuffix_found_sameRegistrarRequested() + throws Exception { + action.registrarParam = Optional.of("TheRegistrar"); + assertThat(generateActualJson(RequestType.NS_LDH_NAME, "n*.cat.lol")) + .isEqualTo(generateExpectedJsonForTwoDomains()); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testNameserverMatchWithPrefixAndDomainSuffix_notFound_differentRegistrarRequested() + throws Exception { + action.registrarParam = Optional.of("otherregistrar"); + runNotFoundTest(RequestType.NS_LDH_NAME, "n*.cat.lol", "No matching nameservers found"); + } + + @Test + public void testNameserverMatchWithTwoCharacterPrefixAndDomainSuffix_found() throws Exception { assertThat(generateActualJson(RequestType.NS_LDH_NAME, "ns*.cat.lol")) .isEqualTo(generateExpectedJsonForTwoDomains()); @@ -1019,11 +1177,13 @@ public class RdapDomainSearchActionTest { @Test public void testNameserverMatch_ns2_cat_lol_found() throws Exception { + login("evilregistrar"); runSuccessfulTestWithCatLol(RequestType.NS_LDH_NAME, "ns2.cat.lol", "rdap_domain.json"); } @Test public void testNameserverMatch_ns2_dog_lol_found() throws Exception { + login("evilregistrar"); runSuccessfulTestWithCat2Lol(RequestType.NS_LDH_NAME, "ns2.dog.lol", "rdap_domain_cat2.json"); } @@ -1092,8 +1252,41 @@ public class RdapDomainSearchActionTest { runNotFoundTest(RequestType.NS_LDH_NAME, "ns2.cat.lol", "No matching nameservers found"); } + @Test + public void testNameserverMatchDeletedDomain_notFound() throws Exception { + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runNotFoundTest(RequestType.NS_LDH_NAME, "ns2.cat.lol", "No domains found"); + } + + @Test + public void testNameserverMatchDeletedDomain_found_loggedInAsSameRegistrar() throws Exception { + login("evilregistrar"); + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runSuccessfulTestWithCatLol(RequestType.NS_LDH_NAME, "ns2.cat.lol", "rdap_domain_deleted.json"); + } + + @Test + public void testNameserverMatchDeletedDomain_notFound_loggedInAsOtherRegistrar() + throws Exception { + login("otherregistrar"); + action.includeDeletedParam = Optional.of(true); + persistDomainAsDeleted(domainCatLol, clock.nowUtc().minusDays(1)); + runNotFoundTest(RequestType.NS_LDH_NAME, "ns2.cat.lol", "No domains found"); + } + + @Test + public void testNameserverMatchDeletedDomain_found_loggedInAsAdmin() throws Exception { + loginAsAdmin(); + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runSuccessfulTestWithCatLol(RequestType.NS_LDH_NAME, "ns2.cat.lol", "rdap_domain_deleted.json"); + } + @Test public void testNameserverMatchOneDeletedDomain_foundTheOther() throws Exception { + login("evilregistrar"); persistDomainAsDeleted(domainCatExample, clock.nowUtc().minusDays(1)); runSuccessfulTestWithCatLol(RequestType.NS_LDH_NAME, "ns1.cat.lol", "rdap_domain.json"); } @@ -1254,12 +1447,27 @@ public class RdapDomainSearchActionTest { } @Test - public void testAddressMatchV6Address_foundMultiple() throws Exception { - assertThat(generateActualJson(RequestType.NS_IP, "bad:f00d:cafe:0:0:0:15:beef")) + public void testAddressMatchV4Address_foundMultiple_sameRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("TheRegistrar"); + assertThat(generateActualJson(RequestType.NS_IP, "1.2.3.4")) .isEqualTo(generateExpectedJsonForTwoDomains()); assertThat(response.getStatus()).isEqualTo(200); } + @Test + public void testAddressMatchV4Address_notFound_differentRegistrarRequested() throws Exception { + action.registrarParam = Optional.of("otherregistrar"); + runNotFoundTest(RequestType.NS_IP, "1.2.3.4", "No domains found"); + } + + @Test + public void testAddressMatchV6Address_foundOne() throws Exception { + runSuccessfulTestWithCatLol( + RequestType.NS_IP, + "bad:f00d:cafe:0:0:0:15:beef", + "rdap_domain_no_contacts_with_remark.json"); + } + @Test public void testAddressMatchLocalhost_notFound() throws Exception { runNotFoundTest(RequestType.NS_IP, "127.0.0.1", "No domains found"); @@ -1274,8 +1482,42 @@ public class RdapDomainSearchActionTest { runNotFoundTest(RequestType.NS_IP, "127.0.0.1", "No matching nameservers found"); } + @Test + public void testAddressMatchDeletedDomain_notFound() throws Exception { + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runNotFoundTest(RequestType.NS_IP, "bad:f00d:cafe:0:0:0:15:beef", "No domains found"); + } + + @Test + public void testAddressMatchDeletedDomain_found_loggedInAsSameRegistrar() throws Exception { + login("evilregistrar"); + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runSuccessfulTestWithCatLol( + RequestType.NS_IP, "bad:f00d:cafe:0:0:0:15:beef", "rdap_domain_deleted.json"); + } + + @Test + public void testAddressMatchDeletedDomain_notFound_loggedInAsOtherRegistrar() throws Exception { + login("otherregistrar"); + action.includeDeletedParam = Optional.of(true); + persistDomainAsDeleted(domainCatLol, clock.nowUtc().minusDays(1)); + runNotFoundTest(RequestType.NS_IP, "bad:f00d:cafe:0:0:0:15:beef", "No domains found"); + } + + @Test + public void testAddressMatchDeletedDomain_found_loggedInAsAdmin() throws Exception { + loginAsAdmin(); + action.includeDeletedParam = Optional.of(true); + deleteCatLol(); + runSuccessfulTestWithCatLol( + RequestType.NS_IP, "bad:f00d:cafe:0:0:0:15:beef", "rdap_domain_deleted.json"); + } + @Test public void testAddressMatchOneDeletedDomain_foundTheOther() throws Exception { + login("evilregistrar"); persistDomainAsDeleted(domainCatExample, clock.nowUtc().minusDays(1)); assertThat(generateActualJson(RequestType.NS_IP, "1.2.3.4")) .isEqualTo( diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json b/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json index f276b6b8a..bbb9d7f2f 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json @@ -75,8 +75,7 @@ }, { "status": [ - "active", - "associated" + "active" ], "handle": "%NAMESERVER2ROID%", "links": [ @@ -111,8 +110,7 @@ "entities": [ { "status": [ - "active", - "associated" + "active" ], "handle": "%CONTACT1ROID%", "roles": [ @@ -204,8 +202,7 @@ }, { "status": [ - "active", - "associated" + "active" ], "handle": "%CONTACT2ROID%", "roles": [ @@ -297,8 +294,7 @@ }, { "status": [ - "active", - "associated" + "active" ], "handle": "%CONTACT3ROID%", "roles": [