google.registry.ui.server.registrar.RegistrarSettingsAction.java Source code

Java tutorial

Introduction

Here is the source code for google.registry.ui.server.registrar.RegistrarSettingsAction.java

Source

// Copyright 2016 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.ui.server.registrar;

import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Sets.difference;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.googlecode.objectify.Work;
import google.registry.config.RegistryConfig.Config;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarContact;
import google.registry.request.Action;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.JsonActionRunner;
import google.registry.security.JsonResponseHelper;
import google.registry.ui.forms.FormException;
import google.registry.ui.forms.FormFieldException;
import google.registry.ui.server.RegistrarFormFields;
import google.registry.util.CidrAddressBlock;
import google.registry.util.CollectionUtils;
import google.registry.util.DiffUtils;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

/**
 * Admin servlet that allows creating or updating a registrar. Deletes are not allowed so as to
 * preserve history.
 */
@Action(path = RegistrarSettingsAction.PATH, requireLogin = true, xsrfProtection = true, xsrfScope = "console", method = Action.Method.POST)
public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonAction {

    public static final String PATH = "/registrar-settings";

    private static final String OP_PARAM = "op";
    private static final String ARGS_PARAM = "args";

    @Inject
    HttpServletRequest request;
    @Inject
    JsonActionRunner jsonActionRunner;
    @Inject
    Registrar initialRegistrar;
    @Inject
    SendEmailUtils sendEmailUtils;
    @Inject
    SessionUtils sessionUtils;
    @Inject
    @Config("registrarChangesNotificationEmailAddresses")
    ImmutableList<String> registrarChangesNotificationEmailAddresses;

    @Inject
    RegistrarSettingsAction() {
    }

    private static final Predicate<RegistrarContact> HAS_PHONE = new Predicate<RegistrarContact>() {
        @Override
        public boolean apply(RegistrarContact contact) {
            return contact.getPhoneNumber() != null;
        }
    };

    @Override
    public void run() {
        jsonActionRunner.run(this);
    }

    @Override
    public Map<String, Object> handleJsonRequest(Map<String, ?> input) {
        if (input == null) {
            throw new BadRequestException("Malformed JSON");
        }

        if (!sessionUtils.checkRegistrarConsoleLogin(request)) {
            return JsonResponseHelper.create(ERROR, "Not authorized to access Registrar Console");
        }

        // Process the operation.  Though originally derived from a CRUD
        // handlder, registrar-settings really only supports read and update.
        String op = Optional.fromNullable((String) input.get(OP_PARAM)).or("read");
        @SuppressWarnings("unchecked")
        Map<String, ?> args = (Map<String, Object>) Optional.<Object>fromNullable(input.get(ARGS_PARAM))
                .or(ImmutableMap.of());
        try {
            switch (op) {
            case "update":
                return update(args, initialRegistrar);
            case "read":
                return JsonResponseHelper.create(SUCCESS, "Success", initialRegistrar.toJsonMap());
            default:
                return JsonResponseHelper.create(ERROR, "Unknown or unsupported operation: " + op);
            }
        } catch (FormFieldException e) {
            return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName());
        } catch (FormException ee) {
            return JsonResponseHelper.create(ERROR, ee.getMessage());
        }

    }

    Map<String, Object> update(final Map<String, ?> args, final Registrar registrar) {
        final String clientId = sessionUtils.getRegistrarClientId(request);
        return ofy().transact(new Work<Map<String, Object>>() {
            @Override
            public Map<String, Object> run() {
                ImmutableSet<RegistrarContact> oldContacts = registrar.getContacts();
                Map<String, Object> existingRegistrarMap = expandRegistrarWithContacts(oldContacts, registrar);
                Registrar.Builder builder = registrar.asBuilder();
                ImmutableSet<RegistrarContact> updatedContacts = changeRegistrarFields(registrar, builder, args);
                if (!updatedContacts.isEmpty()) {
                    builder.setContactsRequireSyncing(true);
                }
                Registrar updatedRegistrar = builder.build();
                ofy().save().entity(updatedRegistrar);
                if (!updatedContacts.isEmpty()) {
                    checkContactRequirements(oldContacts, updatedContacts);
                    RegistrarContact.updateContacts(updatedRegistrar, updatedContacts);
                }
                // Update the registrar map with updated contacts to bypass Objectify caching issues that
                // come into play with calling getContacts().
                Map<String, Object> updatedRegistrarMap = expandRegistrarWithContacts(updatedContacts,
                        updatedRegistrar);
                sendExternalUpdatesIfNecessary(updatedRegistrar.getRegistrarName(), existingRegistrarMap,
                        updatedRegistrarMap);
                return JsonResponseHelper.create(SUCCESS, "Saved " + clientId, updatedRegistrar.toJsonMap());
            }
        });
    }

    private Map<String, Object> expandRegistrarWithContacts(Iterable<RegistrarContact> contacts,
            Registrar registrar) {
        ImmutableSet<Map<String, Object>> expandedContacts = FluentIterable.from(contacts)
                .transform(new Function<RegistrarContact, Map<String, Object>>() {
                    @Override
                    public Map<String, Object> apply(RegistrarContact contact) {
                        return contact.toDiffableFieldMap();
                    }
                }).toSet();
        // Use LinkedHashMap here to preserve ordering; null values mean we can't use ImmutableMap.
        LinkedHashMap<String, Object> result = new LinkedHashMap<>();
        result.putAll(registrar.toDiffableFieldMap());
        result.put("contacts", expandedContacts);
        return result;
    }

    /**
     * Updates a registrar builder with the supplied args from the http request, and returns a list of
     * the new registrar contacts.
     */
    public static ImmutableSet<RegistrarContact> changeRegistrarFields(Registrar existingRegistrarObj,
            Registrar.Builder builder, Map<String, ?> args) {

        // WHOIS
        builder.setWhoisServer(RegistrarFormFields.WHOIS_SERVER_FIELD.extractUntyped(args).orNull());
        builder.setReferralUrl(RegistrarFormFields.REFERRAL_URL_FIELD.extractUntyped(args).orNull());
        for (String email : RegistrarFormFields.EMAIL_ADDRESS_FIELD.extractUntyped(args).asSet()) {
            builder.setEmailAddress(email);
        }
        builder.setPhoneNumber(RegistrarFormFields.PHONE_NUMBER_FIELD.extractUntyped(args).orNull());
        builder.setFaxNumber(RegistrarFormFields.FAX_NUMBER_FIELD.extractUntyped(args).orNull());
        builder.setLocalizedAddress(RegistrarFormFields.L10N_ADDRESS_FIELD.extractUntyped(args).orNull());

        // Security
        builder.setIpAddressWhitelist(RegistrarFormFields.IP_ADDRESS_WHITELIST_FIELD.extractUntyped(args)
                .or(ImmutableList.<CidrAddressBlock>of()));
        for (String certificate : RegistrarFormFields.CLIENT_CERTIFICATE_FIELD.extractUntyped(args).asSet()) {
            builder.setClientCertificate(certificate, ofy().getTransactionTime());
        }
        for (String certificate : RegistrarFormFields.FAILOVER_CLIENT_CERTIFICATE_FIELD.extractUntyped(args)
                .asSet()) {
            builder.setFailoverClientCertificate(certificate, ofy().getTransactionTime());
        }

        builder.setUrl(RegistrarFormFields.URL_FIELD.extractUntyped(args).orNull());
        builder.setReferralUrl(RegistrarFormFields.REFERRAL_URL_FIELD.extractUntyped(args).orNull());

        // Contact
        ImmutableSet.Builder<RegistrarContact> contacts = new ImmutableSet.Builder<>();
        for (RegistrarContact.Builder contactBuilder : concat(
                RegistrarFormFields.CONTACTS_FIELD.extractUntyped(args).asSet())) {
            contacts.add(contactBuilder.setParent(existingRegistrarObj).build());
        }

        return contacts.build();
    }

    /**
     * Enforces business logic checks on registrar contacts.
     *
     * @throws FormException if the checks fail.
     */
    void checkContactRequirements(Set<RegistrarContact> existingContacts, Set<RegistrarContact> updatedContacts) {
        // Check that no two contacts use the same email address.
        Set<String> emails = new HashSet<>();
        for (RegistrarContact contact : updatedContacts) {
            if (!emails.add(contact.getEmailAddress())) {
                throw new ContactRequirementException(String.format(
                        "One email address (%s) cannot be used for multiple contacts", contact.getEmailAddress()));
            }
        }
        // Check that required contacts don't go away, once they are set.
        Multimap<RegistrarContact.Type, RegistrarContact> oldContactsByType = HashMultimap.create();
        for (RegistrarContact contact : existingContacts) {
            for (RegistrarContact.Type t : contact.getTypes()) {
                oldContactsByType.put(t, contact);
            }
        }
        Multimap<RegistrarContact.Type, RegistrarContact> newContactsByType = HashMultimap.create();
        for (RegistrarContact contact : updatedContacts) {
            for (RegistrarContact.Type t : contact.getTypes()) {
                newContactsByType.put(t, contact);
            }
        }
        for (RegistrarContact.Type t : difference(oldContactsByType.keySet(), newContactsByType.keySet())) {
            if (t.isRequired()) {
                throw new ContactRequirementException(t);
            }
        }
        // Ensure at least one tech contact has a phone number if one was present before.
        if (any(oldContactsByType.get(RegistrarContact.Type.TECH), HAS_PHONE)
                && !any(newContactsByType.get(RegistrarContact.Type.TECH), HAS_PHONE)) {
            throw new ContactRequirementException(String.format("At least one %s contact must have a phone number",
                    RegistrarContact.Type.TECH.getDisplayName()));
        }
    }

    /**
     * Determines if any changes were made to the registrar besides the lastUpdateTime, and if so,
     * sends an email with a diff of the changes to the configured notification email address and
     * enqueues a task to re-sync the registrar sheet.
     */
    private void sendExternalUpdatesIfNecessary(String registrarName, Map<String, Object> existingRegistrar,
            Map<String, Object> updatedRegistrar) {
        Map<?, ?> diffs = DiffUtils.deepDiff(existingRegistrar, updatedRegistrar, true);
        @SuppressWarnings("unchecked")
        Set<String> changedKeys = (Set<String>) diffs.keySet();
        if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) {
            return;
        }
        SyncRegistrarsSheetAction.enqueueBackendTask();
        if (!registrarChangesNotificationEmailAddresses.isEmpty()) {
            sendEmailUtils.sendEmail(registrarChangesNotificationEmailAddresses,
                    String.format("Registrar %s updated", registrarName),
                    "The following changes were made to the registrar:\n"
                            + DiffUtils.prettyPrintDiffedMap(diffs, null));
        }
    }

    /** Thrown when a set of contacts doesn't meet certain constraints. */
    private static class ContactRequirementException extends FormException {
        ContactRequirementException(String msg) {
            super(msg);
        }

        ContactRequirementException(RegistrarContact.Type type) {
            super("Must have at least one " + type.getDisplayName() + " contact");
        }
    }
}