Remove email-editing footgun (#503)

* Remove email-editing footgun

Email address is used as the primary key so we should be very careful
about changing it. This will have even more importance when this is the
location to which we will be sending registry lock confirmation emails.

Note: we allow addition or removal of contacts through the UI (and don't
want to disable that) and because all edits are performed by saving the
entire list of contacts, we can't explicitly prevent all possible edits
of email address in the backend. So this doesn't technically prevent
anything security-wise, but it makes it much more difficult to
accidentally edit an email when you shouldn't.

* Enforce non-deletion of registry-lock-enabled contacts

* Fix tests

* Specify contact
This commit is contained in:
gbrodman 2020-04-29 11:44:51 -04:00 committed by GitHub
parent aa0dcea537
commit c361c9e601
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 52 deletions

View file

@ -66,13 +66,12 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
@Test
public void testPost_updateContacts_success() throws Exception {
// Remove all the contacts but the first by updating with list of
// just it.
// Remove all the contacts but one by updating with a list of just it
ImmutableMap<String, String> adminContact1 =
ImmutableMap.of(
"name", "contact1",
"emailAddress", "contact1@email.com",
"phoneNumber", "+1.2125650001",
"name", "Marla Singer",
"emailAddress", "Marla.Singer@crr.com",
"phoneNumber", "+1.2128675309",
// Have to keep ADMIN or else expect FormException for at-least-one.
"types", "ADMIN,TECH");
@ -83,15 +82,12 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", regMap));
assertThat(response).containsEntry("status", "SUCCESS");
RegistrarContact newContact =
new RegistrarContact.Builder()
.setParent(registrar)
.setName(adminContact1.get("name"))
.setEmailAddress(adminContact1.get("emailAddress"))
.setPhoneNumber(adminContact1.get("phoneNumber"))
.setTypes(ImmutableList.of(Type.ADMIN, Type.TECH))
.build();
assertThat(loadRegistrar(CLIENT_ID).getContacts()).containsExactly(newContact);
RegistrarContact foundContact =
Iterables.getOnlyElement(loadRegistrar(CLIENT_ID).getContacts());
assertThat(foundContact.getName()).isEqualTo(adminContact1.get("name"));
assertThat(foundContact.getEmailAddress()).isEqualTo(adminContact1.get("emailAddress"));
assertThat(foundContact.getPhoneNumber()).isEqualTo(adminContact1.get("phoneNumber"));
assertThat(foundContact.getTypes()).containsExactly(Type.ADMIN, Type.TECH);
assertMetric(CLIENT_ID, "update", "[OWNER]", "SUCCESS");
verifyNotificationEmailsSent();
}
@ -188,41 +184,65 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
@Test
public void testSuccess_setRegistryLockPassword() {
addPasswordToTechContact();
techContact = Iterables.getOnlyElement(loadRegistrar(CLIENT_ID).getContactsOfType(Type.TECH));
assertThat(techContact.verifyRegistryLockPassword("hellothere")).isTrue();
addPasswordToContactTwo();
String emailAddress = AppEngineRule.makeRegistrarContact2().getEmailAddress();
RegistrarContact newContactWithPassword =
loadRegistrar(CLIENT_ID).getContacts().stream()
.filter(rc -> rc.getEmailAddress().equals(emailAddress))
.findFirst()
.get();
assertThat(newContactWithPassword.verifyRegistryLockPassword("hellothere")).isTrue();
assertMetric(CLIENT_ID, "update", "[OWNER]", "SUCCESS");
}
@Test
public void testSuccess_setRegistryLockPassword_notOverriddenLater() {
addPasswordToTechContact();
techContact = Iterables.getOnlyElement(loadRegistrar(CLIENT_ID).getContactsOfType(Type.TECH));
assertThat(techContact.verifyRegistryLockPassword("hellothere")).isTrue();
addPasswordToContactTwo();
String emailAddress = AppEngineRule.makeRegistrarContact2().getEmailAddress();
RegistrarContact newContactWithPassword =
loadRegistrar(CLIENT_ID).getContacts().stream()
.filter(rc -> rc.getEmailAddress().equals(emailAddress))
.findFirst()
.get();
assertThat(newContactWithPassword.verifyRegistryLockPassword("hellothere")).isTrue();
techContact = Iterables.getOnlyElement(loadRegistrar(CLIENT_ID).getContactsOfType(Type.TECH));
Map<String, Object> techContactMap = techContact.toJsonMap();
techContactMap.put("name", "Some Other Name");
Map<String, Object> newContactMap = newContactWithPassword.toJsonMap();
newContactMap.put("name", "Some Other Name");
Map<String, Object> reqJson = loadRegistrar(CLIENT_ID).toJsonMap();
reqJson.put(
"contacts",
ImmutableList.of(AppEngineRule.makeRegistrarContact2().toJsonMap(), techContactMap));
ImmutableList.of(
AppEngineRule.makeRegistrarContact1().toJsonMap(),
newContactMap,
AppEngineRule.makeRegistrarContact3().toJsonMap()));
Map<String, Object> response =
action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", reqJson));
assertThat(response).containsAtLeastEntriesIn(ImmutableMap.of("status", "SUCCESS"));
techContact = Iterables.getOnlyElement(loadRegistrar(CLIENT_ID).getContactsOfType(Type.TECH));
assertThat(techContact.verifyRegistryLockPassword("hellothere")).isTrue();
newContactWithPassword =
loadRegistrar(CLIENT_ID).getContacts().stream()
.filter(rc -> rc.getEmailAddress().equals(emailAddress))
.findFirst()
.get();
assertThat(newContactWithPassword.verifyRegistryLockPassword("hellothere")).isTrue();
}
private void addPasswordToTechContact() {
techContact =
persistResource(techContact.asBuilder().setAllowedToSetRegistryLockPassword(true).build());
Map<String, Object> contactMap = techContact.toJsonMap();
private void addPasswordToContactTwo() {
RegistrarContact contact =
persistResource(
AppEngineRule.makeRegistrarContact2()
.asBuilder()
.setRegistryLockEmailAddress("johndoe@theregistrar.com")
.setAllowedToSetRegistryLockPassword(true)
.build());
Map<String, Object> contactMap = contact.toJsonMap();
contactMap.put("registryLockPassword", "hellothere");
Map<String, Object> reqJson = loadRegistrar(CLIENT_ID).toJsonMap();
reqJson.put(
"contacts",
ImmutableList.of(AppEngineRule.makeRegistrarContact2().toJsonMap(), contactMap));
ImmutableList.of(
AppEngineRule.makeRegistrarContact1().toJsonMap(),
contactMap,
AppEngineRule.makeRegistrarContact3().toJsonMap()));
Map<String, Object> response =
action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", reqJson));
assertThat(response).containsAtLeastEntriesIn(ImmutableMap.of("status", "SUCCESS"));
@ -260,13 +280,16 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
public void testPost_failure_setRegistryLockPassword_notAllowed() {
// "allowedToSetRegistryLockPassword" must be set through the back end first
// before we can set a password through the UI
Map<String, Object> contactMap =
techContact.asBuilder().setAllowedToSetRegistryLockPassword(true).build().toJsonMap();
Map<String, Object> contactMap = AppEngineRule.makeRegistrarContact2().toJsonMap();
contactMap.put("allowedToSetRegistryLockPassword", true);
contactMap.put("registryLockPassword", "hellothere");
Map<String, Object> reqJson = loadRegistrar(CLIENT_ID).toJsonMap();
reqJson.put(
"contacts",
ImmutableList.of(AppEngineRule.makeRegistrarContact2().toJsonMap(), contactMap));
ImmutableList.of(
AppEngineRule.makeRegistrarContact1().toJsonMap(),
contactMap,
AppEngineRule.makeRegistrarContact3().toJsonMap()));
Map<String, Object> response =
action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", reqJson));
@ -304,6 +327,30 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
assertMetric(CLIENT_ID, "update", "[OWNER]", "ERROR: FormException");
}
@Test
public void testPost_failure_removingRegistryLockContact() {
ImmutableMap<String, String> contact =
ImmutableMap.of(
"name", "contact1",
"emailAddress", "contact1@email.com",
"phoneNumber", "+1.2125650001",
// Have to keep ADMIN or else expect FormException for at-least-one.
"types", "ADMIN,TECH");
Map<String, Object> regMap = loadRegistrar(CLIENT_ID).toJsonMap();
regMap.put("contacts", ImmutableList.of(contact));
Map<String, Object> response =
action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", regMap));
assertThat(response)
.containsExactly(
"status",
"ERROR",
"results",
ImmutableList.of(),
"message",
"Cannot delete the contact Marla.Singer@crr.com that has registry lock enabled");
assertMetric(CLIENT_ID, "update", "[OWNER]", "ERROR: FormException");
}
@Test
public void testPost_failure_setRegistryLock_passwordTooShort() {
techContact =

View file

@ -14,6 +14,7 @@
package google.registry.ui.server.registrar;
import static com.google.appengine.repackaged.com.google.common.collect.Iterables.getOnlyElement;
import static com.google.monitoring.metrics.contrib.LongMetricSubject.assertThat;
import static google.registry.config.RegistryConfig.getGSuiteOutgoingEmailAddress;
import static google.registry.config.RegistryConfig.getGSuiteOutgoingEmailDisplayName;
@ -23,14 +24,12 @@ import static google.registry.security.JsonHttpTestUtils.createJsonPayload;
import static google.registry.testing.DatastoreHelper.createTlds;
import static google.registry.testing.DatastoreHelper.disallowRegistrarAccess;
import static google.registry.testing.DatastoreHelper.loadRegistrar;
import static google.registry.testing.DatastoreHelper.persistResource;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.truth.Truth;
import google.registry.model.ofy.Ofy;
@ -95,21 +94,13 @@ public abstract class RegistrarSettingsActionTestCase {
// Add a technical contact to the registrar (in addition to the default admin contact created by
// AppEngineRule).
techContact =
persistResource(
new RegistrarContact.Builder()
.setParent(loadRegistrar(CLIENT_ID))
.setName("Jian-Yang")
.setEmailAddress("jyang@bachman.accelerator")
.setRegistryLockEmailAddress("jyang.registrylock@bachman.accelerator")
.setPhoneNumber("+1.1234567890")
.setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
.build());
getOnlyElement(loadRegistrar(CLIENT_ID).getContactsOfType(RegistrarContact.Type.TECH));
action.registrarAccessor = null;
action.appEngineServiceUtils = appEngineServiceUtils;
when(appEngineServiceUtils.getCurrentVersionHostname("backend")).thenReturn("backend.hostname");
action.jsonActionRunner = new JsonActionRunner(
ImmutableMap.of(), new JsonResponse(new ResponseImpl(rsp)));
action.jsonActionRunner =
new JsonActionRunner(ImmutableMap.of(), new JsonResponse(new ResponseImpl(rsp)));
action.sendEmailUtils =
new SendEmailUtils(
getGSuiteOutgoingEmailAddress(),

View file

@ -18,6 +18,17 @@
"emailAddress": "etphonehome@example.com",
"gaeUserId": null,
"types": "ADMIN,BILLING,TECH,WHOIS"
},
{
"visibleInWhoisAsAdmin": false,
"faxNumber": null,
"phoneNumber": "+1.2128675309",
"name": "Marla Singer",
"visibleInWhoisAsTech": false,
"emailAddress": "Marla.Singer@crr.com",
"registryLockEmailAddress": "Marla.Singer.RegistryLock@crr.com",
"gaeUserId": "12345",
"types": "TECH"
}
],
"allowedTlds": [

View file

@ -13,8 +13,7 @@ contacts:
ADDED:
{parent=Key<?>(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, registryLockEmailAddress=null, 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, registryLockEmailAddress=Marla.Singer.RegistryLock@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, registryLockEmailAddress=null, 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, registryLockEmailAddress=jyang.registrylock@bachman.accelerator, phoneNumber=+1.1234567890, faxNumber=null, types=[TECH], gaeUserId=null, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
{parent=Key<?>(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=John Doe, emailAddress=johndoe@theregistrar.com, registryLockEmailAddress=null, phoneNumber=+1.1234567890, faxNumber=null, types=[ADMIN], gaeUserId=31337, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
FINAL CONTENTS:
{parent=Key<?>(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, registryLockEmailAddress=null, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], gaeUserId=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
{parent=Key<?>(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, registryLockEmailAddress=null, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], gaeUserId=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false},
{parent=Key<?>(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Marla Singer, emailAddress=Marla.Singer@crr.com, registryLockEmailAddress=Marla.Singer.RegistryLock@crr.com, phoneNumber=+1.2128675309, faxNumber=null, types=[TECH], gaeUserId=12345, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Before After
Before After