RDAP: Use IANA identifier as the registrar handle

According to the ICAAN operation profile:

2.1.7. 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.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=130452501
This commit is contained in:
mountford 2016-08-16 15:09:29 -07:00 committed by Ben McIlwain
parent 27820c512e
commit 4a34807b1d
10 changed files with 114 additions and 53 deletions

View file

@ -286,6 +286,7 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
* </ul> * </ul>
* @see "http://www.iana.org/assignments/registrar-ids/registrar-ids.txt" * @see "http://www.iana.org/assignments/registrar-ids/registrar-ids.txt"
*/ */
@Index
Long ianaIdentifier; Long ianaIdentifier;
/** Identifier of registrar used in external billing system (e.g. Oracle). */ /** 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<Registrar> loadByIanaIdentifierRange(
final Long ianaIdentifierStart,
final Long ianaIdentifierAfterEnd,
final int resultSetMaxSize) {
return ofy().doTransactionless(new Work<Iterable<Registrar>>() {
@Override
public Iterable<Registrar> run() {
return ofy().load()
.type(Registrar.class)
.filter("ianaIdentifier >=", ianaIdentifierStart)
.filter("ianaIdentifier <", ianaIdentifierAfterEnd)
.limit(resultSetMaxSize);
}});
}
/** Loads all registrar entities. */ /** Loads all registrar entities. */
public static Iterable<Registrar> loadAll() { public static Iterable<Registrar> loadAll() {
return ofy().load().type(Registrar.class).ancestor(getCrossTldKey()); return ofy().load().type(Registrar.class).ancestor(getCrossTldKey());

View file

@ -20,6 +20,7 @@ import static google.registry.request.Action.Method.HEAD;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.re2j.Pattern; import com.google.re2j.Pattern;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.model.contact.ContactResource; import google.registry.model.contact.ContactResource;
@ -34,7 +35,14 @@ import javax.inject.Inject;
import org.joda.time.DateTime; 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:
*
* <p>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 IANAs
* 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) @Action(path = RdapEntityAction.PATH, method = {GET, HEAD}, isPrefix = true)
public class RdapEntityAction extends RdapActionBase { public class RdapEntityAction extends RdapActionBase {
@ -80,17 +88,23 @@ public class RdapEntityAction extends RdapActionBase {
now); now);
} }
} }
String clientId = pathSearchString.trim(); try {
if ((clientId.length() >= 3) && (clientId.length() <= 16)) { Long ianaIdentifier = Long.parseLong(pathSearchString);
wasValidKey = true; wasValidKey = true;
Registrar registrar = Registrar.loadByClientId(clientId); Registrar registrar = Iterables.getOnlyElement(
Registrar.loadByIanaIdentifierRange(ianaIdentifier, ianaIdentifier + 1, 1), null);
if ((registrar != null) && registrar.isActiveAndPubliclyVisible()) { if ((registrar != null) && registrar.isActiveAndPubliclyVisible()) {
return RdapJsonFormatter.makeRdapJsonForRegistrar( return RdapJsonFormatter.makeRdapJsonForRegistrar(
registrar, true, rdapLinkBase, rdapWhoisServer, now); 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 // At this point, we have failed to find either a contact or a registrar.
? new BadRequestException(pathSearchString + " is not a valid entity handle") throw wasValidKey
: new NotFoundException(pathSearchString + " not found"); ? new NotFoundException(pathSearchString + " not found")
: new BadRequestException(pathSearchString + " is not a valid entity handle");
} }
} }

View file

@ -162,17 +162,18 @@ public class RdapEntitySearchAction extends RdapActionBase {
.type(ContactResource.class) .type(ContactResource.class)
.id(partialStringQuery.getInitialString()) .id(partialStringQuery.getInitialString())
.now(); .now();
Registrar registrar = Registrar.loadByClientId(partialStringQuery.getInitialString()); ImmutableList<Registrar> registrars = getMatchingRegistrars(partialStringQuery);
return makeSearchResults( return makeSearchResults(
((contactResource == null) || !contactResource.getDeletionTime().isEqual(END_OF_TIME)) ((contactResource == null) || !contactResource.getDeletionTime().isEqual(END_OF_TIME))
? ImmutableList.<ContactResource>of() : ImmutableList.of(contactResource), ? ImmutableList.<ContactResource>of() : ImmutableList.of(contactResource),
(registrar == null) registrars,
? ImmutableList.<Registrar>of() : ImmutableList.of(registrar),
now); now);
// Handle queries with a wildcard, but no suffix. For contact resources, the deletion time will // 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 // 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 // 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) { } else if (partialStringQuery.getSuffix() == null) {
return makeSearchResults( return makeSearchResults(
ofy().load() ofy().load()
@ -183,10 +184,7 @@ public class RdapEntitySearchAction extends RdapActionBase {
"<", Key.create(ContactResource.class, partialStringQuery.getNextInitialString())) "<", Key.create(ContactResource.class, partialStringQuery.getNextInitialString()))
.filter("deletionTime", END_OF_TIME) .filter("deletionTime", END_OF_TIME)
.limit(rdapResultSetMaxSize), .limit(rdapResultSetMaxSize),
Registrar.loadByClientIdRange( ImmutableList.<Registrar>of(),
partialStringQuery.getInitialString(),
partialStringQuery.getNextInitialString(),
rdapResultSetMaxSize),
now); now);
// Don't allow suffixes in entity handle search queries. // Don't allow suffixes in entity handle search queries.
} else { } else {
@ -194,6 +192,19 @@ public class RdapEntitySearchAction extends RdapActionBase {
} }
} }
/** Looks up registrars by handle (i.e. IANA identifier). */
private ImmutableList<Registrar>
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. */ /** Builds a JSON array of entity info maps based on the specified contacts and registrars. */
private ImmutableList<ImmutableMap<String, Object>> makeSearchResults( private ImmutableList<ImmutableMap<String, Object>> makeSearchResults(
Iterable<ContactResource> contactResources, Iterable<Registrar> registrars, DateTime now) Iterable<ContactResource> contactResources, Iterable<Registrar> registrars, DateTime now)

