From d09fc7ee057dd0580d2aad72fa6acd08780eefd5 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Mon, 16 Mar 2020 11:38:05 -0400 Subject: [PATCH] Match logged-in GAE user ID with registrar POC user ID (#511) * Match logged-in GAE user ID with registrar POC user ID The reasoning for this is thus: We wish to have the users log in using Google-managed addresses--this is so that we can manage enforcement of things like 2FA, as well as generic account management. However, we wish for the registry-lock confirmation emails to go to their standard non-Google email addresses--e.g. johndoe@theregistrar.com, rather than johndoe@registry.google. As a result, for registry lock, we will enable it on the johndoe@registry.google account, but we will alter the email address of the corresponding Registrar POC account to contain johndoe@theregistrar.com. By doing this, the user will still be logging in using the @registry.google account but we'll match to their actual contact email. * fix up comments and messages * Error if >1 matching contact * include email addresses * set default optional * fix tests --- .../registrar/RegistryLockGetAction.java | 73 ++++++++++++------- .../registrar/RegistryLockPostAction.java | 40 +++++----- .../registry/testing/AppEngineRule.java | 2 +- .../registrar/RegistryLockGetActionTest.java | 32 +++++++- .../registrar/RegistryLockPostActionTest.java | 29 ++++++-- .../RegistrarConsoleScreenshotTest.java | 14 ++-- .../registry/webdriver/TestServerRule.java | 24 ++++-- .../registrar/update_registrar_email.txt | 2 +- 8 files changed, 148 insertions(+), 68 deletions(-) diff --git a/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java index 4f8daff0c..d52050d19 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java @@ -41,7 +41,6 @@ import google.registry.request.auth.Auth; import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException; -import google.registry.request.auth.UserAuthInfo; import google.registry.schema.domain.RegistryLock; import google.registry.security.JsonResponseHelper; import java.util.Optional; @@ -117,41 +116,61 @@ public final class RegistryLockGetAction implements JsonGetAction { } } - private ImmutableMap getLockedDomainsMap(String clientId) - throws RegistrarAccessDeniedException { - // Note: admins always have access to the locks page - checkArgument(authResult.userAuthInfo().isPresent(), "User auth info must be present"); - UserAuthInfo userAuthInfo = authResult.userAuthInfo().get(); - boolean isAdmin = registrarAccessor.isAdmin(); - Registrar registrar = getRegistrarAndVerifyLockAccess(clientId, isAdmin); - User user = userAuthInfo.user(); - boolean isRegistryLockAllowed = - isAdmin - || registrar.getContacts().stream() - .filter(contact -> contact.getEmailAddress().equals(user.getEmail())) - .findFirst() - .map(RegistrarContact::isRegistryLockAllowed) - .orElse(false); - return ImmutableMap.of( - LOCK_ENABLED_FOR_CONTACT_PARAM, - isRegistryLockAllowed, - EMAIL_PARAM, - user.getEmail(), - PARAM_CLIENT_ID, - registrar.getClientId(), - LOCKS_PARAM, - getLockedDomains(clientId, isAdmin)); + static Optional getContactMatchingLogin(User user, Registrar registrar) { + ImmutableList matchingContacts = + registrar.getContacts().stream() + .filter(contact -> contact.getGaeUserId().equals(user.getUserId())) + .collect(toImmutableList()); + if (matchingContacts.size() > 1) { + ImmutableList matchingEmails = + matchingContacts.stream() + .map(RegistrarContact::getEmailAddress) + .collect(toImmutableList()); + throw new IllegalArgumentException( + String.format( + "User ID %s had multiple matching contacts with email addresses %s", + user.getUserId(), matchingEmails)); + } + return matchingContacts.stream().findFirst(); } - private Registrar getRegistrarAndVerifyLockAccess(String clientId, boolean isAdmin) + static Registrar getRegistrarAndVerifyLockAccess( + AuthenticatedRegistrarAccessor registrarAccessor, String clientId, boolean isAdmin) throws RegistrarAccessDeniedException { Registrar registrar = registrarAccessor.getRegistrar(clientId); checkArgument( isAdmin || registrar.isRegistryLockAllowed(), - "Registry lock not allowed for this registrar"); + "Registry lock not allowed for registrar %s", + clientId); return registrar; } + private ImmutableMap getLockedDomainsMap(String clientId) + throws RegistrarAccessDeniedException { + // Note: admins always have access to the locks page + checkArgument(authResult.userAuthInfo().isPresent(), "User auth info must be present"); + + boolean isAdmin = registrarAccessor.isAdmin(); + Registrar registrar = getRegistrarAndVerifyLockAccess(registrarAccessor, clientId, isAdmin); + User user = authResult.userAuthInfo().get().user(); + + Optional contactOptional = getContactMatchingLogin(user, registrar); + boolean isRegistryLockAllowed = + isAdmin || contactOptional.map(RegistrarContact::isRegistryLockAllowed).orElse(false); + // Use the contact email if it's present, else use the login email + String relevantEmail = + contactOptional.map(RegistrarContact::getEmailAddress).orElse(user.getEmail()); + return ImmutableMap.of( + LOCK_ENABLED_FOR_CONTACT_PARAM, + isRegistryLockAllowed, + EMAIL_PARAM, + relevantEmail, + PARAM_CLIENT_ID, + registrar.getClientId(), + LOCKS_PARAM, + getLockedDomains(clientId, isAdmin)); + } + private ImmutableList> getLockedDomains( String clientId, boolean isAdmin) { return jpaTm() diff --git a/core/src/main/java/google/registry/ui/server/registrar/RegistryLockPostAction.java b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockPostAction.java index c8c5ee3b8..376306091 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/RegistryLockPostAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockPostAction.java @@ -20,8 +20,11 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import static google.registry.security.JsonResponseHelper.Status.ERROR; import static google.registry.security.JsonResponseHelper.Status.SUCCESS; import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID; +import static google.registry.ui.server.registrar.RegistryLockGetAction.getContactMatchingLogin; +import static google.registry.ui.server.registrar.RegistryLockGetAction.getRegistrarAndVerifyLockAccess; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; +import com.google.appengine.api.users.User; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; @@ -124,11 +127,7 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc .userAuthInfo() .orElseThrow(() -> new ForbiddenException("User is not logged in")); - boolean isAdmin = userAuthInfo.isUserAdmin(); - String userEmail = userAuthInfo.user().getEmail(); - if (!isAdmin) { - verifyRegistryLockPassword(postInput, userEmail); - } + String userEmail = verifyPasswordAndGetEmail(userAuthInfo, postInput); jpaTm() .transact( () -> { @@ -138,9 +137,11 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc postInput.fullyQualifiedDomainName, postInput.clientId, userEmail, - isAdmin) + registrarAccessor.isAdmin()) : domainLockUtils.saveNewRegistryUnlockRequest( - postInput.fullyQualifiedDomainName, postInput.clientId, isAdmin); + postInput.fullyQualifiedDomainName, + postInput.clientId, + registrarAccessor.isAdmin()); sendVerificationEmail(registryLock, userEmail, postInput.isLock); }); String action = postInput.isLock ? "lock" : "unlock"; @@ -180,25 +181,28 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc } } - private void verifyRegistryLockPassword(RegistryLockPostInput postInput, String userEmail) + private String verifyPasswordAndGetEmail( + UserAuthInfo userAuthInfo, RegistryLockPostInput postInput) throws RegistrarAccessDeniedException { - // Verify that the user can access the registrar and that the user has - // registry lock enabled and provided a correct password - Registrar registrar = registrarAccessor.getRegistrar(postInput.clientId); - checkArgument( - registrar.isRegistryLockAllowed(), "Registry lock not allowed for this registrar"); - checkArgument(!Strings.isNullOrEmpty(postInput.password), "Missing key for password"); + User user = userAuthInfo.user(); + if (registrarAccessor.isAdmin()) { + return user.getEmail(); + } + // Verify that the user can access the registrar, that the user has + // registry lock enabled, and that the user providjed a correct password + Registrar registrar = + getRegistrarAndVerifyLockAccess(registrarAccessor, postInput.clientId, false); RegistrarContact registrarContact = - registrar.getContacts().stream() - .filter(contact -> contact.getEmailAddress().equals(userEmail)) - .findFirst() + getContactMatchingLogin(user, registrar) .orElseThrow( () -> new IllegalArgumentException( - String.format("Unknown user email %s", userEmail))); + String.format( + "Cannot match user %s to registrar contact", user.getUserId()))); checkArgument( registrarContact.verifyRegistryLockPassword(postInput.password), "Incorrect registry lock password for contact"); + return registrarContact.getEmailAddress(); } /** Value class that represents the expected input body from the UI request. */ diff --git a/core/src/test/java/google/registry/testing/AppEngineRule.java b/core/src/test/java/google/registry/testing/AppEngineRule.java index b2de91811..22d56d220 100644 --- a/core/src/test/java/google/registry/testing/AppEngineRule.java +++ b/core/src/test/java/google/registry/testing/AppEngineRule.java @@ -249,7 +249,7 @@ public final class AppEngineRule extends ExternalResource { .setEmailAddress("Marla.Singer@crr.com") .setPhoneNumber("+1.2128675309") .setTypes(ImmutableSet.of(RegistrarContact.Type.TECH)) - .setGaeUserId(THE_REGISTRAR_GAE_USER_ID) + .setGaeUserId("12345") .setAllowedToSetRegistryLockPassword(true) .setRegistryLockPassword("hi") .build(); diff --git a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java index b07d44c4c..b91e465a1 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java @@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSetMultimap; import com.google.gson.Gson; +import google.registry.model.registrar.RegistrarContact; import google.registry.request.Action.Method; import google.registry.request.auth.AuthLevel; import google.registry.request.auth.AuthResult; @@ -67,14 +68,15 @@ public final class RegistryLockGetActionTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); private final FakeResponse response = new FakeResponse(); - private final User user = new User("Marla.Singer@crr.com", "gmail.com", "12345"); + private User user; private AuthResult authResult; private AuthenticatedRegistrarAccessor accessor; private RegistryLockGetAction action; @Before public void setup() { + user = userFromRegistrarContact(AppEngineRule.makeRegistrarContact3()); fakeClock.setTo(DateTime.parse("2000-06-08T22:00:00.0Z")); authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); accessor = @@ -309,6 +311,29 @@ public final class RegistryLockGetActionTest { ImmutableList.of()))); } + @Test + public void testSuccess_linkedToContactEmail() { + // Even though the user is some.email@gmail.com the contact is still Marla Singer + user = new User("some.email@gmail.com", "gmail.com", user.getUserId()); + authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); + action = + new RegistryLockGetAction( + Method.GET, response, accessor, authResult, Optional.of("TheRegistrar")); + action.run(); + assertThat(GSON.fromJson(response.getPayload(), Map.class).get("results")) + .isEqualTo( + ImmutableList.of( + ImmutableMap.of( + "lockEnabledForContact", + true, + "email", + "Marla.Singer@crr.com", + "clientId", + "TheRegistrar", + "locks", + ImmutableList.of()))); + } + @Test public void testFailure_lockNotAllowedForRegistrar() { // The UI shouldn't be making requests where lock isn't enabled for this registrar @@ -337,4 +362,9 @@ public final class RegistryLockGetActionTest { action.run(); assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); } + + static User userFromRegistrarContact(RegistrarContact registrarContact) { + return new User( + registrarContact.getEmailAddress(), "gmail.com", registrarContact.getGaeUserId()); + } } diff --git a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockPostActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockPostActionTest.java index 8b2fc9111..73418c80c 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockPostActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockPostActionTest.java @@ -23,6 +23,7 @@ import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.SqlHelper.getRegistryLockByVerificationCode; import static google.registry.testing.SqlHelper.saveRegistryLock; import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES; +import static google.registry.ui.server.registrar.RegistryLockGetActionTest.userFromRegistrarContact; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -78,11 +79,8 @@ public final class RegistryLockPostActionTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - private final User userWithoutPermission = - new User("johndoe@theregistrar.com", "gmail.com", "31337"); - // Marla Singer has registry lock auth permissions - private final User userWithLockPermission = - new User("Marla.Singer@crr.com", "gmail.com", "31337"); + private User userWithoutPermission; + private User userWithLockPermission; private InternetAddress outgoingAddress; private DomainBase domain; @@ -93,6 +91,8 @@ public final class RegistryLockPostActionTest { @Before public void setup() throws Exception { + userWithLockPermission = userFromRegistrarContact(AppEngineRule.makeRegistrarContact3()); + userWithoutPermission = userFromRegistrarContact(AppEngineRule.makeRegistrarContact2()); createTld("tld"); domain = persistResource(newDomainBase("example.tld")); outgoingAddress = new InternetAddress("domain-registry@example.com"); @@ -133,6 +133,18 @@ public final class RegistryLockPostActionTest { assertSuccess(response, "unlock", "johndoe@theregistrar.com"); } + @Test + public void testSuccess_linkedToContactEmail() throws Exception { + // Even though the user is some.email@gmail.com the contact is still Marla Singer + userWithLockPermission = + new User("some.email@gmail.com", "gmail.com", userWithLockPermission.getUserId()); + action = + createAction( + AuthResult.create(AuthLevel.USER, UserAuthInfo.create(userWithLockPermission, false))); + Map response = action.handleJsonRequest(lockRequest()); + assertSuccess(response, "lock", "Marla.Singer@crr.com"); + } + @Test public void testFailure_unlock_noLock() { persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); @@ -233,7 +245,7 @@ public final class RegistryLockPostActionTest { persistResource( loadRegistrar("TheRegistrar").asBuilder().setRegistryLockAllowed(false).build()); Map response = action.handleJsonRequest(lockRequest()); - assertFailureWithMessage(response, "Registry lock not allowed for this registrar"); + assertFailureWithMessage(response, "Registry lock not allowed for registrar TheRegistrar"); } @Test @@ -244,7 +256,7 @@ public final class RegistryLockPostActionTest { "clientId", "TheRegistrar", "fullyQualifiedDomainName", "example.tld", "isLock", true)); - assertFailureWithMessage(response, "Missing key for password"); + assertFailureWithMessage(response, "Incorrect registry lock password for contact"); } @Test @@ -386,9 +398,10 @@ public final class RegistryLockPostActionTest { } private RegistryLockPostAction createAction(AuthResult authResult) { + Role role = authResult.userAuthInfo().get().isUserAdmin() ? Role.ADMIN : Role.OWNER; AuthenticatedRegistrarAccessor registrarAccessor = AuthenticatedRegistrarAccessor.createForTesting( - ImmutableSetMultimap.of("TheRegistrar", Role.OWNER, "NewRegistrar", Role.OWNER)); + ImmutableSetMultimap.of("TheRegistrar", role, "NewRegistrar", role)); JsonActionRunner jsonActionRunner = new JsonActionRunner(ImmutableMap.of(), new JsonResponse(new ResponseImpl(mockResponse))); DomainLockUtils domainLockUtils = diff --git a/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java b/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java index f658546c3..2f1f9bba8 100644 --- a/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java +++ b/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java @@ -60,7 +60,8 @@ public class RegistrarConsoleScreenshotTest extends WebDriverTestCase { route("/registry-lock-verify", FrontendServlet.class)) .setFilters(ObjectifyFilter.class, OfyFilter.class) .setFixtures(BASIC) - .setEmail("Marla.Singer@crr.com") + .setEmail("Marla.Singer@crr.com") // from AppEngineRule.makeRegistrarContact3 + .setGaeUserId("12345") // from AppEngineRule.makeRegistrarContact3 .build(); @Test @@ -452,11 +453,12 @@ public class RegistrarConsoleScreenshotTest extends WebDriverTestCase { createTld("tld"); // expired unlock request DomainBase expiredUnlockRequestDomain = persistActiveDomain("expiredunlock.tld"); - saveRegistryLock(createRegistryLock(expiredUnlockRequestDomain) - .asBuilder() - .setLockCompletionTimestamp(START_OF_TIME.minusDays(1)) - .setUnlockRequestTimestamp(START_OF_TIME.minusDays(1)) - .build()); + saveRegistryLock( + createRegistryLock(expiredUnlockRequestDomain) + .asBuilder() + .setLockCompletionTimestamp(START_OF_TIME.minusDays(1)) + .setUnlockRequestTimestamp(START_OF_TIME.minusDays(1)) + .build()); DomainBase domain = persistActiveDomain("example.tld"); saveRegistryLock(createRegistryLock(domain).asBuilder().isSuperuser(true).build()); DomainBase otherDomain = persistActiveDomain("otherexample.tld"); diff --git a/core/src/test/java/google/registry/webdriver/TestServerRule.java b/core/src/test/java/google/registry/webdriver/TestServerRule.java index b2217d08a..d8b7f7191 100644 --- a/core/src/test/java/google/registry/webdriver/TestServerRule.java +++ b/core/src/test/java/google/registry/webdriver/TestServerRule.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static google.registry.testing.AppEngineRule.THE_REGISTRAR_GAE_USER_ID; import static google.registry.util.NetworkUtils.getExternalAddressOfLocalSystem; import static google.registry.util.NetworkUtils.pickUnusedPort; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -32,6 +33,7 @@ import google.registry.testing.UserInfo; import java.net.URL; import java.net.UnknownHostException; import java.nio.file.Path; +import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; @@ -66,7 +68,8 @@ public final class TestServerRule extends ExternalResource { ImmutableList routes, ImmutableList> filters, ImmutableList fixtures, - String email) { + String email, + Optional gaeUserId) { this.runfiles = runfiles; this.routes = routes; this.filters = filters; @@ -79,7 +82,8 @@ public final class TestServerRule extends ExternalResource { .withLocalModules() .withUrlFetch() .withTaskQueue() - .withUserService(UserInfo.createAdmin(email, THE_REGISTRAR_GAE_USER_ID)) + .withUserService( + UserInfo.createAdmin(email, gaeUserId.orElse(THE_REGISTRAR_GAE_USER_ID))) .build(); } @@ -149,8 +153,8 @@ public final class TestServerRule extends ExternalResource { /** * Runs arbitrary code inside server event loop thread. * - *

You should use this method when you want to do things like change Datastore, because the - * App Engine testing environment is thread-local. + *

You should use this method when you want to do things like change Datastore, because the App + * Engine testing environment is thread-local. */ public T runInAppEngineEnvironment(Callable callback) throws Throwable { FutureTask job = new FutureTask<>(callback); @@ -211,7 +215,6 @@ public final class TestServerRule extends ExternalResource { * *

This builder has three required fields: {@link #setRunfiles}, {@link #setRoutes}, and {@link * #setFilters}. - * */ public static final class Builder { private ImmutableMap runfiles; @@ -219,6 +222,7 @@ public final class TestServerRule extends ExternalResource { ImmutableList> filters; private ImmutableList fixtures = ImmutableList.of(); private String email; + private Optional gaeUserId = Optional.empty(); /** Sets the directories containing the static files for {@link TestServer}. */ public Builder setRunfiles(ImmutableMap runfiles) { @@ -256,6 +260,13 @@ public final class TestServerRule extends ExternalResource { return this; } + /** Optionally, sets the GAE user ID for the logged in user. */ + public Builder setGaeUserId(String gaeUserId) { + this.gaeUserId = + Optional.of(checkArgumentNotNull(gaeUserId, "Must specify a non-null GAE user ID")); + return this; + } + /** Returns a new {@link TestServerRule} instance. */ public TestServerRule build() { return new TestServerRule( @@ -263,7 +274,8 @@ public final class TestServerRule extends ExternalResource { checkNotNull(this.routes), checkNotNull(this.filters), checkNotNull(this.fixtures), - checkNotNull(this.email)); + checkNotNull(this.email), + this.gaeUserId); } } } diff --git a/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt b/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt index 46d6546ef..98a14402a 100644 --- a/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt +++ b/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt @@ -13,7 +13,7 @@ contacts: ADDED: {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], gaeUserId=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false} REMOVED: - {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Marla Singer, emailAddress=Marla.Singer@crr.com, phoneNumber=+1.2128675309, faxNumber=null, types=[TECH], gaeUserId=31337, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}, + {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Marla Singer, emailAddress=Marla.Singer@crr.com, phoneNumber=+1.2128675309, faxNumber=null, types=[TECH], gaeUserId=12345, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}, {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=John Doe, emailAddress=johndoe@theregistrar.com, phoneNumber=+1.1234567890, faxNumber=null, types=[ADMIN], gaeUserId=31337, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}, {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Jian-Yang, emailAddress=jyang@bachman.accelerator, phoneNumber=+1.1234567890, faxNumber=null, types=[TECH], gaeUserId=null, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false} FINAL CONTENTS: