Allow only OWNERs to change owner-related data on registrar console

The console will have 2 different "updatable things":
- only ADMINs (GAE-admins and users in the support G-Suite group) can change the things in the "admin settings" tab (currently just the allowed TLDs)
- only OWNERs can change things from the other tabs: WHOIS info, certificates, whitelisted IPs, contacts etc.

Also, all ADMINs are now OWNERS of "non-REAL" registrars. Meaning - we're only
preventing ADMINs from editing "REAL" registrars (usually in production).

Specifically, OTE registrars on sandbox are NOT "REAL", meaning ADMINS will
still be able to update them.

This only changes the backend (registrar-settings endpoint). As-is, the console
website will still make ADMINs *think* they can change everything, but if they
try - they will get an error.

Changing the frontend will happen in the next CL - because I want to get this
out this release cycle and getting JS reviewed takes a long time :(

TESTED=deployed to alpha, and saw I can't update fields even as admin on REAL
registrars, but could change it on non-REAL registrars. Also checked that I can
update the allowed TLDs on REAL registrars

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=222698270
This commit is contained in:
guyben 2018-11-24 13:57:14 -08:00 committed by jianglai
parent 5f283ebd09
commit 19b7a7b3ec
6 changed files with 443 additions and 288 deletions

View file

@ -14,6 +14,7 @@
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static google.registry.export.sheet.SyncRegistrarsSheetAction.enqueueRegistrarSheetSync;
@ -57,6 +58,7 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
@ -144,7 +146,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
ERROR, Optional.ofNullable(e.getMessage()).orElse("Unspecified error"));
} finally {
registrarConsoleMetrics.registerSettingsRequest(
clientId, op, registrarAccessor.getAllClientIdWithRoles().get(clientId), status);
clientId, op, registrarAccessor.getRolesForRegistrar(clientId), status);
}
}
@ -205,29 +207,31 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
// removed, email the changes to the contacts
ImmutableSet<RegistrarContact> contacts = registrar.getContacts();
// Update the registrar from the request.
Registrar.Builder builder = registrar.asBuilder();
Set<Role> roles = registrarAccessor.getAllClientIdWithRoles().get(clientId);
changeRegistrarFields(registrar, roles, builder, args);
Registrar updatedRegistrar = registrar;
// Do OWNER only updates to the registrar from the request.
updatedRegistrar = checkAndUpdateOwnerControlledFields(updatedRegistrar, args);
// Do ADMIN only updates to the registrar from the request.
updatedRegistrar = checkAndUpdateAdminControlledFields(updatedRegistrar, args);
// read the contacts from the request.
ImmutableSet<RegistrarContact> updatedContacts = readContacts(registrar, args);
if (!updatedContacts.isEmpty()) {
builder.setContactsRequireSyncing(true);
// Save the updated contacts
if (!updatedContacts.equals(contacts)) {
if (!registrarAccessor.hasRoleOnRegistrar(Role.OWNER, registrar.getClientId())) {
throw new ForbiddenException("Only OWNERs can update the contacts");
}
checkContactRequirements(contacts, updatedContacts);
RegistrarContact.updateContacts(updatedRegistrar, updatedContacts);
updatedRegistrar =
updatedRegistrar.asBuilder().setContactsRequireSyncing(true).build();
}
// Save the updated registrar
Registrar updatedRegistrar = builder.build();
if (!updatedRegistrar.equals(registrar)) {
ofy().save().entity(updatedRegistrar);
}
// Save the updated contacts
if (!updatedContacts.isEmpty()) {
checkContactRequirements(contacts, updatedContacts);
RegistrarContact.updateContacts(updatedRegistrar, updatedContacts);
}
// Email and return update.
sendExternalUpdatesIfNecessary(
registrar, contacts, updatedRegistrar, updatedContacts);
@ -248,12 +252,15 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
return result;
}
/** Updates a registrar builder with the supplied args from the http request; */
public static void changeRegistrarFields(
Registrar existingRegistrarObj,
Set<Role> roles,
Registrar.Builder builder,
Map<String, ?> args) {
/**
* Updates registrar with the OWNER-controlled args from the http request.
*
* <p>If any changes were made and the user isn't an OWNER - throws a {@link ForbiddenException}.
*/
private Registrar checkAndUpdateOwnerControlledFields(
Registrar initialRegistrar, Map<String, ?> args) {
Registrar.Builder builder = initialRegistrar.asBuilder();
// BILLING
RegistrarFormFields.PREMIUM_PRICE_ACK_REQUIRED
@ -261,13 +268,27 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
.ifPresent(builder::setPremiumPriceAckRequired);
// WHOIS
builder.setWhoisServer(
RegistrarFormFields.WHOIS_SERVER_FIELD.extractUntyped(args).orElse(null));
//
// Because of how whoisServer handles "default value", it's possible that setting the existing
// value will still change the Registrar. So we first check whether the value has changed.
//
// The problem is - if the Registrar has a "null" whoisServer value, the console gets the
// "default value" instead of the actual (null) value.
// This was done so we display the "default" value, but it also means that it always looks like
// the user updated the whoisServer value from "null" to the default value.
//
// TODO(b/119913848):once a null whoisServer value is sent to the console as "null", there's no
// need to check for equality before setting the value in the builder.
String updatedWhoisServer =
RegistrarFormFields.WHOIS_SERVER_FIELD.extractUntyped(args).orElse(null);
if (!Objects.equals(initialRegistrar.getWhoisServer(), updatedWhoisServer)) {
builder.setWhoisServer(updatedWhoisServer);
}
builder.setUrl(RegistrarFormFields.URL_FIELD.extractUntyped(args).orElse(null));
// If the email is already null / empty - we can keep it so. But if it's set - it's required to
// remain set.
(Strings.isNullOrEmpty(existingRegistrarObj.getEmailAddress())
(Strings.isNullOrEmpty(initialRegistrar.getEmailAddress())
? RegistrarFormFields.EMAIL_ADDRESS_FIELD_OPTIONAL
: RegistrarFormFields.EMAIL_ADDRESS_FIELD_REQUIRED)
.extractUntyped(args)
@ -294,25 +315,59 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
certificate ->
builder.setFailoverClientCertificate(certificate, ofy().getTransactionTime()));
// Update allowed TLDs only when it is modified
return checkNotChangedUnlessAllowed(builder, initialRegistrar, Role.OWNER);
}
/**
* Updates a registrar with the ADMIN-controlled args from the http request.
*
* <p>If any changes were made and the user isn't an ADMIN - throws a {@link ForbiddenException}.
*/
private Registrar checkAndUpdateAdminControlledFields(
Registrar initialRegistrar, Map<String, ?> args) {
Registrar.Builder builder = initialRegistrar.asBuilder();
Set<String> updatedAllowedTlds =
RegistrarFormFields.ALLOWED_TLDS_FIELD.extractUntyped(args).orElse(ImmutableSet.of());
if (!updatedAllowedTlds.equals(existingRegistrarObj.getAllowedTlds())) {
// Only admin is allowed to update allowed TLDs
if (!roles.contains(Role.ADMIN)) {
throw new ForbiddenException("Only admin can update allowed TLDs.");
}
// Temporarily block anyone from removing an allowed TLD.
// This is so we can start having Support users use the console in production before we finish
// implementing configurable access control.
// TODO(b/119549884): remove this code once configurable access control is implemented.
Set<String> removedTlds =
Sets.difference(existingRegistrarObj.getAllowedTlds(), updatedAllowedTlds);
if (!removedTlds.isEmpty()) {
throw new ForbiddenException("Can't remove allowed TLDs using the console.");
}
builder.setAllowedTlds(updatedAllowedTlds);
// Temporarily block anyone from removing an allowed TLD.
// This is so we can start having Support users use the console in production before we finish
// implementing configurable access control.
// TODO(b/119549884): remove this code once configurable access control is implemented.
if (!Sets.difference(initialRegistrar.getAllowedTlds(), updatedAllowedTlds).isEmpty()) {
throw new ForbiddenException("Can't remove allowed TLDs using the console.");
}
builder.setAllowedTlds(updatedAllowedTlds);
return checkNotChangedUnlessAllowed(builder, initialRegistrar, Role.ADMIN);
}
/**
* Makes sure builder.build is different than originalRegistrar only if we have the correct role.
*
* <p>On success, returns {@code builder.build()}.
*/
private Registrar checkNotChangedUnlessAllowed(
Registrar.Builder builder,
Registrar originalRegistrar,
Role allowedRole) {
Registrar updatedRegistrar = builder.build();
if (updatedRegistrar.equals(originalRegistrar)) {
return updatedRegistrar;
}
checkArgument(
updatedRegistrar.getClientId().equals(originalRegistrar.getClientId()),
"Can't change clientId (%s -> %s)",
originalRegistrar.getClientId(),
updatedRegistrar.getClientId());
if (registrarAccessor.hasRoleOnRegistrar(allowedRole, originalRegistrar.getClientId())) {
return updatedRegistrar;
}
Map<?, ?> diffs =
DiffUtils.deepDiff(
originalRegistrar.toDiffableFieldMap(),
updatedRegistrar.toDiffableFieldMap(),
true);
throw new ForbiddenException(
String.format("Unauthorized: only %s can change fields %s", allowedRole, diffs.keySet()));
}
/** Reads the contacts from the supplied args. */