diff --git a/java/google/registry/model/registrar/Registrar.java b/java/google/registry/model/registrar/Registrar.java index 073e80302..b97431d23 100644 --- a/java/google/registry/model/registrar/Registrar.java +++ b/java/google/registry/model/registrar/Registrar.java @@ -286,6 +286,7 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable * * @see "http://www.iana.org/assignments/registrar-ids/registrar-ids.txt" */ + @Index Long ianaIdentifier; /** Identifier of registrar used in external billing system (e.g. Oracle). */ @@ -840,6 +841,29 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable }}); } + /** + * Load registrar entities by IANA identifier range outside of a transaction. + * + * @param ianaIdentifierStart returned registrars will have an IANA id greater than or equal to + * this + * @param ianaIdentifierAfterEnd returned registrars will have an IANA id less than this + * @param resultSetMaxSize the maximum number of registrar entities to be returned + */ + public static Iterable loadByIanaIdentifierRange( + final Long ianaIdentifierStart, + final Long ianaIdentifierAfterEnd, + final int resultSetMaxSize) { + return ofy().doTransactionless(new Work>() { + @Override + public Iterable run() { + return ofy().load() + .type(Registrar.class) + .filter("ianaIdentifier >=", ianaIdentifierStart) + .filter("ianaIdentifier <", ianaIdentifierAfterEnd) + .limit(resultSetMaxSize); + }}); + } + /** Loads all registrar entities. */ public static Iterable loadAll() { return ofy().load().type(Registrar.class).ancestor(getCrossTldKey()); diff --git a/java/google/registry/rdap/RdapEntityAction.java b/java/google/registry/rdap/RdapEntityAction.java index 87a0571ca..65fae9625 100644 --- a/java/google/registry/rdap/RdapEntityAction.java +++ b/java/google/registry/rdap/RdapEntityAction.java @@ -20,6 +20,7 @@ import static google.registry.request.Action.Method.HEAD; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.re2j.Pattern; import com.googlecode.objectify.Key; import google.registry.model.contact.ContactResource; @@ -34,7 +35,14 @@ import javax.inject.Inject; import org.joda.time.DateTime; /** - * RDAP (new WHOIS) action for entity (contact and registrar) requests. + * RDAP (new WHOIS) action for entity (contact and registrar) requests. the ICANN operational + * profile dictates that the "handle" for registrars is to be the IANA registrar ID: + * + *

2.8.3. Registries MUST support lookup for entities with the registrar role within other + * objects using the handle (as described in 3.1.5 of RFC7482). The handle of the entity with the + * registrar role MUST be equal to IANA Registrar ID. The entity with the registrar role in the RDAP + * response MUST contain a publicIDs member to identify the IANA Registrar ID from the IANA’s + * Registrar ID registry. The type value of the publicID object MUST be equal to IANA Registrar ID. */ @Action(path = RdapEntityAction.PATH, method = {GET, HEAD}, isPrefix = true) public class RdapEntityAction extends RdapActionBase { @@ -80,17 +88,23 @@ public class RdapEntityAction extends RdapActionBase { now); } } - String clientId = pathSearchString.trim(); - if ((clientId.length() >= 3) && (clientId.length() <= 16)) { + try { + Long ianaIdentifier = Long.parseLong(pathSearchString); wasValidKey = true; - Registrar registrar = Registrar.loadByClientId(clientId); + Registrar registrar = Iterables.getOnlyElement( + Registrar.loadByIanaIdentifierRange(ianaIdentifier, ianaIdentifier + 1, 1), null); if ((registrar != null) && registrar.isActiveAndPubliclyVisible()) { return RdapJsonFormatter.makeRdapJsonForRegistrar( registrar, true, rdapLinkBase, rdapWhoisServer, now); } + } catch (NumberFormatException e) { + // Although the search string was not a valid IANA identifier, it might still have been a + // valid ROID. } - throw !wasValidKey - ? new BadRequestException(pathSearchString + " is not a valid entity handle") - : new NotFoundException(pathSearchString + " not found"); + // At this point, we have failed to find either a contact or a registrar. + throw wasValidKey + ? new NotFoundException(pathSearchString + " not found") + : new BadRequestException(pathSearchString + " is not a valid entity handle"); } } + diff --git a/java/google/registry/rdap/RdapEntitySearchAction.java b/java/google/registry/rdap/RdapEntitySearchAction.java index c0e36a5d9..8a1608b0f 100644 --- a/java/google/registry/rdap/RdapEntitySearchAction.java +++ b/java/google/registry/rdap/RdapEntitySearchAction.java @@ -104,7 +104,7 @@ public class RdapEntitySearchAction extends RdapActionBase { /** * Searches for entities by name, returning a JSON array of entity info maps. - * + * *

As per Gustavo Lozano of ICANN, registrar name search should be by registrar name only, not * by registrar contact name: * @@ -112,7 +112,7 @@ public class RdapEntitySearchAction extends RdapActionBase { * in the Base Registry Agreement (see 1.6 of Section 4 of the Base Registry Agreement, * https://newgtlds.icann.org/sites/default/files/agreements/ * agreement-approved-09jan14-en.htm). - * + * *

According to RFC 7482 section 6.1, punycode is only used for domain name labels, so we can * assume that entity names are regular unicode. */ @@ -162,17 +162,18 @@ public class RdapEntitySearchAction extends RdapActionBase { .type(ContactResource.class) .id(partialStringQuery.getInitialString()) .now(); - Registrar registrar = Registrar.loadByClientId(partialStringQuery.getInitialString()); + ImmutableList registrars = getMatchingRegistrars(partialStringQuery); return makeSearchResults( ((contactResource == null) || !contactResource.getDeletionTime().isEqual(END_OF_TIME)) ? ImmutableList.of() : ImmutableList.of(contactResource), - (registrar == null) - ? ImmutableList.of() : ImmutableList.of(registrar), + registrars, now); // Handle queries with a wildcard, but no suffix. For contact resources, the deletion time will // always be END_OF_TIME for non-deleted records; unlike domain resources, we don't need to // worry about deletion times in the future. That allows us to use an equality query for the - // deletion time. + // deletion time. Because the handle for registrars is the IANA identifier number, don't allow + // wildcard searches for registrars, by simply not searching for registrars if a wildcard is + // present. } else if (partialStringQuery.getSuffix() == null) { return makeSearchResults( ofy().load() @@ -183,10 +184,7 @@ public class RdapEntitySearchAction extends RdapActionBase { "<", Key.create(ContactResource.class, partialStringQuery.getNextInitialString())) .filter("deletionTime", END_OF_TIME) .limit(rdapResultSetMaxSize), - Registrar.loadByClientIdRange( - partialStringQuery.getInitialString(), - partialStringQuery.getNextInitialString(), - rdapResultSetMaxSize), + ImmutableList.of(), now); // Don't allow suffixes in entity handle search queries. } else { @@ -194,6 +192,19 @@ public class RdapEntitySearchAction extends RdapActionBase { } } + /** Looks up registrars by handle (i.e. IANA identifier). */ + private ImmutableList + getMatchingRegistrars(final RdapSearchPattern partialStringQuery) { + Long ianaIdentifier; + try { + ianaIdentifier = Long.parseLong(partialStringQuery.getInitialString()); + } catch (NumberFormatException e) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(Registrar.loadByIanaIdentifierRange( + ianaIdentifier, ianaIdentifier + 1, rdapResultSetMaxSize)); + } + /** Builds a JSON array of entity info maps based on the specified contacts and registrars. */ private ImmutableList> makeSearchResults( Iterable contactResources, Iterable registrars, DateTime now) diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index c0cb5b874..91a38a348 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -621,11 +621,11 @@ public class RdapJsonFormatter { DateTime now) { ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); builder.put("objectClassName", "entity"); - builder.put("handle", registrar.getClientIdentifier()); + builder.put("handle", registrar.getIanaIdentifier().toString()); builder.put("status", STATUS_LIST_ACTIVE); builder.put("roles", ImmutableList.of(RdapEntityRole.REGISTRAR.rfc7483String)); builder.put("links", - ImmutableList.of(makeLink("entity", registrar.getClientIdentifier(), linkBase))); + ImmutableList.of(makeLink("entity", registrar.getIanaIdentifier().toString(), linkBase))); builder.put("publicIds", ImmutableList.of( ImmutableMap.of( @@ -805,7 +805,7 @@ public class RdapJsonFormatter { ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); eventsBuilder.add(makeEvent( RdapEventAction.REGISTRATION, - registrar.getClientIdentifier(), + registrar.getIanaIdentifier().toString(), registrar.getCreationTime())); if ((registrar.getLastUpdateTime() != null) && registrar.getLastUpdateTime().isAfter(registrar.getCreationTime())) { diff --git a/javatests/google/registry/model/registrar/RegistrarTest.java b/javatests/google/registry/model/registrar/RegistrarTest.java index ba2300d57..e1452e5a5 100644 --- a/javatests/google/registry/model/registrar/RegistrarTest.java +++ b/javatests/google/registry/model/registrar/RegistrarTest.java @@ -125,7 +125,7 @@ public class RegistrarTest extends EntityTestCase { @Test public void testIndexing() throws Exception { - verifyIndexing(registrar, "registrarName"); + verifyIndexing(registrar, "registrarName", "ianaIdentifier"); } @Test diff --git a/javatests/google/registry/rdap/RdapEntityActionTest.java b/javatests/google/registry/rdap/RdapEntityActionTest.java index 597c04067..6a5b75895 100644 --- a/javatests/google/registry/rdap/RdapEntityActionTest.java +++ b/javatests/google/registry/rdap/RdapEntityActionTest.java @@ -75,7 +75,7 @@ public class RdapEntityActionTest { // lol createTld("lol"); registrarLol = persistResource(makeRegistrar( - "evilregistrar", "Yes Virginia