diff --git a/java/google/registry/flows/contact/ContactDeleteFlow.java b/java/google/registry/flows/contact/ContactDeleteFlow.java index 578386d84..645061a41 100644 --- a/java/google/registry/flows/contact/ContactDeleteFlow.java +++ b/java/google/registry/flows/contact/ContactDeleteFlow.java @@ -64,6 +64,13 @@ public class ContactDeleteFlow extends LoggedInFlow implements TransactionalFlow StatusValue.PENDING_DELETE, StatusValue.SERVER_DELETE_PROHIBITED); + private static final Function> GET_REFERENCED_CONTACTS = + new Function>() { + @Override + public ImmutableSet apply(DomainBase domain) { + return domain.getReferencedContacts(); + }}; + @Inject AsyncFlowEnqueuer asyncFlowEnqueuer; @Inject @ClientId String clientId; @Inject @TargetId String targetId; @@ -79,15 +86,7 @@ public class ContactDeleteFlow extends LoggedInFlow implements TransactionalFlow @Override public final EppOutput run() throws EppException { - failfastForAsyncDelete( - targetId, - now, - ContactResource.class, - new Function>() { - @Override - public ImmutableSet apply(DomainBase domain) { - return domain.getReferencedContacts(); - }}); + failfastForAsyncDelete(targetId, now, ContactResource.class, GET_REFERENCED_CONTACTS); ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); if (existingResource == null) { throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); diff --git a/java/google/registry/flows/host/HostCheckFlow.java b/java/google/registry/flows/host/HostCheckFlow.java index 9fbdfed73..0f434ade1 100644 --- a/java/google/registry/flows/host/HostCheckFlow.java +++ b/java/google/registry/flows/host/HostCheckFlow.java @@ -15,34 +15,46 @@ package google.registry.flows.host; import static google.registry.model.EppResourceUtils.checkResourcesExist; +import static google.registry.model.eppoutput.Result.Code.Success; import com.google.common.collect.ImmutableList; -import google.registry.flows.ResourceCheckFlow; -import google.registry.model.eppoutput.CheckData; +import google.registry.config.ConfigModule.Config; +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.exceptions.TooManyResourceChecksException; +import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.CheckData.HostCheck; import google.registry.model.eppoutput.CheckData.HostCheckData; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.host.HostCommand.Check; import google.registry.model.host.HostResource; +import java.util.List; import java.util.Set; import javax.inject.Inject; /** * An EPP flow that checks whether a host can be provisioned. * - * @error {@link google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException} + * @error {@link google.registry.flows.exceptions.TooManyResourceChecksException} */ -public class HostCheckFlow extends ResourceCheckFlow { +public class HostCheckFlow extends LoggedInFlow { + @Inject ResourceCommand resourceCommand; + @Inject @Config("maxChecks") int maxChecks; @Inject HostCheckFlow() {} @Override - protected CheckData getCheckData() { - Set existingIds = checkResourcesExist(resourceClass, targetIds, now); + protected final EppOutput run() throws EppException { + List targetIds = ((Check) resourceCommand).getTargetIds(); + if (targetIds.size() > maxChecks) { + throw new TooManyResourceChecksException(maxChecks); + } + Set existingIds = checkResourcesExist(HostResource.class, targetIds, now); ImmutableList.Builder checks = new ImmutableList.Builder<>(); for (String id : targetIds) { boolean unused = !existingIds.contains(id); checks.add(HostCheck.create(unused, id, unused ? null : "In use")); } - return HostCheckData.create(checks.build()); + return createOutput(Success, HostCheckData.create(checks.build())); } } diff --git a/java/google/registry/flows/host/HostCreateFlow.java b/java/google/registry/flows/host/HostCreateFlow.java index a1692198f..a47d5926f 100644 --- a/java/google/registry/flows/host/HostCreateFlow.java +++ b/java/google/registry/flows/host/HostCreateFlow.java @@ -18,23 +18,34 @@ 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.EppResourceUtils.createContactHostRoid; +import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CollectionUtils.isNullOrEmpty; +import static google.registry.util.CollectionUtils.union; 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.ParameterValueRangeErrorException; import google.registry.flows.EppException.RequiredParameterMissingException; -import google.registry.flows.ResourceCreateFlow; +import google.registry.flows.FlowModule.ClientId; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; +import google.registry.flows.exceptions.ResourceAlreadyExistsException; +import google.registry.model.ImmutableObject; import google.registry.model.domain.DomainResource; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.CreateData.HostCreateData; import google.registry.model.eppoutput.EppOutput; import google.registry.model.host.HostCommand.Create; import google.registry.model.host.HostResource; import google.registry.model.host.HostResource.Builder; +import google.registry.model.index.EppResourceIndex; +import google.registry.model.index.ForeignKeyIndex; import google.registry.model.ofy.ObjectifyService; import google.registry.model.reporting.HistoryEntry; import javax.inject.Inject; @@ -43,7 +54,7 @@ import javax.inject.Inject; * An EPP flow that creates a new host resource. * * @error {@link google.registry.flows.EppXmlTransformer.IpAddressVersionMismatchException} - * @error {@link google.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException} + * @error {@link google.registry.flows.exceptions.ResourceAlreadyExistsException} * @error {@link HostFlowUtils.HostNameTooLongException} * @error {@link HostFlowUtils.HostNameTooShallowException} * @error {@link HostFlowUtils.InvalidHostNameException} @@ -51,37 +62,33 @@ import javax.inject.Inject; * @error {@link SubordinateHostMustHaveIpException} * @error {@link UnexpectedExternalHostIpException} */ -public class HostCreateFlow extends ResourceCreateFlow { - - /** - * The superordinate domain of the host object if creating an in-bailiwick host, or null if - * creating an external host. This is looked up before we actually create the Host object so that - * we can detect error conditions earlier. By the time {@link #setCreateProperties} is called - * (where this reference is actually used), we no longer have the ability to return an - * {@link EppException}. - * - *

The general model of these classes is to do validation of parameters up front before we get - * to the actual object creation, which is why this class looks up and stores the superordinate - * domain ahead of time. - */ - private Optional> superordinateDomain; +public class HostCreateFlow extends LoggedInFlow implements TransactionalFlow { + @Inject ResourceCommand resourceCommand; + @Inject @ClientId String clientId; + @Inject HistoryEntry.Builder historyBuilder; @Inject HostCreateFlow() {} @Override - protected void initResourceCreateOrMutateFlow() throws EppException { - superordinateDomain = Optional.fromNullable(lookupSuperordinateDomain( - validateHostName(command.getFullyQualifiedHostName()), now)); + @SuppressWarnings("unchecked") + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); } @Override - protected String createFlowRepoId() { - return createContactHostRoid(ObjectifyService.allocateId()); - } - - @Override - protected void verifyCreateIsAllowed() throws EppException { - verifyDomainIsSameRegistrar(superordinateDomain.orNull(), getClientId()); + protected final EppOutput run() throws EppException { + Create command = (Create) resourceCommand; + String targetId = command.getTargetId(); + HostResource existingResource = loadByUniqueId(HostResource.class, targetId, now); + if (existingResource != null) { + throw new ResourceAlreadyExistsException(targetId); + } + // The superordinate domain of the host object if creating an in-bailiwick host, or null if + // creating an external host. This is looked up before we actually create the Host object so + // we can detect error conditions earlier. + Optional superordinateDomain = Optional.fromNullable( + lookupSuperordinateDomain(validateHostName(command.getFullyQualifiedHostName()), now)); + verifyDomainIsSameRegistrar(superordinateDomain.orNull(), clientId); boolean willBeSubordinate = superordinateDomain.isPresent(); boolean hasIpAddresses = !isNullOrEmpty(command.getInetAddresses()); if (willBeSubordinate != hasIpAddresses) { @@ -90,43 +97,36 @@ public class HostCreateFlow extends ResourceCreateFlow entitiesToSave = ImmutableSet.of( + newResource, + historyBuilder.build(), + ForeignKeyIndex.create(newResource, newResource.getDeletionTime()), + EppResourceIndex.create(Key.create(newResource))); if (superordinateDomain.isPresent()) { - builder.setSuperordinateDomain(superordinateDomain.get()); + entitiesToSave = union( + entitiesToSave, + superordinateDomain.get().asBuilder() + .addSubordinateHost(command.getFullyQualifiedHostName()) + .build()); + // Only update DNS if this is a subordinate host. External hosts have no glue to write, so + // they are only written as NS records from the referencing domain. + DnsQueue.create().addHostRefreshTask(targetId); } - } - - /** Modify any other resources that need to be informed of this create. */ - @Override - protected void modifyCreateRelatedResources() { - if (superordinateDomain.isPresent()) { - ofy().save().entity(ofy().load().key(superordinateDomain.get()).now().asBuilder() - .addSubordinateHost(command.getFullyQualifiedHostName()) - .build()); - } - } - - @Override - protected void enqueueTasks() { - // Only update DNS if this is a subordinate host. External hosts have no glue to write, so they - // are only written as NS records from the referencing domain. - if (superordinateDomain.isPresent()) { - DnsQueue.create().addHostRefreshTask(newResource.getFullyQualifiedHostName()); - } - } - - @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.HOST_CREATE; - } - - @Override - protected EppOutput getOutput() { - return createOutput(Success, - HostCreateData.create(newResource.getFullyQualifiedHostName(), now)); + ofy().save().entities(entitiesToSave); + return createOutput(Success, HostCreateData.create(targetId, now)); } /** Subordinate hosts must have an ip address. */ diff --git a/java/google/registry/flows/host/HostDeleteFlow.java b/java/google/registry/flows/host/HostDeleteFlow.java index 3b6bac841..9a2308ce1 100644 --- a/java/google/registry/flows/host/HostDeleteFlow.java +++ b/java/google/registry/flows/host/HostDeleteFlow.java @@ -14,79 +14,106 @@ package google.registry.flows.host; -import static google.registry.model.EppResourceUtils.queryDomainsUsingResource; +import static google.registry.flows.ResourceFlowUtils.failfastForAsyncDelete; +import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.SuccessWithActionPending; import static google.registry.model.ofy.ObjectifyService.ofy; -import com.google.common.base.Predicate; +import com.google.common.base.Function; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; +import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; -import google.registry.config.RegistryEnvironment; +import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; -import google.registry.flows.ResourceAsyncDeleteFlow; -import google.registry.flows.async.AsyncFlowEnqueuer; +import google.registry.flows.FlowModule.ClientId; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; import google.registry.flows.async.AsyncFlowUtils; import google.registry.flows.async.DeleteEppResourceAction; import google.registry.flows.async.DeleteHostResourceAction; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.domain.DomainBase; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.AuthInfo; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.host.HostCommand.Delete; import google.registry.model.host.HostResource; -import google.registry.model.host.HostResource.Builder; import google.registry.model.reporting.HistoryEntry; import javax.inject.Inject; +import org.joda.time.Duration; /** * An EPP flow that deletes a host resource. * - * @error {@link google.registry.flows.ResourceAsyncDeleteFlow.ResourceToDeleteIsReferencedException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} + * @error {@link google.registry.flows.exceptions.ResourceToDeleteIsReferencedException} */ -public class HostDeleteFlow extends ResourceAsyncDeleteFlow { +public class HostDeleteFlow extends LoggedInFlow implements TransactionalFlow { - /** In {@link #isLinkedForFailfast}, check this (arbitrary) number of resources from the query. */ - private static final int FAILFAST_CHECK_COUNT = 5; + private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( + StatusValue.LINKED, + StatusValue.CLIENT_DELETE_PROHIBITED, + StatusValue.PENDING_DELETE, + StatusValue.SERVER_DELETE_PROHIBITED); - @Inject AsyncFlowEnqueuer asyncFlowEnqueuer; + private static final Function> GET_NAMESERVERS = + new Function>() { + @Override + public ImmutableSet apply(DomainBase domain) { + return domain.getNameservers(); + }}; + + @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; + @Inject @ClientId String clientId; + @Inject @Config("asyncDeleteFlowMapreduceDelay") Duration mapreduceDelay; + @Inject HistoryEntry.Builder historyBuilder; @Inject HostDeleteFlow() {} @Override - protected boolean isLinkedForFailfast(final Key key) { - // Query for the first few linked domains, and if found, actually load them. The query is - // eventually consistent and so might be very stale, but the direct load will not be stale, - // just non-transactional. If we find at least one actual reference then we can reliably - // fail. If we don't find any, we can't trust the query and need to do the full mapreduce. - return Iterables.any( - ofy().load().keys( - queryDomainsUsingResource( - HostResource.class, key, now, FAILFAST_CHECK_COUNT)).values(), - new Predicate() { - @Override - public boolean apply(DomainBase domain) { - return domain.getNameservers().contains(key); - }}); + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); } - /** Enqueues an asynchronous host resource deletion. */ @Override - protected final void enqueueTasks() throws EppException { + public final EppOutput run() throws EppException { + Delete command = (Delete) resourceCommand; + String targetId = command.getTargetId(); + failfastForAsyncDelete(targetId, now, HostResource.class, GET_NAMESERVERS); + HostResource existingResource = loadByUniqueId(HostResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(HostResource.class, targetId); + } + verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES); + verifyOptionalAuthInfoForResource(authInfo, existingResource); + if (!isSuperuser) { + verifyResourceOwnership(clientId, existingResource); + } AsyncFlowUtils.enqueueMapreduceAction( DeleteHostResourceAction.class, ImmutableMap.of( DeleteEppResourceAction.PARAM_RESOURCE_KEY, Key.create(existingResource).getString(), DeleteEppResourceAction.PARAM_REQUESTING_CLIENT_ID, - getClientId(), + clientId, DeleteEppResourceAction.PARAM_IS_SUPERUSER, Boolean.toString(isSuperuser)), - RegistryEnvironment.get().config().getAsyncDeleteFlowMapreduceDelay()); - // TODO(b/26140521): Switch over to batch async operations as follows: - // asyncFlowEnqueuer.enqueueAsyncDelete(existingResource, getClientId(), isSuperuser); - } - - @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.HOST_PENDING_DELETE; + mapreduceDelay); + HostResource newResource = + existingResource.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build(); + historyBuilder + .setType(HistoryEntry.Type.HOST_PENDING_DELETE) + .setModificationTime(now) + .setParent(Key.create(existingResource)); + ofy().save().entities(newResource, historyBuilder.build()); + return createOutput(SuccessWithActionPending); } } diff --git a/java/google/registry/flows/host/HostFlowUtils.java b/java/google/registry/flows/host/HostFlowUtils.java index 2ca9fc044..e31d0295e 100644 --- a/java/google/registry/flows/host/HostFlowUtils.java +++ b/java/google/registry/flows/host/HostFlowUtils.java @@ -16,14 +16,12 @@ package google.registry.flows.host; import static google.registry.model.EppResourceUtils.isActive; import static google.registry.model.EppResourceUtils.loadByUniqueId; -import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.registry.Registries.findTldForName; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.Iterables; import com.google.common.net.InternetDomainName; -import com.googlecode.objectify.Key; import google.registry.flows.EppException; import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.ObjectDoesNotExistException; @@ -76,23 +74,21 @@ public class HostFlowUtils { } /** Return the {@link DomainResource} this host is subordinate to, or null for external hosts. */ - static Key lookupSuperordinateDomain( + static DomainResource lookupSuperordinateDomain( InternetDomainName hostName, DateTime now) throws EppException { - Optional tldParsed = findTldForName(hostName); - if (!tldParsed.isPresent()) { + Optional tld = findTldForName(hostName); + if (!tld.isPresent()) { // This is an host on a TLD we don't run, therefore obviously external, so we are done. return null; } - // This is a subordinate host - @SuppressWarnings("deprecation") String domainName = Joiner.on('.').join(Iterables.skip( - hostName.parts(), hostName.parts().size() - (tldParsed.get().parts().size() + 1))); + hostName.parts(), hostName.parts().size() - (tld.get().parts().size() + 1))); DomainResource superordinateDomain = loadByUniqueId(DomainResource.class, domainName, now); if (superordinateDomain == null || !isActive(superordinateDomain, now)) { throw new SuperordinateDomainDoesNotExistException(domainName); } - return Key.create(superordinateDomain); + return superordinateDomain; } /** Superordinate domain for this hostname does not exist. */ @@ -104,11 +100,10 @@ public class HostFlowUtils { /** Ensure that the superordinate domain is sponsored by the provided clientId. */ static void verifyDomainIsSameRegistrar( - Key superordinateDomain, + DomainResource superordinateDomain, String clientId) throws EppException { if (superordinateDomain != null - && !clientId.equals( - ofy().load().key(superordinateDomain).now().getCurrentSponsorClientId())) { + && !clientId.equals(superordinateDomain.getCurrentSponsorClientId())) { throw new HostDomainNotOwnedException(); } } diff --git a/java/google/registry/flows/host/HostInfoFlow.java b/java/google/registry/flows/host/HostInfoFlow.java index 09aa33ad4..577df5fbc 100644 --- a/java/google/registry/flows/host/HostInfoFlow.java +++ b/java/google/registry/flows/host/HostInfoFlow.java @@ -14,16 +14,42 @@ package google.registry.flows.host; -import google.registry.flows.ResourceInfoFlow; -import google.registry.model.host.HostCommand; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; +import static google.registry.model.EppResourceUtils.cloneResourceWithLinkedStatus; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; + +import com.google.common.base.Optional; +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; +import google.registry.model.eppcommon.AuthInfo; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; +import google.registry.model.host.HostCommand.Info; import google.registry.model.host.HostResource; import javax.inject.Inject; /** * An EPP flow that reads a host. * - * @error {@link google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException} + * @error {@link google.registry.flows.exceptions.ResourceToQueryDoesNotExistException} */ -public class HostInfoFlow extends ResourceInfoFlow { +public class HostInfoFlow extends LoggedInFlow { + + @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; @Inject HostInfoFlow() {} + + @Override + public EppOutput run() throws EppException { + Info command = (Info) resourceCommand; + String targetId = command.getTargetId(); + HostResource existingResource = loadByUniqueId(HostResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToQueryDoesNotExistException(HostResource.class, targetId); + } + verifyOptionalAuthInfoForResource(authInfo, existingResource); + return createOutput(Success, cloneResourceWithLinkedStatus(existingResource, now)); + } } diff --git a/java/google/registry/flows/host/HostUpdateFlow.java b/java/google/registry/flows/host/HostUpdateFlow.java index 5d9de383d..0e6b37364 100644 --- a/java/google/registry/flows/host/HostUpdateFlow.java +++ b/java/google/registry/flows/host/HostUpdateFlow.java @@ -15,14 +15,22 @@ package google.registry.flows.host; import static com.google.common.base.MoreObjects.firstNonNull; +import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; +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.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; 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.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import com.googlecode.objectify.Key; import google.registry.dns.DnsQueue; import google.registry.flows.EppException; @@ -30,10 +38,23 @@ 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.ResourceUpdateFlow; +import google.registry.flows.FlowModule.ClientId; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; import google.registry.flows.async.AsyncFlowUtils; import google.registry.flows.async.DnsRefreshForHostRenameAction; +import google.registry.flows.exceptions.AddRemoveSameValueEppException; +import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; +import google.registry.flows.exceptions.StatusNotClientSettableException; +import google.registry.model.ImmutableObject; import google.registry.model.domain.DomainResource; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.AuthInfo; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.host.HostCommand.Update; import google.registry.model.host.HostResource; import google.registry.model.host.HostResource.Builder; @@ -47,10 +68,10 @@ import org.joda.time.Duration; * An EPP flow that updates a host resource. * * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException} - * @error {@link google.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException} - * @error {@link google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException} + * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} + * @error {@link google.registry.flows.exceptions.StatusNotClientSettableException} * @error {@link HostFlowUtils.HostNameTooShallowException} * @error {@link HostFlowUtils.InvalidHostNameException} * @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException} @@ -60,40 +81,115 @@ import org.joda.time.Duration; * @error {@link RenameHostToExternalRemoveIpException} * @error {@link RenameHostToSubordinateRequiresIpException} */ -public class HostUpdateFlow extends ResourceUpdateFlow { +public class HostUpdateFlow extends LoggedInFlow implements TransactionalFlow { - private Key superordinateDomain; - - private String oldHostName; - private String newHostName; - private boolean isHostRename; + /** + * 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 DISALLOWED_STATUSES = ImmutableSet.of( + StatusValue.PENDING_DELETE, + StatusValue.SERVER_UPDATE_PROHIBITED); + @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; + @Inject @ClientId String clientId; + @Inject HistoryEntry.Builder historyBuilder; @Inject HostUpdateFlow() {} @Override - protected void initResourceCreateOrMutateFlow() throws EppException { - String suppliedNewHostName = command.getInnerChange().getFullyQualifiedHostName(); - isHostRename = suppliedNewHostName != null; - oldHostName = targetId; - newHostName = firstNonNull(suppliedNewHostName, oldHostName); - superordinateDomain = - lookupSuperordinateDomain(validateHostName(newHostName), now); + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); } @Override - protected void verifyUpdateIsAllowed() throws EppException { - verifyDomainIsSameRegistrar(superordinateDomain, getClientId()); - if (isHostRename - && loadAndGetKey(HostResource.class, newHostName, now) != null) { + public final EppOutput run() throws EppException { + Update command = (Update) resourceCommand; + String suppliedNewHostName = command.getInnerChange().getFullyQualifiedHostName(); + String targetId = command.getTargetId(); + HostResource existingResource = loadByUniqueId(HostResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(HostResource.class, targetId); + } + boolean isHostRename = suppliedNewHostName != null; + String oldHostName = targetId; + String newHostName = firstNonNull(suppliedNewHostName, oldHostName); + Optional superordinateDomain = + Optional.fromNullable(lookupSuperordinateDomain(validateHostName(newHostName), now)); + verifyUpdateAllowed(command, existingResource, superordinateDomain.orNull()); + if (isHostRename && loadAndGetKey(HostResource.class, newHostName, now) != null) { throw new HostAlreadyExistsException(newHostName); } + Builder builder = existingResource.asBuilder(); + try { + command.applyTo(builder); + } catch (AddRemoveSameValueException e) { + throw new AddRemoveSameValueEppException(); + } + builder + .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); + // Rely on the host's cloneProjectedAtTime() method to handle setting of transfer data. + HostResource newResource = builder.build().cloneProjectedAtTime(now); + verifyHasIpsIffIsExternal(command, existingResource, newResource); + ImmutableSet.Builder entitiesToSave = new ImmutableSet.Builder<>(); + entitiesToSave.add(newResource); + // 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(existingResource, now), + ForeignKeyIndex.create(newResource, newResource.getDeletionTime())); + updateSuperordinateDomains(existingResource, newResource); + } + enqueueTasks(existingResource, newResource); + entitiesToSave.add(historyBuilder + .setType(HistoryEntry.Type.HOST_UPDATE) + .setModificationTime(now) + .setParent(Key.create(existingResource)) + .build()); + ofy().save().entities(entitiesToSave.build()); + return createOutput(Success); } - @Override - protected void verifyNewUpdatedStateIsAllowed() throws EppException { + private void verifyUpdateAllowed( + Update command, HostResource existingResource, DomainResource superordinateDomain) + throws EppException { + verifyOptionalAuthInfoForResource(authInfo, existingResource); + if (!isSuperuser) { + verifyResourceOwnership(clientId, existingResource); + // 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) + && !command.getInnerRemove().getStatusValues() + .contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) { + throw new ResourceHasClientUpdateProhibitedException(); + } + } + for (StatusValue statusValue : Sets.union( + command.getInnerAdd().getStatusValues(), + command.getInnerRemove().getStatusValues())) { + if (!isSuperuser && !statusValue.isClientSettable()) { // The superuser can set any status. + throw new StatusNotClientSettableException(statusValue.getXmlName()); + } + } + verifyDomainIsSameRegistrar(superordinateDomain, clientId); + verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES); + } + + void verifyHasIpsIffIsExternal( + Update command, HostResource existingResource, HostResource newResource) throws EppException { boolean wasExternal = existingResource.getSuperordinateDomain() == null; boolean wasSubordinate = !wasExternal; - boolean willBeExternal = superordinateDomain == null; + boolean willBeExternal = newResource.getSuperordinateDomain() == null; boolean willBeSubordinate = !willBeExternal; boolean newResourceHasIps = !isNullOrEmpty(newResource.getInetAddresses()); boolean commandAddsIps = !isNullOrEmpty(command.getInnerAdd().getInetAddresses()); @@ -114,43 +210,18 @@ public class HostUpdateFlow extends ResourceUpdateFlow oldSuperordinateDomain = existingResource.getSuperordinateDomain(); - if (oldSuperordinateDomain != null || superordinateDomain != null) { - if (Objects.equals(oldSuperordinateDomain, superordinateDomain)) { + Key newSuperordinateDomain = newResource.getSuperordinateDomain(); + if (oldSuperordinateDomain != null || newSuperordinateDomain != null) { + if (Objects.equals(oldSuperordinateDomain, newSuperordinateDomain)) { ofy().save().entity( ofy().load().key(oldSuperordinateDomain).now().asBuilder() - .removeSubordinateHost(oldHostName) - .addSubordinateHost(newHostName) + .removeSubordinateHost(existingResource.getFullyQualifiedHostName()) + .addSubordinateHost(newResource.getFullyQualifiedHostName()) .build()); } else { if (oldSuperordinateDomain != null) { ofy().save().entity( ofy().load().key(oldSuperordinateDomain).now() .asBuilder() - .removeSubordinateHost(oldHostName) + .removeSubordinateHost(existingResource.getFullyQualifiedHostName()) .build()); } - if (superordinateDomain != null) { + if (newSuperordinateDomain != null) { ofy().save().entity( - ofy().load().key(superordinateDomain).now() + ofy().load().key(newSuperordinateDomain).now() .asBuilder() - .addSubordinateHost(newHostName) + .addSubordinateHost(newResource.getFullyQualifiedHostName()) .build()); } } diff --git a/javatests/google/registry/flows/host/HostCheckFlowTest.java b/javatests/google/registry/flows/host/HostCheckFlowTest.java index c0f68ecd6..2827dd6e4 100644 --- a/javatests/google/registry/flows/host/HostCheckFlowTest.java +++ b/javatests/google/registry/flows/host/HostCheckFlowTest.java @@ -18,8 +18,8 @@ import static google.registry.model.eppoutput.CheckData.HostCheck.create; import static google.registry.testing.DatastoreHelper.persistActiveHost; import static google.registry.testing.DatastoreHelper.persistDeletedHost; -import google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException; import google.registry.flows.ResourceCheckFlowTestCase; +import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.host.HostResource; import org.junit.Test; diff --git a/javatests/google/registry/flows/host/HostCreateFlowTest.java b/javatests/google/registry/flows/host/HostCreateFlowTest.java index 2aa19413f..d5a799934 100644 --- a/javatests/google/registry/flows/host/HostCreateFlowTest.java +++ b/javatests/google/registry/flows/host/HostCreateFlowTest.java @@ -28,8 +28,8 @@ import static google.registry.testing.TaskQueueHelper.assertNoDnsTasksEnqueued; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import google.registry.flows.EppXmlTransformer.IpAddressVersionMismatchException; -import google.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException; import google.registry.flows.ResourceFlowTestCase; +import google.registry.flows.exceptions.ResourceAlreadyExistsException; import google.registry.flows.host.HostCreateFlow.SubordinateHostMustHaveIpException; import google.registry.flows.host.HostCreateFlow.UnexpectedExternalHostIpException; import google.registry.flows.host.HostFlowUtils.HostNameTooLongException; diff --git a/javatests/google/registry/flows/host/HostDeleteFlowTest.java b/javatests/google/registry/flows/host/HostDeleteFlowTest.java index 0358a4ac3..e0c8c9041 100644 --- a/javatests/google/registry/flows/host/HostDeleteFlowTest.java +++ b/javatests/google/registry/flows/host/HostDeleteFlowTest.java @@ -30,13 +30,13 @@ import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; -import google.registry.flows.ResourceAsyncDeleteFlow.ResourceToDeleteIsReferencedException; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException; import google.registry.flows.async.DeleteEppResourceAction; import google.registry.flows.async.DeleteHostResourceAction; +import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; +import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.HostResource; import google.registry.model.reporting.HistoryEntry; diff --git a/javatests/google/registry/flows/host/HostInfoFlowTest.java b/javatests/google/registry/flows/host/HostInfoFlowTest.java index 6811737a9..023edf98f 100644 --- a/javatests/google/registry/flows/host/HostInfoFlowTest.java +++ b/javatests/google/registry/flows/host/HostInfoFlowTest.java @@ -25,7 +25,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.net.InetAddresses; import com.googlecode.objectify.Key; import google.registry.flows.ResourceFlowTestCase; -import google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.domain.DomainResource; import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.HostResource; diff --git a/javatests/google/registry/flows/host/HostUpdateFlowTest.java b/javatests/google/registry/flows/host/HostUpdateFlowTest.java index fb8ea87b8..80d734dc8 100644 --- a/javatests/google/registry/flows/host/HostUpdateFlowTest.java +++ b/javatests/google/registry/flows/host/HostUpdateFlowTest.java @@ -43,11 +43,11 @@ import com.googlecode.objectify.Key; import google.registry.flows.EppRequestSource; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException; -import google.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException; -import google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException; import google.registry.flows.async.DnsRefreshForHostRenameAction; +import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; +import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; +import google.registry.flows.exceptions.StatusNotClientSettableException; import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException; import google.registry.flows.host.HostFlowUtils.InvalidHostNameException; import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;