Add RDAP search support for only contacts or only registrars

By default, RDAP entity searches return both contacts and registrars. This CL
adds a new query parameter to request only one or the other. Among other
benefits, this will allow a future CL to permit wildcard searches that return
all registrars.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=181605990
This commit is contained in:
mountford 2018-01-11 07:30:03 -08:00 committed by Ben McIlwain
parent e07d011bc6
commit 716ba726fc
4 changed files with 253 additions and 47 deletions

View file

@ -341,6 +341,14 @@ formatted version can be requested by adding an extra parameter:
The result is still valid JSON, but with extra whitespace added to align the
data on the page.
### `subtype` parameter <a id="subtype_parameter"></a>
The subtype parameter is used only for entity searches, to select whether the
results should include contacts, registrars or both. If specified, the subtype
should be 'all', 'contacts' or 'registrars'. Setting the subtype to 'all'
duplicates the normal behavior of returning both. Setting it to 'contacts' or
'registrars' causes an entity search to return only contacts or only registrars.
### Next page links <a id="next_page_links"></a>
The number of results returned in a domain, nameserver or entity search is

View file

@ -86,6 +86,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
@Inject Clock clock;
@Inject @Parameter("fn") Optional<String> fnParam;
@Inject @Parameter("handle") Optional<String> handleParam;
@Inject @Parameter("subtype") Optional<String> subtypeParam;
@Inject RdapEntitySearchAction() {}
private enum QueryType {
@ -93,6 +94,12 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
HANDLE
}
private enum Subtype {
ALL,
CONTACTS,
REGISTRARS
}
private enum CursorType {
NONE,
CONTACT,
@ -132,6 +139,18 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
throw new BadRequestException("You must specify either fn=XXXX or handle=YYYY");
}
// Check the subtype.
Subtype subtype;
if (!subtypeParam.isPresent() || subtypeParam.get().equalsIgnoreCase("all")) {
subtype = Subtype.ALL;
} else if (subtypeParam.get().equalsIgnoreCase("contacts")) {
subtype = Subtype.CONTACTS;
} else if (subtypeParam.get().equalsIgnoreCase("registrars")) {
subtype = Subtype.REGISTRARS;
} else {
throw new BadRequestException("Subtype parameter must specify contacts, registrars or all");
}
// Decode the cursor token and extract the prefix and string portions.
decodeCursorToken();
CursorType cursorType;
@ -164,6 +183,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
recordWildcardType(RdapSearchPattern.create(fnParam.get(), false)),
cursorType,
cursorQueryString,
subtype,
now);
// Search by handle.
@ -175,6 +195,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
searchByHandle(
recordWildcardType(RdapSearchPattern.create(handleParam.get(), false)),
cursorQueryString,
subtype,
now);
}
@ -221,6 +242,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
final RdapSearchPattern partialStringQuery,
CursorType cursorType,
Optional<String> cursorQueryString,
Subtype subtype,
DateTime now) {
// For wildcard searches, make sure the initial string is long enough, and don't allow suffixes.
if (partialStringQuery.getHasWildcard() && (partialStringQuery.getSuffix() != null)) {
@ -238,8 +260,12 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
: "Initial search string required when searching for deleted entities");
}
// Get the registrar matches. If we have a registrar cursor, weed out registrars up to and
// including the one we ended with last time.
ImmutableList<Registrar> registrars =
// including the one we ended with last time. We can skip registrars if subtype is CONTACTS.
ImmutableList<Registrar> registrars;
if (subtype == Subtype.CONTACTS) {
registrars = ImmutableList.of();
} else {
registrars =
Streams.stream(Registrar.loadAllCached())
.filter(
registrar ->
@ -250,11 +276,16 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
&& shouldBeVisible(registrar))
.limit(rdapResultSetMaxSize + 1)
.collect(toImmutableList());
}
// Get the contact matches and return the results, fetching an additional contact to detect
// truncation. Don't bother searching for contacts by name if the request would not be able to
// see any names anyway. Also, if a registrar cursor is present, we have already moved past the
// contacts, and don't need to fetch them this time.
// contacts, and don't need to fetch them this time. We can skip contacts if subtype is
// REGISTRARS.
RdapResultSet<ContactResource> resultSet;
if (subtype == Subtype.REGISTRARS) {
resultSet = RdapResultSet.create(ImmutableList.of());
} else {
RdapAuthorization authorization = getAuthorization();
if ((authorization.role() == RdapAuthorization.Role.PUBLIC)
|| (cursorType == CursorType.REGISTRAR)) {
@ -273,6 +304,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
}
resultSet = getMatchingResources(query, false, now, rdapResultSetMaxSize + 1);
}
}
return makeSearchResults(resultSet, registrars, QueryType.FULL_NAME, now);
}
@ -289,25 +321,39 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
private RdapSearchResults searchByHandle(
final RdapSearchPattern partialStringQuery,
Optional<String> cursorQueryString,
Subtype subtype,
DateTime now) {
if (partialStringQuery.getSuffix() != null) {
throw new UnprocessableEntityException("Suffixes not allowed in entity handle searches");
}
// Handle queries without a wildcard (and not including deleted) -- load by ID.
if (!partialStringQuery.getHasWildcard() && !shouldIncludeDeleted()) {
ContactResource contactResource = ofy().load()
ImmutableList<ContactResource> contactResourceList;
if (subtype == Subtype.REGISTRARS) {
contactResourceList = ImmutableList.of();
} else {
ContactResource contactResource =
ofy()
.load()
.type(ContactResource.class)
.id(partialStringQuery.getInitialString())
.now();
ImmutableList<ContactResource> contactResourceList =
contactResourceList =
((contactResource != null) && shouldBeVisible(contactResource, now))
? ImmutableList.of(contactResource)
: ImmutableList.of();
}
ImmutableList<Registrar> registrarList;
if (subtype == Subtype.CONTACTS) {
registrarList = ImmutableList.of();
} else {
registrarList = getMatchingRegistrars(partialStringQuery.getInitialString());
}
return makeSearchResults(
contactResourceList,
IncompletenessWarningType.COMPLETE,
contactResourceList.size(),
getMatchingRegistrars(partialStringQuery.getInitialString()),
registrarList,
QueryType.HANDLE,
now);
// Handle queries with a wildcard (or including deleted), but no suffix. Because the handle
@ -316,7 +362,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
// detect result set truncation.
} else {
ImmutableList<Registrar> registrars =
partialStringQuery.getHasWildcard()
((subtype == Subtype.CONTACTS) || partialStringQuery.getHasWildcard())
? ImmutableList.of()
: getMatchingRegistrars(partialStringQuery.getInitialString());
// Get the contact matches and return the results, fetching an additional contact to detect
@ -324,15 +370,24 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
// get excluded due to permissioning. Any cursor present must be a contact cursor, because we
// would never return a registrar for this search.
int querySizeLimit = getStandardQuerySizeLimit();
Query<ContactResource> query =
RdapResultSet<ContactResource> contactResultSet;
if (subtype == Subtype.REGISTRARS) {
contactResultSet = RdapResultSet.create(ImmutableList.of());
} else {
contactResultSet =
getMatchingResources(
queryItemsByKey(
ContactResource.class,
partialStringQuery,
cursorQueryString,
getDeletedItemHandling(),
querySizeLimit),
shouldIncludeDeleted(),
now,
querySizeLimit);
}
return makeSearchResults(
getMatchingResources(query, shouldIncludeDeleted(), now, querySizeLimit),
contactResultSet,
registrars,
QueryType.HANDLE,
now);

View file

@ -67,6 +67,12 @@ public final class RdapModule {
return RequestParameters.extractOptionalParameter(req, "registrar");
}
@Provides
@Parameter("subtype")
static Optional<String> provideSubtype(HttpServletRequest req) {
return RequestParameters.extractOptionalParameter(req, "subtype");
}
@Provides
@Parameter("includeDeleted")
static Optional<Boolean> provideIncludeDeleted(HttpServletRequest req) {

View file

@ -26,6 +26,7 @@ import static google.registry.testing.DatastoreHelper.persistSimpleResources;
import static google.registry.testing.FullFieldsTestEntityHelper.makeAndPersistContactResource;
import static google.registry.testing.FullFieldsTestEntityHelper.makeAndPersistDeletedContactResource;
import static google.registry.testing.FullFieldsTestEntityHelper.makeContactResource;
import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntry;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarContacts;
import static google.registry.testing.TestDataHelper.loadFile;
@ -40,6 +41,7 @@ import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource;
import google.registry.model.ofy.Ofy;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.HistoryEntry;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapMetrics.SearchType;
import google.registry.rdap.RdapSearchResults.IncompletenessWarningType;
@ -192,6 +194,7 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
action.rdapWhoisServer = null;
action.fnParam = Optional.empty();
action.handleParam = Optional.empty();
action.subtypeParam = Optional.empty();
action.registrarParam = Optional.empty();
action.includeDeletedParam = Optional.empty();
action.formatOutputParam = Optional.empty();
@ -281,16 +284,20 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
.asBuilder()
.setRepoId(String.format("%04d-ROID", i))
.build();
resourcesBuilder.add(makeHistoryEntry(
contact, HistoryEntry.Type.CONTACT_CREATE, null, "created", clock.nowUtc()));
resourcesBuilder.add(contact);
}
persistResources(resourcesBuilder.build());
for (int i = 1; i <= numRegistrars; i++) {
resourcesBuilder.add(
Registrar registrar =
makeRegistrar(
String.format("registrar%d", i),
String.format("Entity %d", i + numContacts),
Registrar.State.ACTIVE,
300L + i));
300L + i);
resourcesBuilder.add(registrar);
resourcesBuilder.addAll(makeRegistrarContacts(registrar));
}
persistResources(resourcesBuilder.build());
}
@ -537,6 +544,19 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyErrorMetrics(Optional.empty(), 422);
}
@Test
public void testInvalidSubtype_rejected() throws Exception {
action.subtypeParam = Optional.of("Space Aliens");
assertThat(generateActualJsonWithFullName("Blinky (赤ベイ)"))
.isEqualTo(
generateExpectedJson(
"Subtype parameter must specify contacts, registrars or all",
"rdap_error_400.json"));
assertThat(response.getStatus()).isEqualTo(400);
metricSearchType = SearchType.NONE; // Error occurs before search type is set.
verifyErrorMetrics(Optional.empty(), 400);
}
@Test
public void testNameMatchContact_found() throws Exception {
login("2-RegistrarTest");
@ -544,6 +564,30 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyMetrics(1);
}
@Test
public void testNameMatchContact_found_subtypeAll() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("aLl");
runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
verifyMetrics(1);
}
@Test
public void testNameMatchContact_found_subtypeContacts() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("cONTACTS");
runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
verifyMetrics(1);
}
@Test
public void testNameMatchContact_notFound_subtypeRegistrars() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("Registrars");
runNotFoundNameTest("Blinky (赤ベイ)");
verifyErrorMetrics(0);
}
@Test
public void testNameMatchContact_found_specifyingSameRegistrar() throws Exception {
login("2-RegistrarTest");
@ -653,6 +697,32 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyMetrics(0);
}
@Test
public void testNameMatchRegistrar_found_subtypeAll() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("all");
runSuccessfulNameTest(
"Yes Virginia <script>", "20", "Yes Virginia <script>", "rdap_registrar.json");
verifyMetrics(0);
}
@Test
public void testNameMatchRegistrar_found_subtypeRegistrars() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("REGISTRARS");
runSuccessfulNameTest(
"Yes Virginia <script>", "20", "Yes Virginia <script>", "rdap_registrar.json");
verifyMetrics(0);
}
@Test
public void testNameMatchRegistrar_notFound_subtypeContacts() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("contacts");
runNotFoundNameTest("Yes Virginia <script>");
verifyErrorMetrics(0);
}
@Test
public void testNameMatchRegistrar_found_specifyingSameRegistrar() throws Exception {
action.registrarParam = Optional.of("2-Registrar");
@ -810,6 +880,28 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
"Entity 6"));
}
@Test
public void testNameMatchMix_subtypeContacts() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("contacts");
createManyContactsAndRegistrars(4, 4, registrarTest);
rememberWildcardType("Entity *");
assertThat(generateActualJsonWithFullName("Entity *"))
.isEqualTo(generateExpectedJson("rdap_nontruncated_contacts.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(4);
}
@Test
public void testNameMatchMix_subtypeRegistrars() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("registrars");
createManyContactsAndRegistrars(1, 1, registrarTest);
runSuccessfulNameTest(
"Entity *", "301", "Entity 2", "rdap_registrar.json");
verifyMetrics(0);
}
@Test
public void testNameMatchRegistrar_notFound_inactive() throws Exception {
runNotFoundNameTest("No Way");
@ -879,6 +971,30 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyMetrics(1);
}
@Test
public void testHandleMatchContact_found_subtypeAll() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("all");
runSuccessfulHandleTestWithBlinky("2-ROID", "rdap_contact.json");
verifyMetrics(1);
}
@Test
public void testHandleMatchContact_found_subtypeContacts() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("contacts");
runSuccessfulHandleTestWithBlinky("2-ROID", "rdap_contact.json");
verifyMetrics(1);
}
@Test
public void testHandleMatchContact_notFound_subtypeRegistrars() throws Exception {
login("2-RegistrarTest");
action.subtypeParam = Optional.of("reGistrars");
runNotFoundHandleTest("2-ROID");
verifyErrorMetrics(0);
}
@Test
public void testHandleMatchContact_found_specifyingSameRegistrar() throws Exception {
action.registrarParam = Optional.of("2-RegistrarTest");
@ -992,6 +1108,27 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyMetrics(0);
}
@Test
public void testHandleMatchRegistrar_found_subtypeAll() throws Exception {
action.subtypeParam = Optional.of("all");
runSuccessfulHandleTest("20", "20", "Yes Virginia <script>", "rdap_registrar.json");
verifyMetrics(0);
}
@Test
public void testHandleMatchRegistrar_found_subtypeRegistrars() throws Exception {
action.subtypeParam = Optional.of("registrars");
runSuccessfulHandleTest("20", "20", "Yes Virginia <script>", "rdap_registrar.json");
verifyMetrics(0);
}
@Test
public void testHandleMatchRegistrar_notFound_subtypeContacts() throws Exception {
action.subtypeParam = Optional.of("contacts");
runNotFoundHandleTest("20");
verifyErrorMetrics(0);
}
@Test
public void testHandleMatchRegistrar_found_specifyingSameRegistrar() throws Exception {
action.registrarParam = Optional.of("2-Registrar");