View file

@ -621,11 +621,11 @@ public class RdapJsonFormatter {
DateTime now) { DateTime now) {
ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>(); ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
builder.put("objectClassName", "entity"); builder.put("objectClassName", "entity");
builder.put("handle", registrar.getClientIdentifier()); builder.put("handle", registrar.getIanaIdentifier().toString());
builder.put("status", STATUS_LIST_ACTIVE); builder.put("status", STATUS_LIST_ACTIVE);
builder.put("roles", ImmutableList.of(RdapEntityRole.REGISTRAR.rfc7483String)); builder.put("roles", ImmutableList.of(RdapEntityRole.REGISTRAR.rfc7483String));
builder.put("links", builder.put("links",
ImmutableList.of(makeLink("entity", registrar.getClientIdentifier(), linkBase))); ImmutableList.of(makeLink("entity", registrar.getIanaIdentifier().toString(), linkBase)));
builder.put("publicIds", builder.put("publicIds",
ImmutableList.of( ImmutableList.of(
ImmutableMap.of( ImmutableMap.of(
@ -805,7 +805,7 @@ public class RdapJsonFormatter {
ImmutableList.Builder<Object> eventsBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder<Object> eventsBuilder = new ImmutableList.Builder<>();
eventsBuilder.add(makeEvent( eventsBuilder.add(makeEvent(
RdapEventAction.REGISTRATION, RdapEventAction.REGISTRATION,
registrar.getClientIdentifier(), registrar.getIanaIdentifier().toString(),
registrar.getCreationTime())); registrar.getCreationTime()));
if ((registrar.getLastUpdateTime() != null) if ((registrar.getLastUpdateTime() != null)
&& registrar.getLastUpdateTime().isAfter(registrar.getCreationTime())) { && registrar.getLastUpdateTime().isAfter(registrar.getCreationTime())) {

View file

@ -125,7 +125,7 @@ public class RegistrarTest extends EntityTestCase {
@Test @Test
public void testIndexing() throws Exception { public void testIndexing() throws Exception {
verifyIndexing(registrar, "registrarName"); verifyIndexing(registrar, "registrarName", "ianaIdentifier");
} }
@Test @Test

View file

@ -75,7 +75,7 @@ public class RdapEntityActionTest {
// lol // lol
createTld("lol"); createTld("lol");
registrarLol = persistResource(makeRegistrar( registrarLol = persistResource(makeRegistrar(
"evilregistrar", "Yes Virginia <script>", Registrar.State.ACTIVE)); "evilregistrar", "Yes Virginia <script>", Registrar.State.ACTIVE, 101L));
persistSimpleResources(makeRegistrarContacts(registrarLol)); persistSimpleResources(makeRegistrarContacts(registrarLol));
registrant = makeAndPersistContactResource( registrant = makeAndPersistContactResource(
"8372808-ERL", "8372808-ERL",
@ -108,8 +108,8 @@ public class RdapEntityActionTest {
registrarLol)); registrarLol));
// xn--q9jyb4c // xn--q9jyb4c
createTld("xn--q9jyb4c"); createTld("xn--q9jyb4c");
Registrar registrarIdn = Registrar registrarIdn = persistResource(
persistResource(makeRegistrar("idnregistrar", "IDN Registrar", Registrar.State.ACTIVE)); makeRegistrar("idnregistrar", "IDN Registrar", Registrar.State.ACTIVE, 102L));
persistSimpleResources(makeRegistrarContacts(registrarIdn)); persistSimpleResources(makeRegistrarContacts(registrarIdn));
persistResource(makeDomainResource("cat.みんな", persistResource(makeDomainResource("cat.みんな",
registrant, registrant,
@ -120,7 +120,7 @@ public class RdapEntityActionTest {
registrarIdn)); registrarIdn));
createTld("1.tld"); createTld("1.tld");
Registrar registrar1tld = persistResource( Registrar registrar1tld = persistResource(
makeRegistrar("1tldregistrar", "Multilevel Registrar", Registrar.State.ACTIVE)); makeRegistrar("1tldregistrar", "Multilevel Registrar", Registrar.State.ACTIVE, 103L));
persistSimpleResources(makeRegistrarContacts(registrar1tld)); persistSimpleResources(makeRegistrarContacts(registrar1tld));
persistResource(makeDomainResource("cat.1.tld", persistResource(makeDomainResource("cat.1.tld",
registrant, registrant,
@ -238,12 +238,29 @@ public class RdapEntityActionTest {
@Test @Test
public void testRegistrar_works() throws Exception { public void testRegistrar_works() throws Exception {
assertThat(generateActualJson(registrarLol.getClientIdentifier())).isEqualTo( assertThat(generateActualJson("101")).isEqualTo(
generateExpectedJsonWithTopLevelEntries( generateExpectedJsonWithTopLevelEntries("101", "rdap_registrar.json"));
registrarLol.getClientIdentifier(), "rdap_registrar.json"));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@Test
public void testRegistrar102_works() throws Exception {
generateActualJson("102");
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void testRegistrar103_works() throws Exception {
generateActualJson("103");
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void testRegistrar104_doesNotExist() throws Exception {
generateActualJson("104");
assertThat(response.getStatus()).isEqualTo(404);
}
@Test @Test
public void testDeletedContact_returns404() throws Exception { public void testDeletedContact_returns404() throws Exception {
assertThat(generateActualJson(deletedContact.getRepoId())).isEqualTo( assertThat(generateActualJson(deletedContact.getRepoId())).isEqualTo(

View file

@ -99,12 +99,12 @@ public class RdapEntitySearchActionTest {
registrar = registrar =
persistResource( persistResource(
makeRegistrar("2-Registrar", "Yes Virginia <script>", Registrar.State.ACTIVE)); makeRegistrar("2-Registrar", "Yes Virginia <script>", Registrar.State.ACTIVE, 20L));
persistSimpleResources(makeRegistrarContacts(registrar)); persistSimpleResources(makeRegistrarContacts(registrar));
// inactive // inactive
registrarInactive = registrarInactive =
persistResource(makeRegistrar("2-RegistrarInact", "No Way", Registrar.State.PENDING)); persistResource(makeRegistrar("2-RegistrarInact", "No Way", Registrar.State.PENDING, 21L));
persistSimpleResources(makeRegistrarContacts(registrarInactive)); persistSimpleResources(makeRegistrarContacts(registrarInactive));
// test // test
@ -270,7 +270,7 @@ public class RdapEntitySearchActionTest {
assertThat(generateActualJsonWithFullName("Yes Virginia <script>")) assertThat(generateActualJsonWithFullName("Yes Virginia <script>"))
.isEqualTo( .isEqualTo(
generateExpectedJsonForEntity( generateExpectedJsonForEntity(
"2-Registrar", "Yes Virginia <script>", null, null, "rdap_registrar.json")); "20", "Yes Virginia <script>", null, null, "rdap_registrar.json"));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@ -288,11 +288,11 @@ public class RdapEntitySearchActionTest {
} }
@Test @Test
public void testHandleMatch_registrar_found() throws Exception { public void testHandleMatch_20_found() throws Exception {
assertThat(generateActualJsonWithHandle("2-Registrar")) assertThat(generateActualJsonWithHandle("20"))
.isEqualTo( .isEqualTo(
generateExpectedJsonForEntity( generateExpectedJsonForEntity(
"2-Registrar", "Yes Virginia <script>", null, null, "rdap_registrar.json")); "20", "Yes Virginia <script>", null, null, "rdap_registrar.json"));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@ -314,13 +314,6 @@ public class RdapEntitySearchActionTest {
assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getStatus()).isEqualTo(404);
} }
@Test
public void testHandleMatch_2rstar_found() throws Exception {
assertThat(generateActualJsonWithHandle("2-R*"))
.isEqualTo(generateExpectedJson("rdap_multiple_contacts.json"));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test @Test
public void testHandleMatch_2rstarWithResultSetSize1_foundOne() throws Exception { public void testHandleMatch_2rstarWithResultSetSize1_foundOne() throws Exception {
action.rdapResultSetMaxSize = 1; action.rdapResultSetMaxSize = 1;
@ -349,12 +342,9 @@ public class RdapEntitySearchActionTest {
} }
@Test @Test
public void testHandleMatch_2registstar_found() throws Exception { public void testHandleMatch_20star_notFound() throws Exception {
assertThat(generateActualJsonWithHandle("2-Regist*")) generateActualJsonWithHandle("20*");
.isEqualTo( assertThat(response.getStatus()).isEqualTo(404);
generateExpectedJsonForEntity(
"2-Registrar", "Yes Virginia <script>", null, null, "rdap_registrar.json"));
assertThat(response.getStatus()).isEqualTo(200);
} }
@Test @Test

View file

@ -16,7 +16,7 @@
[ [
{ {
"type" : "IANA Registrar ID", "type" : "IANA Registrar ID",
"identifier" : "1" "identifier" : "%NAME%"
} }
], ],
"events": [ "events": [

View file

@ -1,21 +1,21 @@
{ {
"objectClassName" : "entity", "objectClassName" : "entity",
"handle" : "unicoderegistrar", "handle" : "1",
"status" : ["active"], "status" : ["active"],
"roles" : ["registrar"], "roles" : ["registrar"],
"links" : "links" :
[ [
{ {
"value" : "http://myserver.google.com/entity/unicoderegistrar", "value" : "http://myserver.google.com/entity/1",
"rel" : "self", "rel" : "self",
"href" : "http://myserver.google.com/entity/unicoderegistrar", "href" : "http://myserver.google.com/entity/1",
"type" : "application/rdap+json" "type" : "application/rdap+json"
} }
], ],
"events": [ "events": [
{ {
"eventAction": "registration", "eventAction": "registration",
"eventActor": "unicoderegistrar", "eventActor": "1",
"eventDate": "1999-01-01T00:00:00.000Z" "eventDate": "1999-01-01T00:00:00.000Z"
}, },
{ {

View file

@ -51,11 +51,16 @@ public final class FullFieldsTestEntityHelper {
public static Registrar makeRegistrar( public static Registrar makeRegistrar(
String clientId, String registrarName, Registrar.State state) { String clientId, String registrarName, Registrar.State state) {
return makeRegistrar(clientId, registrarName, state, 1L);
}
public static Registrar makeRegistrar(
String clientId, String registrarName, Registrar.State state, Long ianaIdentifier) {
Registrar registrar = new Registrar.Builder() Registrar registrar = new Registrar.Builder()
.setClientIdentifier(clientId) .setClientIdentifier(clientId)
.setRegistrarName(registrarName) .setRegistrarName(registrarName)
.setType(Registrar.Type.REAL) .setType(Registrar.Type.REAL)
.setIanaIdentifier(1L) .setIanaIdentifier(ianaIdentifier)
.setState(state) .setState(state)
.setInternationalizedAddress(new RegistrarAddress.Builder() .setInternationalizedAddress(new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard <script>")) .setStreet(ImmutableList.of("123 Example Boulevard <script>"))