From 5b444d93fba6cf52c580dcb2707ba499d74447f3 Mon Sep 17 00:00:00 2001 From: mountford Date: Mon, 11 Sep 2017 15:18:43 -0700 Subject: [PATCH] Add administrator privileges to RDAP RDAP filters some contact data when the request is not authenticated as coming from a user associated with the registrar owning the contact. This CL adds an exception for admin users, which for the App Engine Users API are defined as App Engine project viewers. This means that the registry team will always get all information when logged in. This will also be useful when building tools that use RDAP output. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168293820 --- java/google/registry/rdap/RdapActionBase.java | 13 ++-- .../registry/rdap/RdapAuthorization.java | 62 +++++++++++++++++++ .../registry/rdap/RdapDomainAction.java | 2 +- .../registry/rdap/RdapDomainSearchAction.java | 4 +- .../registry/rdap/RdapEntityAction.java | 2 +- .../registry/rdap/RdapEntitySearchAction.java | 4 +- .../registry/rdap/RdapJsonFormatter.java | 21 +++---- .../registry/rdap/RdapDomainActionTest.java | 17 +++++ .../rdap/RdapDomainSearchActionTest.java | 12 ++-- .../registry/rdap/RdapEntityActionTest.java | 12 ++++ .../rdap/RdapEntitySearchActionTest.java | 17 +++++ .../registry/rdap/RdapJsonFormatterTest.java | 26 ++++---- 12 files changed, 152 insertions(+), 40 deletions(-) create mode 100644 java/google/registry/rdap/RdapAuthorization.java diff --git a/java/google/registry/rdap/RdapActionBase.java b/java/google/registry/rdap/RdapActionBase.java index 4b345e7b8..06d8d09ad 100644 --- a/java/google/registry/rdap/RdapActionBase.java +++ b/java/google/registry/rdap/RdapActionBase.java @@ -150,20 +150,23 @@ public abstract class RdapActionBase implements Runnable { } } - Optional getLoggedInClientId() { + RdapAuthorization getAuthorization() { if (!authResult.userAuthInfo().isPresent()) { - return Optional.absent(); + return RdapAuthorization.PUBLIC_AUTHORIZATION; } UserAuthInfo userAuthInfo = authResult.userAuthInfo().get(); + if (userAuthInfo.isUserAdmin()) { + return RdapAuthorization.ADMINISTRATOR_AUTHORIZATION; + } if (!sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)) { - return Optional.absent(); + return RdapAuthorization.PUBLIC_AUTHORIZATION; } String clientId = sessionUtils.getRegistrarClientId(request); Optional registrar = Registrar.loadByClientIdCached(clientId); if (!registrar.isPresent()) { - return Optional.absent(); + return RdapAuthorization.PUBLIC_AUTHORIZATION; } - return Optional.of(clientId); + return RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, clientId); } void validateDomainName(String name) { diff --git a/java/google/registry/rdap/RdapAuthorization.java b/java/google/registry/rdap/RdapAuthorization.java new file mode 100644 index 000000000..7e09c3032 --- /dev/null +++ b/java/google/registry/rdap/RdapAuthorization.java @@ -0,0 +1,62 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.rdap; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import google.registry.model.ImmutableObject; + +/** Authorization information for RDAP data access. */ +@AutoValue +public abstract class RdapAuthorization extends ImmutableObject { + + enum Role { + ADMINISTRATOR, + REGISTRAR, + PUBLIC + } + + /** The role to be used for access. */ + public abstract Role role(); + + /** The registrar client IDs for which access is granted (used only if the role is REGISTRAR. */ + public abstract ImmutableList clientIds(); + + static RdapAuthorization create(Role role, String clientId) { + return new AutoValue_RdapAuthorization(role, ImmutableList.of(clientId)); + } + + static RdapAuthorization create(Role role, ImmutableList clientIds) { + return new AutoValue_RdapAuthorization(role, clientIds); + } + + boolean isAuthorizedForClientId(String clientId) { + switch (role()) { + case ADMINISTRATOR: + return true; + case REGISTRAR: + return clientIds().contains(clientId); + default: + return false; + } + } + + public static final RdapAuthorization PUBLIC_AUTHORIZATION = + create(Role.PUBLIC, ImmutableList.of()); + + public static final RdapAuthorization ADMINISTRATOR_AUTHORIZATION = + create(Role.ADMINISTRATOR, ImmutableList.of()); +} + diff --git a/java/google/registry/rdap/RdapDomainAction.java b/java/google/registry/rdap/RdapDomainAction.java index 34f69da99..356e49f9c 100644 --- a/java/google/registry/rdap/RdapDomainAction.java +++ b/java/google/registry/rdap/RdapDomainAction.java @@ -70,6 +70,6 @@ public class RdapDomainAction extends RdapActionBase { rdapWhoisServer, now, OutputDataType.FULL, - getLoggedInClientId()); + getAuthorization()); } } diff --git a/java/google/registry/rdap/RdapDomainSearchAction.java b/java/google/registry/rdap/RdapDomainSearchAction.java index 4b24bd266..1e15ea868 100644 --- a/java/google/registry/rdap/RdapDomainSearchAction.java +++ b/java/google/registry/rdap/RdapDomainSearchAction.java @@ -375,12 +375,12 @@ public class RdapDomainSearchAction extends RdapActionBase { List domains, boolean isTruncated, DateTime now) { OutputDataType outputDataType = (domains.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; - Optional loggedInClientId = getLoggedInClientId(); + RdapAuthorization authorization = getAuthorization(); ImmutableList.Builder> jsonBuilder = new ImmutableList.Builder<>(); for (DomainResource domain : domains) { jsonBuilder.add( rdapJsonFormatter.makeRdapJsonForDomain( - domain, false, rdapLinkBase, rdapWhoisServer, now, outputDataType, loggedInClientId)); + domain, false, rdapLinkBase, rdapWhoisServer, now, outputDataType, authorization)); } return RdapSearchResults.create(jsonBuilder.build(), isTruncated); } diff --git a/java/google/registry/rdap/RdapEntityAction.java b/java/google/registry/rdap/RdapEntityAction.java index 4a04d5d2a..3fd51f2ed 100644 --- a/java/google/registry/rdap/RdapEntityAction.java +++ b/java/google/registry/rdap/RdapEntityAction.java @@ -94,7 +94,7 @@ public class RdapEntityAction extends RdapActionBase { rdapWhoisServer, now, OutputDataType.FULL, - getLoggedInClientId()); + getAuthorization()); } } Long ianaIdentifier = Longs.tryParse(pathSearchString); diff --git a/java/google/registry/rdap/RdapEntitySearchAction.java b/java/google/registry/rdap/RdapEntitySearchAction.java index cdf52d76a..e3c5eb39b 100644 --- a/java/google/registry/rdap/RdapEntitySearchAction.java +++ b/java/google/registry/rdap/RdapEntitySearchAction.java @@ -254,7 +254,7 @@ public class RdapEntitySearchAction extends RdapActionBase { // There can be more results than our max size, partially because we have two pools to draw from // (contacts and registrars), and partially because we try to fetch one more than the max size, // so we can tell whether to display the truncation notification. - Optional loggedInClientId = getLoggedInClientId(); + RdapAuthorization authorization = getAuthorization(); List> jsonOutputList = new ArrayList<>(); for (ContactResource contact : contacts) { if (jsonOutputList.size() >= rdapResultSetMaxSize) { @@ -270,7 +270,7 @@ public class RdapEntitySearchAction extends RdapActionBase { rdapWhoisServer, now, outputDataType, - loggedInClientId)); + authorization)); } for (Registrar registrar : registrars) { if (registrar.isActiveAndPubliclyVisible()) { diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index b001a5e65..d32341ba9 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -444,8 +444,8 @@ public class RdapJsonFormatter { * port43 field; if null, port43 is not added to the object * @param now the as-date * @param outputDataType whether to generate full or summary data - * @param loggedInClientId the logged-in client ID (or null if not logged in); if the requester is - * not logged in as the registrar owning the domain, no contact information is included + * @param authorization the authorization level of the request; if not authorized for the + * registrar owning the domain, no contact information is included */ ImmutableMap makeRdapJsonForDomain( DomainResource domainResource, @@ -454,7 +454,7 @@ public class RdapJsonFormatter { @Nullable String whoisServer, DateTime now, OutputDataType outputDataType, - Optional loggedInClientId) { + RdapAuthorization authorization) { // Start with the domain-level information. ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); jsonBuilder.put("objectClassName", "domain"); @@ -467,8 +467,8 @@ public class RdapJsonFormatter { jsonBuilder.put("status", makeStatusValueList(domainResource.getStatusValues())); jsonBuilder.put("links", ImmutableList.of( makeLink("domain", domainResource.getFullyQualifiedDomainName(), linkBase))); - boolean displayContacts = loggedInClientId.isPresent() - && loggedInClientId.get().equals(domainResource.getCurrentSponsorClientId()); + boolean displayContacts = + authorization.isAuthorizedForClientId(domainResource.getCurrentSponsorClientId()); // If we are outputting all data (not just summary data), also add information about hosts, // contacts and events (history entries). If we are outputting summary data, instead add a // remark indicating that fact. @@ -505,7 +505,7 @@ public class RdapJsonFormatter { null, now, outputDataType, - loggedInClientId)); + authorization)); } ImmutableList entities = entitiesBuilder.build(); if (!entities.isEmpty()) { @@ -644,8 +644,8 @@ public class RdapJsonFormatter { * port43 field; if null, port43 is not added to the object * @param now the as-date * @param outputDataType whether to generate full or summary data - * @param loggedInClientId the logged-in client ID (or null if not logged in); personal contact - * data is only shown if the contact is owned by the logged-in client + * @param authorization the authorization level of the request; personal contact data is only + * shown if the contact is owned by a registrar for which the request is authorized */ ImmutableMap makeRdapJsonForContact( ContactResource contactResource, @@ -655,7 +655,7 @@ public class RdapJsonFormatter { @Nullable String whoisServer, DateTime now, OutputDataType outputDataType, - Optional loggedInClientId) { + RdapAuthorization authorization) { ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); ImmutableList.Builder> remarksBuilder = new ImmutableList.Builder<>(); @@ -672,8 +672,7 @@ public class RdapJsonFormatter { jsonBuilder.put("links", ImmutableList.of(makeLink("entity", contactResource.getRepoId(), linkBase))); // If we are logged in as the owner of this contact, create the vCard. - if (loggedInClientId.isPresent() - && loggedInClientId.get().equals(contactResource.getCurrentSponsorClientId())) { + if (authorization.isAuthorizedForClientId(contactResource.getCurrentSponsorClientId())) { ImmutableList.Builder vcardBuilder = new ImmutableList.Builder<>(); vcardBuilder.add(VCARD_ENTRY_VERSION); PostalInfo postalInfo = contactResource.getInternationalizedPostalInfo(); diff --git a/javatests/google/registry/rdap/RdapDomainActionTest.java b/javatests/google/registry/rdap/RdapDomainActionTest.java index d267b9540..4652d9cc8 100644 --- a/javatests/google/registry/rdap/RdapDomainActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainActionTest.java @@ -350,6 +350,23 @@ public class RdapDomainActionTest { assertThat(response.getStatus()).isEqualTo(200); } + @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"); + assertJsonEqual( + generateActualJson("cat.lol"), + generateExpectedJsonWithTopLevelEntries( + "cat.lol", + null, + "C-LOL", + ImmutableList.of("4-ROID", "6-ROID", "2-ROID"), + "rdap_domain.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void testValidDomain_notLoggedIn_noContacts() throws Exception { when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(false); diff --git a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java index 4149e4b3c..55e062f70 100644 --- a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java @@ -455,17 +455,19 @@ public class RdapDomainSearchActionTest { } @Test - public void testDomainMatch_found_notLoggedIn() throws Exception { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(false); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("evilregistrar"); + 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"); assertThat(generateActualJson(RequestType.NAME, "cat.lol")) .isEqualTo( generateExpectedJsonForDomain( "cat.lol", null, "C-LOL", - null, - "rdap_domain_no_contacts_with_remark.json")); + ImmutableList.of("4-ROID", "6-ROID", "2-ROID"), + "rdap_domain.json")); assertThat(response.getStatus()).isEqualTo(200); } diff --git a/javatests/google/registry/rdap/RdapEntityActionTest.java b/javatests/google/registry/rdap/RdapEntityActionTest.java index 62212f1f3..53d3a55d3 100644 --- a/javatests/google/registry/rdap/RdapEntityActionTest.java +++ b/javatests/google/registry/rdap/RdapEntityActionTest.java @@ -235,6 +235,18 @@ public class RdapEntityActionTest { assertThat(response.getStatus()).isEqualTo(200); } + @Test + public void testValidRegistrantContact_works_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"); + assertThat(generateActualJson(registrant.getRepoId())).isEqualTo( + generateExpectedJsonWithTopLevelEntries( + registrant.getRepoId(), "rdap_associated_contact.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void testValidRegistrantContact_works_notLoggedIn() throws Exception { when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(false); diff --git a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java index 34c665255..e6b29e011 100644 --- a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java +++ b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java @@ -308,6 +308,23 @@ public class RdapEntitySearchActionTest { assertThat(response.getStatus()).isEqualTo(200); } + @Test + public void testNameMatch_contactFound_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"); + assertThat(generateActualJsonWithFullName("Blinky (赤ベイ)")) + .isEqualTo( + generateExpectedJsonForEntity( + "2-ROID", + "Blinky (赤ベイ)", + "blinky@b.tld", + "\"123 Blinky St\", \"Blinkyland\"", + "rdap_contact.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void testNameMatch_contactFound_notLoggedIn() throws Exception { when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(false); diff --git a/javatests/google/registry/rdap/RdapJsonFormatterTest.java b/javatests/google/registry/rdap/RdapJsonFormatterTest.java index 518aa2a55..e196f8205 100644 --- a/javatests/google/registry/rdap/RdapJsonFormatterTest.java +++ b/javatests/google/registry/rdap/RdapJsonFormatterTest.java @@ -370,7 +370,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_registrant.json")); } @@ -385,7 +385,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.SUMMARY, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_registrant_summary.json")); } @@ -400,7 +400,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.absent())) + RdapAuthorization.PUBLIC_AUTHORIZATION)) .isEqualTo(loadJson("rdapjson_registrant_logged_out.json")); } @@ -415,7 +415,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_registrant.json")); } @@ -430,7 +430,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_registrant_nobase.json")); } @@ -445,7 +445,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_admincontact.json")); } @@ -460,7 +460,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_techcontact.json")); } @@ -475,7 +475,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_rolelesscontact.json")); } @@ -490,7 +490,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_unlinkedcontact.json")); } @@ -503,7 +503,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_domain_full.json")); } @@ -516,7 +516,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.SUMMARY, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_domain_summary.json")); } @@ -529,7 +529,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.absent())) + RdapAuthorization.PUBLIC_AUTHORIZATION)) .isEqualTo(loadJson("rdapjson_domain_logged_out.json")); } @@ -542,7 +542,7 @@ public class RdapJsonFormatterTest { WHOIS_SERVER, clock.nowUtc(), OutputDataType.FULL, - Optional.of("unicoderegistrar"))) + RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar"))) .isEqualTo(loadJson("rdapjson_domain_no_nameservers.json")); }