diff --git a/docs/flows.md b/docs/flows.md index 9039010bd..4020508cf 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -1009,6 +1009,7 @@ are enqueued to update DNS accordingly. * This resource has clientUpdateProhibited on it, and the update does not clear that status. * Resource status prohibits this operation. + * Superordinate domain for this hostname is in pending delete. * Cannot remove all IP addresses from a subordinate host. * Cannot rename an external host. * 2306 @@ -1094,6 +1095,8 @@ allows creating a host name, and if necessary enqueues tasks to update DNS. * Resource with this id already exists. * 2303 * Superordinate domain for this hostname does not exist. +* 2304 + * Superordinate domain for this hostname is in pending delete. * 2306 * Host names must be at least two levels below the public suffix. diff --git a/java/google/registry/flows/host/HostCreateFlow.java b/java/google/registry/flows/host/HostCreateFlow.java index 9eed8a911..f85be62ba 100644 --- a/java/google/registry/flows/host/HostCreateFlow.java +++ b/java/google/registry/flows/host/HostCreateFlow.java @@ -18,6 +18,7 @@ import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist; import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain; import static google.registry.flows.host.HostFlowUtils.validateHostName; +import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete; import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership; import static google.registry.model.EppResourceUtils.createRepoId; import static google.registry.model.ofy.ObjectifyService.ofy; @@ -72,6 +73,7 @@ import org.joda.time.DateTime; * @error {@link HostFlowUtils.HostNameNotNormalizedException} * @error {@link HostFlowUtils.HostNameNotPunyCodedException} * @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException} + * @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException} * @error {@link SubordinateHostMustHaveIpException} * @error {@link UnexpectedExternalHostIpException} */ @@ -101,6 +103,7 @@ public final class HostCreateFlow implements TransactionalFlow { // we can detect error conditions earlier. Optional superordinateDomain = lookupSuperordinateDomain(validateHostName(targetId), now); + verifySuperordinateDomainNotInPendingDelete(superordinateDomain.orNull()); verifySuperordinateDomainOwnership(clientId, superordinateDomain.orNull()); boolean willBeSubordinate = superordinateDomain.isPresent(); boolean hasIpAddresses = !isNullOrEmpty(command.getInetAddresses()); diff --git a/java/google/registry/flows/host/HostFlowUtils.java b/java/google/registry/flows/host/HostFlowUtils.java index 520e6b093..1a8e5c9a4 100644 --- a/java/google/registry/flows/host/HostFlowUtils.java +++ b/java/google/registry/flows/host/HostFlowUtils.java @@ -30,7 +30,9 @@ import google.registry.flows.EppException.ObjectDoesNotExistException; import google.registry.flows.EppException.ParameterValuePolicyErrorException; import google.registry.flows.EppException.ParameterValueRangeErrorException; import google.registry.flows.EppException.ParameterValueSyntaxErrorException; +import google.registry.flows.EppException.StatusProhibitsOperationException; import google.registry.model.domain.DomainResource; +import google.registry.model.eppcommon.StatusValue; import google.registry.util.Idn; import org.joda.time.DateTime; @@ -127,6 +129,24 @@ public class HostFlowUtils { } } + /** Ensure that the superordinate domain is not in pending delete. */ + static void verifySuperordinateDomainNotInPendingDelete( + DomainResource superordinateDomain) throws EppException { + if ((superordinateDomain != null) + && superordinateDomain.getStatusValues().contains(StatusValue.PENDING_DELETE)) { + throw new SuperordinateDomainInPendingDeleteException( + superordinateDomain.getFullyQualifiedDomainName()); + } + } + + /** Superordinate domain for this hostname is in pending delete. */ + static class SuperordinateDomainInPendingDeleteException + extends StatusProhibitsOperationException { + public SuperordinateDomainInPendingDeleteException(String domainName) { + super(domainName); + } + } + /** Host names are limited to 253 characters. */ static class HostNameTooLongException extends ParameterValueRangeErrorException { public HostNameTooLongException() { diff --git a/java/google/registry/flows/host/HostUpdateFlow.java b/java/google/registry/flows/host/HostUpdateFlow.java index 1b3c2c1b4..142ec2004 100644 --- a/java/google/registry/flows/host/HostUpdateFlow.java +++ b/java/google/registry/flows/host/HostUpdateFlow.java @@ -24,6 +24,7 @@ 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.verifySuperordinateDomainNotInPendingDelete; import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership; import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey; import static google.registry.model.ofy.ObjectifyService.ofy; @@ -90,6 +91,7 @@ import org.joda.time.DateTime; * @error {@link HostFlowUtils.HostNameTooShallowException} * @error {@link HostFlowUtils.InvalidHostNameException} * @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException} + * @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException} * @error {@link CannotAddIpToExternalHostException} * @error {@link CannotRemoveSubordinateHostLastIpException} * @error {@link CannotRenameExternalHostException} @@ -139,6 +141,7 @@ public final class HostUpdateFlow implements TransactionalFlow { // Note that lookupSuperordinateDomain calls cloneProjectedAtTime on the domain for us. Optional newSuperordinateDomain = lookupSuperordinateDomain(validateHostName(newHostName), now); + verifySuperordinateDomainNotInPendingDelete(newSuperordinateDomain.orNull()); EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost); verifyUpdateAllowed( command, existingHost, newSuperordinateDomain.orNull(), owningResource, isHostRename); diff --git a/java/google/registry/rde/imports/RdeHostLinkAction.java b/java/google/registry/rde/imports/RdeHostLinkAction.java index 5093358fb..512f8d109 100644 --- a/java/google/registry/rde/imports/RdeHostLinkAction.java +++ b/java/google/registry/rde/imports/RdeHostLinkAction.java @@ -28,6 +28,7 @@ import com.googlecode.objectify.VoidWork; import google.registry.config.RegistryConfig.Config; import google.registry.mapreduce.MapreduceRunner; import google.registry.model.domain.DomainResource; +import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.HostResource; import google.registry.request.Action; import google.registry.request.Parameter; @@ -107,6 +108,14 @@ public class RdeHostLinkAction implements Runnable { logger.infofmt("Host %s is out of zone", xjcHost.getName()); return; } + if (superordinateDomain.get().getStatusValues().contains(StatusValue.PENDING_DELETE)) { + getContext() + .incrementCounter( + "post-import hosts with superordinate domains in pending delete"); + logger.infofmt( + "Host %s has a superordinate domain in pending delete", xjcHost.getName()); + return; + } // at this point, the host is definitely in zone and should be linked getContext().incrementCounter("post-import hosts in zone"); final Key superordinateDomainKey = Key.create(superordinateDomain.get()); diff --git a/javatests/google/registry/flows/host/HostCreateFlowTest.java b/javatests/google/registry/flows/host/HostCreateFlowTest.java index 6366b855e..f63134294 100644 --- a/javatests/google/registry/flows/host/HostCreateFlowTest.java +++ b/javatests/google/registry/flows/host/HostCreateFlowTest.java @@ -18,15 +18,18 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.testing.DatastoreHelper.assertNoBillingEvents; import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.newDomainResource; import static google.registry.testing.DatastoreHelper.persistActiveDomain; import static google.registry.testing.DatastoreHelper.persistActiveHost; import static google.registry.testing.DatastoreHelper.persistDeletedHost; +import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.HostResourceSubject.assertAboutHosts; import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued; import static google.registry.testing.TaskQueueHelper.assertNoDnsTasksEnqueued; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.flows.EppXmlTransformer.IpAddressVersionMismatchException; import google.registry.flows.ResourceFlowTestCase; @@ -40,7 +43,9 @@ import google.registry.flows.host.HostFlowUtils.HostNameTooLongException; import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException; import google.registry.flows.host.HostFlowUtils.InvalidHostNameException; import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException; +import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException; import google.registry.model.domain.DomainResource; +import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.HostResource; import google.registry.model.reporting.HistoryEntry; import org.joda.time.DateTime; @@ -167,6 +172,22 @@ public class HostCreateFlowTest extends ResourceFlowTestCase192.0.2.22", + "1080:0:0:0:8:800:200C:417A"); + createTld("tld"); + DomainResource domain = persistResource(newDomainResource("example.tld") + .asBuilder() + .setSubordinateHosts(ImmutableSet.of(oldHostName())) + .setDeletionTime(clock.nowUtc().plusDays(35)) + .setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE)) + .build()); + persistActiveSubordinateHost(oldHostName(), domain); + clock.advanceOneMilli(); + thrown.expect( + SuperordinateDomainInPendingDeleteException.class, + "example.tld"); + runFlow(); + } + @Test public void testFailure_neverExisted() throws Exception { thrown.expect(