mirror of
https://github.com/google/nomulus.git
synced 2025-05-01 12:37:52 +02:00
This now throws errors when a non-lower-cased, non-puny-coded, or non-canonicalized host name is passed in as an input parameter. The approach we'll take is to first notify registrars which hosts we'll be renaming, then issue EPP host update commands to effect those renames as superuser, then push this code live to production. This fixes #38 on GitHub. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138441130
312 lines
16 KiB
Java
312 lines
16 KiB
Java
// 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.flows.host;
|
|
|
|
import static com.google.common.base.MoreObjects.firstNonNull;
|
|
import static com.google.common.collect.Sets.union;
|
|
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
|
|
import static google.registry.flows.ResourceFlowUtils.checkSameValuesNotAddedAndRemoved;
|
|
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
|
|
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
|
|
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
|
|
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
|
import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
|
|
import static google.registry.flows.host.HostFlowUtils.validateHostName;
|
|
import static google.registry.flows.host.HostFlowUtils.verifyDomainIsSameRegistrar;
|
|
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
|
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
|
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
|
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.googlecode.objectify.Key;
|
|
import google.registry.dns.DnsQueue;
|
|
import google.registry.flows.EppException;
|
|
import google.registry.flows.EppException.ObjectAlreadyExistsException;
|
|
import google.registry.flows.EppException.ParameterValueRangeErrorException;
|
|
import google.registry.flows.EppException.RequiredParameterMissingException;
|
|
import google.registry.flows.EppException.StatusProhibitsOperationException;
|
|
import google.registry.flows.ExtensionManager;
|
|
import google.registry.flows.FlowModule.ClientId;
|
|
import google.registry.flows.FlowModule.Superuser;
|
|
import google.registry.flows.FlowModule.TargetId;
|
|
import google.registry.flows.TransactionalFlow;
|
|
import google.registry.flows.async.AsyncFlowEnqueuer;
|
|
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
|
|
import google.registry.model.ImmutableObject;
|
|
import google.registry.model.domain.DomainResource;
|
|
import google.registry.model.domain.metadata.MetadataExtension;
|
|
import google.registry.model.eppcommon.StatusValue;
|
|
import google.registry.model.eppinput.ResourceCommand;
|
|
import google.registry.model.eppoutput.EppResponse;
|
|
import google.registry.model.host.HostCommand.Update;
|
|
import google.registry.model.host.HostCommand.Update.AddRemove;
|
|
import google.registry.model.host.HostCommand.Update.Change;
|
|
import google.registry.model.host.HostResource;
|
|
import google.registry.model.index.ForeignKeyIndex;
|
|
import google.registry.model.reporting.HistoryEntry;
|
|
import java.util.Objects;
|
|
import javax.inject.Inject;
|
|
import org.joda.time.DateTime;
|
|
|
|
/**
|
|
* An EPP flow that updates a host.
|
|
*
|
|
* <p>Hosts can be "external", or "internal" (also known as "in bailiwick"). Internal hosts are
|
|
* those that are under a top level domain within this registry, and external hosts are all other
|
|
* hosts. Internal hosts must have at least one IP address associated with them, whereas external
|
|
* hosts cannot have any.
|
|
*
|
|
* <p>This flow allows changing a host name, and adding or removing IP addresses to hosts. When
|
|
* a host is renamed from internal to external all IP addresses must be simultaneously removed, and
|
|
* when it is renamed from external to internal at least one must be added. If the host is renamed
|
|
* or IP addresses are added, tasks are enqueued to update DNS accordingly.
|
|
*
|
|
* @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException}
|
|
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
|
|
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
|
|
* @error {@link google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException}
|
|
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
|
|
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
|
|
* @error {@link HostFlowUtils.HostNameTooShallowException}
|
|
* @error {@link HostFlowUtils.InvalidHostNameException}
|
|
* @error {@link HostFlowUtils.HostNameNotLowerCaseException}
|
|
* @error {@link HostFlowUtils.HostNameNotNormalizedException}
|
|
* @error {@link HostFlowUtils.HostNameNotPunyCodedException}
|
|
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
|
|
* @error {@link CannotAddIpToExternalHostException}
|
|
* @error {@link CannotRemoveSubordinateHostLastIpException}
|
|
* @error {@link HostAlreadyExistsException}
|
|
* @error {@link RenameHostToExternalRemoveIpException}
|
|
* @error {@link RenameHostToSubordinateRequiresIpException}
|
|
*/
|
|
public final class HostUpdateFlow implements TransactionalFlow {
|
|
|
|
/**
|
|
* Note that CLIENT_UPDATE_PROHIBITED is intentionally not in this list. This is because it
|
|
* requires special checking, since you must be able to clear the status off the object with an
|
|
* update.
|
|
*/
|
|
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
|
|
StatusValue.PENDING_DELETE,
|
|
StatusValue.SERVER_UPDATE_PROHIBITED);
|
|
|
|
@Inject ResourceCommand resourceCommand;
|
|
@Inject ExtensionManager extensionManager;
|
|
@Inject @ClientId String clientId;
|
|
@Inject @TargetId String targetId;
|
|
@Inject @Superuser boolean isSuperuser;
|
|
@Inject HistoryEntry.Builder historyBuilder;
|
|
@Inject AsyncFlowEnqueuer asyncFlowEnqueuer;
|
|
@Inject DnsQueue dnsQueue;
|
|
@Inject EppResponse.Builder responseBuilder;
|
|
@Inject HostUpdateFlow() {}
|
|
|
|
@Override
|
|
public final EppResponse run() throws EppException {
|
|
extensionManager.register(MetadataExtension.class);
|
|
extensionManager.validate();
|
|
validateClientIsLoggedIn(clientId);
|
|
Update command = (Update) resourceCommand;
|
|
Change change = command.getInnerChange();
|
|
String suppliedNewHostName = change.getFullyQualifiedHostName();
|
|
DateTime now = ofy().getTransactionTime();
|
|
// Validation is disabled for superusers to allow renaming of existing invalid hostnames.
|
|
// TODO(b/32328995): Remove superuser override once all bad data in prod has been fixed.
|
|
if (!isSuperuser) {
|
|
validateHostName(targetId);
|
|
}
|
|
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
|
|
boolean isHostRename = suppliedNewHostName != null;
|
|
String oldHostName = targetId;
|
|
String newHostName = firstNonNull(suppliedNewHostName, oldHostName);
|
|
Optional<DomainResource> superordinateDomain =
|
|
Optional.fromNullable(lookupSuperordinateDomain(validateHostName(newHostName), now));
|
|
verifyUpdateAllowed(command, existingHost, superordinateDomain.orNull());
|
|
if (isHostRename && loadAndGetKey(HostResource.class, newHostName, now) != null) {
|
|
throw new HostAlreadyExistsException(newHostName);
|
|
}
|
|
AddRemove add = command.getInnerAdd();
|
|
AddRemove remove = command.getInnerRemove();
|
|
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
|
|
checkSameValuesNotAddedAndRemoved(add.getInetAddresses(), remove.getInetAddresses());
|
|
HostResource newHost = existingHost.asBuilder()
|
|
.setFullyQualifiedHostName(newHostName)
|
|
.addStatusValues(add.getStatusValues())
|
|
.removeStatusValues(remove.getStatusValues())
|
|
.addInetAddresses(add.getInetAddresses())
|
|
.removeInetAddresses(remove.getInetAddresses())
|
|
.setLastEppUpdateTime(now)
|
|
.setLastEppUpdateClientId(clientId)
|
|
// The superordinateDomain can be missing if the new name is external.
|
|
// Note that the value of superordinateDomain is projected to the current time inside of
|
|
// the lookupSuperordinateDomain(...) call above, so that it will never be stale.
|
|
.setSuperordinateDomain(
|
|
superordinateDomain.isPresent() ? Key.create(superordinateDomain.get()) : null)
|
|
.setLastSuperordinateChange(superordinateDomain == null ? null : now)
|
|
.build()
|
|
// Rely on the host's cloneProjectedAtTime() method to handle setting of transfer data.
|
|
.cloneProjectedAtTime(now);
|
|
verifyHasIpsIffIsExternal(command, existingHost, newHost);
|
|
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
|
|
entitiesToSave.add(newHost);
|
|
// Keep the {@link ForeignKeyIndex} for this host up to date.
|
|
if (isHostRename) {
|
|
// Update the foreign key for the old host name and save one for the new host name.
|
|
entitiesToSave.add(
|
|
ForeignKeyIndex.create(existingHost, now),
|
|
ForeignKeyIndex.create(newHost, newHost.getDeletionTime()));
|
|
updateSuperordinateDomains(existingHost, newHost);
|
|
}
|
|
enqueueTasks(existingHost, newHost);
|
|
entitiesToSave.add(historyBuilder
|
|
.setType(HistoryEntry.Type.HOST_UPDATE)
|
|
.setModificationTime(now)
|
|
.setParent(Key.create(existingHost))
|
|
.build());
|
|
ofy().save().entities(entitiesToSave.build());
|
|
return responseBuilder.build();
|
|
}
|
|
|
|
private void verifyUpdateAllowed(
|
|
Update command, HostResource existingResource, DomainResource superordinateDomain)
|
|
throws EppException {
|
|
if (!isSuperuser) {
|
|
verifyResourceOwnership(clientId, existingResource);
|
|
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
|
|
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
|
|
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
|
|
// status, then the update must be disallowed (unless a superuser is requesting the change).
|
|
if (!isSuperuser
|
|
&& existingResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
|
|
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
|
|
throw new ResourceHasClientUpdateProhibitedException();
|
|
}
|
|
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
|
|
}
|
|
verifyDomainIsSameRegistrar(superordinateDomain, clientId);
|
|
verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES);
|
|
}
|
|
|
|
private void verifyHasIpsIffIsExternal(
|
|
Update command, HostResource existingResource, HostResource newResource) throws EppException {
|
|
boolean wasExternal = existingResource.getSuperordinateDomain() == null;
|
|
boolean wasSubordinate = !wasExternal;
|
|
boolean willBeExternal = newResource.getSuperordinateDomain() == null;
|
|
boolean willBeSubordinate = !willBeExternal;
|
|
boolean newResourceHasIps = !isNullOrEmpty(newResource.getInetAddresses());
|
|
boolean commandAddsIps = !isNullOrEmpty(command.getInnerAdd().getInetAddresses());
|
|
// These checks are order-dependent. For example a subordinate-to-external rename that adds new
|
|
// ips should hit the first exception, whereas one that only fails to remove the existing ips
|
|
// should hit the second.
|
|
if (willBeExternal && commandAddsIps) {
|
|
throw new CannotAddIpToExternalHostException();
|
|
}
|
|
if (wasSubordinate && willBeExternal && newResourceHasIps) {
|
|
throw new RenameHostToExternalRemoveIpException();
|
|
}
|
|
if (wasExternal && willBeSubordinate && !commandAddsIps) {
|
|
throw new RenameHostToSubordinateRequiresIpException();
|
|
}
|
|
if (willBeSubordinate && !newResourceHasIps) {
|
|
throw new CannotRemoveSubordinateHostLastIpException();
|
|
}
|
|
}
|
|
|
|
private void enqueueTasks(HostResource existingResource, HostResource newResource) {
|
|
// Only update DNS for subordinate hosts. External hosts have no glue to write, so they
|
|
// are only written as NS records from the referencing domain.
|
|
if (existingResource.getSuperordinateDomain() != null) {
|
|
dnsQueue.addHostRefreshTask(existingResource.getFullyQualifiedHostName());
|
|
}
|
|
// In case of a rename, there are many updates we need to queue up.
|
|
if (((Update) resourceCommand).getInnerChange().getFullyQualifiedHostName() != null) {
|
|
// If the renamed host is also subordinate, then we must enqueue an update to write the new
|
|
// glue.
|
|
if (newResource.getSuperordinateDomain() != null) {
|
|
dnsQueue.addHostRefreshTask(newResource.getFullyQualifiedHostName());
|
|
}
|
|
// We must also enqueue updates for all domains that use this host as their nameserver so
|
|
// that their NS records can be updated to point at the new name.
|
|
asyncFlowEnqueuer.enqueueAsyncDnsRefresh(existingResource);
|
|
}
|
|
}
|
|
|
|
private void updateSuperordinateDomains(HostResource existingResource, HostResource newResource) {
|
|
Key<DomainResource> oldSuperordinateDomain = existingResource.getSuperordinateDomain();
|
|
Key<DomainResource> newSuperordinateDomain = newResource.getSuperordinateDomain();
|
|
if (oldSuperordinateDomain != null || newSuperordinateDomain != null) {
|
|
if (Objects.equals(oldSuperordinateDomain, newSuperordinateDomain)) {
|
|
ofy().save().entity(
|
|
ofy().load().key(oldSuperordinateDomain).now().asBuilder()
|
|
.removeSubordinateHost(existingResource.getFullyQualifiedHostName())
|
|
.addSubordinateHost(newResource.getFullyQualifiedHostName())
|
|
.build());
|
|
} else {
|
|
if (oldSuperordinateDomain != null) {
|
|
ofy().save().entity(
|
|
ofy().load().key(oldSuperordinateDomain).now()
|
|
.asBuilder()
|
|
.removeSubordinateHost(existingResource.getFullyQualifiedHostName())
|
|
.build());
|
|
}
|
|
if (newSuperordinateDomain != null) {
|
|
ofy().save().entity(
|
|
ofy().load().key(newSuperordinateDomain).now()
|
|
.asBuilder()
|
|
.addSubordinateHost(newResource.getFullyQualifiedHostName())
|
|
.build());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Host with specified name already exists. */
|
|
static class HostAlreadyExistsException extends ObjectAlreadyExistsException {
|
|
public HostAlreadyExistsException(String hostName) {
|
|
super(String.format("Object with given ID (%s) already exists", hostName));
|
|
}
|
|
}
|
|
|
|
/** Cannot add IP addresses to an external host. */
|
|
static class CannotAddIpToExternalHostException extends ParameterValueRangeErrorException {
|
|
public CannotAddIpToExternalHostException() {
|
|
super("Cannot add IP addresses to external hosts");
|
|
}
|
|
}
|
|
|
|
/** Cannot remove all IP addresses from a subordinate host. */
|
|
static class CannotRemoveSubordinateHostLastIpException
|
|
extends StatusProhibitsOperationException {
|
|
public CannotRemoveSubordinateHostLastIpException() {
|
|
super("Cannot remove all IP addresses from a subordinate host");
|
|
}
|
|
}
|
|
|
|
/** Host rename from external to subordinate must also add an IP addresses. */
|
|
static class RenameHostToSubordinateRequiresIpException
|
|
extends RequiredParameterMissingException {
|
|
public RenameHostToSubordinateRequiresIpException() {
|
|
super("Host rename from external to subordinate must also add an IP address");
|
|
}
|
|
}
|
|
|
|
/** Host rename from subordinate to external must also remove all IP addresses. */
|
|
static class RenameHostToExternalRemoveIpException extends ParameterValueRangeErrorException {
|
|
public RenameHostToExternalRemoveIpException() {
|
|
super("Host rename from subordinate to external must also remove all IP addresses");
|
|
}
|
|
}
|
|
}
|