diff --git a/docs/flows.md b/docs/flows.md index 6bf53d42e..64664856b 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -965,16 +965,18 @@ are enqueued to update DNS accordingly. * Host rename from external to subordinate must also add an IP addresses. * 2004 * The specified status value cannot be set by clients. + * Host names are limited to 253 characters. * Cannot add IP addresses to an external host. * Host rename from subordinate to external must also remove all IP addresses. * 2005 - * Invalid host name. * Host names must be in lower-case. * Host names must be in normalized format. * Host names must be puny-coded. + * Invalid host name. * 2201 * The specified resource belongs to another client. + * Domain for host is sponsored by another registrar. * 2302 * Host with specified name already exists. * 2303 diff --git a/java/google/registry/batch/DeleteContactsAndHostsAction.java b/java/google/registry/batch/DeleteContactsAndHostsAction.java index 7fb6ce362..52ed03090 100644 --- a/java/google/registry/batch/DeleteContactsAndHostsAction.java +++ b/java/google/registry/batch/DeleteContactsAndHostsAction.java @@ -276,9 +276,18 @@ public class DeleteContactsAndHostsAction implements Runnable { if (!doesResourceStateAllowDeletion(resource, now)) { return DeletionResult.create(Type.ERRORED, ""); } - + // Contacts and external hosts have a direct client id. For subordinate hosts it needs to be + // read off of the superordinate domain. + String resourceClientId = resource.getPersistedCurrentSponsorClientId(); + if (resource instanceof HostResource && ((HostResource) resource).isSubordinate()) { + resourceClientId = + ofy().load().key(((HostResource) resource).getSuperordinateDomain()).now() + .cloneProjectedAtTime(now) + .getCurrentSponsorClientId(); + } boolean requestedByCurrentOwner = - resource.getCurrentSponsorClientId().equals(deletionRequest.requestingClientId()); + resourceClientId.equals(deletionRequest.requestingClientId()); + boolean deleteAllowed = hasNoActiveReferences && (requestedByCurrentOwner || deletionRequest.isSuperuser()); diff --git a/java/google/registry/flows/ResourceFlowUtils.java b/java/google/registry/flows/ResourceFlowUtils.java index 67c1edc82..0ef6e65b4 100644 --- a/java/google/registry/flows/ResourceFlowUtils.java +++ b/java/google/registry/flows/ResourceFlowUtils.java @@ -139,7 +139,7 @@ public final class ResourceFlowUtils { /** Check that the given clientId corresponds to the owner of given resource. */ public static void verifyResourceOwnership(String myClientId, EppResource resource) throws EppException { - if (!myClientId.equals(resource.getCurrentSponsorClientId())) { + if (!myClientId.equals(resource.getPersistedCurrentSponsorClientId())) { throw new ResourceNotOwnedException(); } } @@ -264,7 +264,7 @@ public final class ResourceFlowUtils { B builder = ResourceFlowUtils.resolvePendingTransfer(resource, transferStatus, now); return builder .setLastTransferTime(now) - .setCurrentSponsorClientId(resource.getTransferData().getGainingClientId()) + .setPersistedCurrentSponsorClientId(resource.getTransferData().getGainingClientId()) .build(); } diff --git a/java/google/registry/flows/contact/ContactCreateFlow.java b/java/google/registry/flows/contact/ContactCreateFlow.java index ed85fcd53..279deda11 100644 --- a/java/google/registry/flows/contact/ContactCreateFlow.java +++ b/java/google/registry/flows/contact/ContactCreateFlow.java @@ -72,7 +72,7 @@ public final class ContactCreateFlow implements TransactionalFlow { .setContactId(targetId) .setAuthInfo(command.getAuthInfo()) .setCreationClientId(clientId) - .setCurrentSponsorClientId(clientId) + .setPersistedCurrentSponsorClientId(clientId) .setRepoId(createRepoId(ObjectifyService.allocateId(), roidSuffix)) .setFaxNumber(command.getFax()) .setVoiceNumber(command.getVoice()) diff --git a/java/google/registry/flows/domain/DomainAllocateFlow.java b/java/google/registry/flows/domain/DomainAllocateFlow.java index f5d5060cf..51deff155 100644 --- a/java/google/registry/flows/domain/DomainAllocateFlow.java +++ b/java/google/registry/flows/domain/DomainAllocateFlow.java @@ -155,7 +155,7 @@ public class DomainAllocateFlow implements TransactionalFlow { DateTime registrationExpirationTime = leapSafeAddYears(now, years); DomainResource newDomain = new DomainResource.Builder() .setCreationClientId(clientId) - .setCurrentSponsorClientId(clientId) + .setPersistedCurrentSponsorClientId(clientId) .setRepoId(repoId) .setIdnTableName(validateDomainNameWithIdnTables(domainName)) .setRegistrationExpirationTime(registrationExpirationTime) diff --git a/java/google/registry/flows/domain/DomainApplicationCreateFlow.java b/java/google/registry/flows/domain/DomainApplicationCreateFlow.java index 88398ed6f..9ce807a1b 100644 --- a/java/google/registry/flows/domain/DomainApplicationCreateFlow.java +++ b/java/google/registry/flows/domain/DomainApplicationCreateFlow.java @@ -234,7 +234,7 @@ public final class DomainApplicationCreateFlow implements TransactionalFlow { DomainApplication newApplication = new DomainApplication.Builder() .setCreationTrid(trid) .setCreationClientId(clientId) - .setCurrentSponsorClientId(clientId) + .setPersistedCurrentSponsorClientId(clientId) .setRepoId(createDomainRepoId(ObjectifyService.allocateId(), tld)) .setLaunchNotice(launchCreate == null ? null : launchCreate.getNotice()) .setIdnTableName(idnTableName) diff --git a/java/google/registry/flows/domain/DomainCreateFlow.java b/java/google/registry/flows/domain/DomainCreateFlow.java index 59e886d21..d68b581cc 100644 --- a/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/java/google/registry/flows/domain/DomainCreateFlow.java @@ -266,7 +266,7 @@ public class DomainCreateFlow implements TransactionalFlow { } DomainResource newDomain = new DomainResource.Builder() .setCreationClientId(clientId) - .setCurrentSponsorClientId(clientId) + .setPersistedCurrentSponsorClientId(clientId) .setRepoId(repoId) .setIdnTableName(validateDomainNameWithIdnTables(domainName)) .setRegistrationExpirationTime(registrationExpirationTime) diff --git a/java/google/registry/flows/host/HostCreateFlow.java b/java/google/registry/flows/host/HostCreateFlow.java index b33392cea..09a0330a4 100644 --- a/java/google/registry/flows/host/HostCreateFlow.java +++ b/java/google/registry/flows/host/HostCreateFlow.java @@ -18,7 +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.verifyDomainIsSameRegistrar; +import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership; import static google.registry.model.EppResourceUtils.createRepoId; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CollectionUtils.isNullOrEmpty; @@ -98,7 +98,7 @@ public final class HostCreateFlow implements TransactionalFlow { // we can detect error conditions earlier. Optional superordinateDomain = lookupSuperordinateDomain(validateHostName(targetId), now); - verifyDomainIsSameRegistrar(superordinateDomain.orNull(), clientId); + verifySuperordinateDomainOwnership(clientId, superordinateDomain.orNull()); boolean willBeSubordinate = superordinateDomain.isPresent(); boolean hasIpAddresses = !isNullOrEmpty(command.getInetAddresses()); if (willBeSubordinate != hasIpAddresses) { @@ -109,7 +109,7 @@ public final class HostCreateFlow implements TransactionalFlow { } HostResource newHost = new Builder() .setCreationClientId(clientId) - .setCurrentSponsorClientId(clientId) + .setPersistedCurrentSponsorClientId(clientId) .setFullyQualifiedHostName(targetId) .setInetAddresses(command.getInetAddresses()) .setRepoId(createRepoId(ObjectifyService.allocateId(), roidSuffix)) diff --git a/java/google/registry/flows/host/HostDeleteFlow.java b/java/google/registry/flows/host/HostDeleteFlow.java index 8c6df3f0e..864771b02 100644 --- a/java/google/registry/flows/host/HostDeleteFlow.java +++ b/java/google/registry/flows/host/HostDeleteFlow.java @@ -33,6 +33,7 @@ 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.model.EppResource; import google.registry.model.domain.DomainBase; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.eppcommon.StatusValue; @@ -93,7 +94,14 @@ public final class HostDeleteFlow implements TransactionalFlow { HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now); verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES); if (!isSuperuser) { - verifyResourceOwnership(clientId, existingHost); + // Hosts transfer with their superordinate domains, so for hosts with a superordinate domain, + // the client id, needs to be read off of it. + EppResource owningResource = + existingHost.isSubordinate() + ? ofy().load().key(existingHost.getSuperordinateDomain()).now() + .cloneProjectedAtTime(now) + : existingHost; + verifyResourceOwnership(clientId, owningResource); } asyncFlowEnqueuer.enqueueAsyncDelete(existingHost, clientId, isSuperuser); HostResource newHost = diff --git a/java/google/registry/flows/host/HostFlowUtils.java b/java/google/registry/flows/host/HostFlowUtils.java index 06243b9a0..520e6b093 100644 --- a/java/google/registry/flows/host/HostFlowUtils.java +++ b/java/google/registry/flows/host/HostFlowUtils.java @@ -111,9 +111,9 @@ public class HostFlowUtils { } /** Ensure that the superordinate domain is sponsored by the provided clientId. */ - static void verifyDomainIsSameRegistrar( - DomainResource superordinateDomain, - String clientId) throws EppException { + static void verifySuperordinateDomainOwnership( + String clientId, + DomainResource superordinateDomain) throws EppException { if (superordinateDomain != null && !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 b0aafaefa..e5b3d5fb6 100644 --- a/java/google/registry/flows/host/HostInfoFlow.java +++ b/java/google/registry/flows/host/HostInfoFlow.java @@ -18,6 +18,7 @@ import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; import static google.registry.flows.host.HostFlowUtils.validateHostName; import static google.registry.model.EppResourceUtils.isLinked; +import static google.registry.model.ofy.ObjectifyService.ofy; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; @@ -26,9 +27,11 @@ import google.registry.flows.ExtensionManager; import google.registry.flows.Flow; import google.registry.flows.FlowModule.ClientId; import google.registry.flows.FlowModule.TargetId; +import google.registry.model.domain.DomainResource; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppoutput.EppResponse; import google.registry.model.host.HostInfoData; +import google.registry.model.host.HostInfoData.Builder; import google.registry.model.host.HostResource; import google.registry.util.Clock; import javax.inject.Inject; @@ -66,18 +69,34 @@ public final class HostInfoFlow implements Flow { if (isLinked(Key.create(host), now)) { statusValues.add(StatusValue.LINKED); } + Builder hostInfoDataBuilder = HostInfoData.newBuilder(); + // Hosts transfer with their superordinate domains, so for hosts with a superordinate domain, + // the client id, last transfer time, and pending transfer status need to be read off of it. If + // there is no superordinate domain, the host's own values for these fields will be correct. + if (host.isSubordinate()) { + DomainResource superordinateDomain = + ofy().load().key(host.getSuperordinateDomain()).now().cloneProjectedAtTime(now); + hostInfoDataBuilder + .setCurrentSponsorClientId(superordinateDomain.getCurrentSponsorClientId()) + .setLastTransferTime(host.computeLastTransferTime(superordinateDomain)); + if (superordinateDomain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) { + statusValues.add(StatusValue.PENDING_TRANSFER); + } + } else { + hostInfoDataBuilder + .setCurrentSponsorClientId(host.getPersistedCurrentSponsorClientId()) + .setLastTransferTime(host.getLastTransferTime()); + } return responseBuilder - .setResData(HostInfoData.newBuilder() + .setResData(hostInfoDataBuilder .setFullyQualifiedHostName(host.getFullyQualifiedHostName()) .setRepoId(host.getRepoId()) .setStatusValues(statusValues.build()) .setInetAddresses(host.getInetAddresses()) - .setCurrentSponsorClientId(host.getCurrentSponsorClientId()) .setCreationClientId(host.getCreationClientId()) .setCreationTime(host.getCreationTime()) .setLastEppUpdateClientId(host.getLastEppUpdateClientId()) .setLastEppUpdateTime(host.getLastEppUpdateTime()) - .setLastTransferTime(host.getLastTransferTime()) .build()) .build(); } diff --git a/java/google/registry/flows/host/HostUpdateFlow.java b/java/google/registry/flows/host/HostUpdateFlow.java index bac86100e..5e33a6bb4 100644 --- a/java/google/registry/flows/host/HostUpdateFlow.java +++ b/java/google/registry/flows/host/HostUpdateFlow.java @@ -24,7 +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.verifyDomainIsSameRegistrar; +import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership; import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CollectionUtils.isNullOrEmpty; @@ -45,6 +45,7 @@ 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.EppResource; import google.registry.model.ImmutableObject; import google.registry.model.domain.DomainResource; import google.registry.model.domain.metadata.MetadataExtension; @@ -80,11 +81,13 @@ import org.joda.time.DateTime; * @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.HostDomainNotOwnedException} * @error {@link HostFlowUtils.HostNameNotLowerCaseException} * @error {@link HostFlowUtils.HostNameNotNormalizedException} * @error {@link HostFlowUtils.HostNameNotPunyCodedException} + * @error {@link HostFlowUtils.HostNameTooLongException} + * @error {@link HostFlowUtils.HostNameTooShallowException} + * @error {@link HostFlowUtils.InvalidHostNameException} * @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException} * @error {@link CannotAddIpToExternalHostException} * @error {@link CannotRemoveSubordinateHostLastIpException} @@ -128,10 +131,14 @@ public final class HostUpdateFlow implements TransactionalFlow { boolean isHostRename = suppliedNewHostName != null; String oldHostName = targetId; String newHostName = firstNonNull(suppliedNewHostName, oldHostName); + DomainResource oldSuperordinateDomain = existingHost.isSubordinate() + ? ofy().load().key(existingHost.getSuperordinateDomain()).now().cloneProjectedAtTime(now) + : null; // Note that lookupSuperordinateDomain calls cloneProjectedAtTime on the domain for us. Optional newSuperordinateDomain = lookupSuperordinateDomain(validateHostName(newHostName), now); - verifyUpdateAllowed(command, existingHost, newSuperordinateDomain.orNull()); + EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost); + verifyUpdateAllowed(command, existingHost, newSuperordinateDomain.orNull(), owningResource); if (isHostRename && loadAndGetKey(HostResource.class, newHostName, now) != null) { throw new HostAlreadyExistsException(newHostName); } @@ -139,13 +146,27 @@ public final class HostUpdateFlow implements TransactionalFlow { AddRemove remove = command.getInnerRemove(); checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues()); checkSameValuesNotAddedAndRemoved(add.getInetAddresses(), remove.getInetAddresses()); - Key newSuperordinateDomainKey = - newSuperordinateDomain.isPresent() ? Key.create(newSuperordinateDomain.get()) : null; + Key newSuperordinateDomainKey = newSuperordinateDomain.isPresent() + ? Key.create(newSuperordinateDomain.get()) + : null; // If the superordinateDomain field is changing, set the lastSuperordinateChange to now. DateTime lastSuperordinateChange = Objects.equals(newSuperordinateDomainKey, existingHost.getSuperordinateDomain()) ? existingHost.getLastSuperordinateChange() : now; + // Compute afresh the last transfer time to handle any superordinate domain transfer that may + // have just completed. This is only critical for updates that rename a host away from its + // current superordinate domain, where we must "freeze" the last transfer time, but it's easiest + // to just update it unconditionally. + DateTime lastTransferTime = existingHost.computeLastTransferTime(oldSuperordinateDomain); + // Copy the clientId onto the host. This is only really needed when the host will be external, + // since external hosts store their own clientId. For subordinate hosts the canonical clientId + // comes from the superordinate domain, but we might as well update the persisted value. For + // non-superusers this is the flow clientId, but for superusers it might not be, so compute it. + String newPersistedClientId = + newSuperordinateDomain.isPresent() + ? newSuperordinateDomain.get().getCurrentSponsorClientId() + : owningResource.getPersistedCurrentSponsorClientId(); HostResource newHost = existingHost.asBuilder() .setFullyQualifiedHostName(newHostName) .addStatusValues(add.getStatusValues()) @@ -156,9 +177,9 @@ public final class HostUpdateFlow implements TransactionalFlow { .setLastEppUpdateClientId(clientId) .setSuperordinateDomain(newSuperordinateDomainKey) .setLastSuperordinateChange(lastSuperordinateChange) - .build() - // Rely on the host's cloneProjectedAtTime() method to handle setting of transfer data. - .cloneProjectedAtTime(now); + .setLastTransferTime(lastTransferTime) + .setPersistedCurrentSponsorClientId(newPersistedClientId) + .build(); verifyHasIpsIffIsExternal(command, existingHost, newHost); ImmutableSet.Builder entitiesToSave = new ImmutableSet.Builder<>(); entitiesToSave.add(newHost); @@ -181,32 +202,37 @@ public final class HostUpdateFlow implements TransactionalFlow { } private void verifyUpdateAllowed( - Update command, HostResource existingResource, DomainResource superordinateDomain) - throws EppException { + Update command, + HostResource existingHost, + DomainResource newSuperordinateDomain, + EppResource owningResource) + throws EppException { if (!isSuperuser) { - verifyResourceOwnership(clientId, existingResource); + // Verify that the host belongs to this registrar, either directly or because it is currently + // subordinate to a domain owned by this registrar. + verifyResourceOwnership(clientId, owningResource); + // Verify that the new superordinate domain belongs to this registrar. + verifySuperordinateDomainOwnership(clientId, newSuperordinateDomain); ImmutableSet statusesToAdd = command.getInnerAdd().getStatusValues(); ImmutableSet 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) + // status, then the update must be disallowed. + if (existingHost.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); + verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES); } private void verifyHasIpsIffIsExternal( - Update command, HostResource existingResource, HostResource newResource) throws EppException { - boolean wasSubordinate = existingResource.isSubordinate(); + Update command, HostResource existingHost, HostResource newHost) throws EppException { + boolean wasSubordinate = existingHost.isSubordinate(); boolean wasExternal = !wasSubordinate; - boolean willBeSubordinate = newResource.isSubordinate(); + boolean willBeSubordinate = newHost.isSubordinate(); boolean willBeExternal = !willBeSubordinate; - boolean newResourceHasIps = !isNullOrEmpty(newResource.getInetAddresses()); + boolean newHostHasIps = !isNullOrEmpty(newHost.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 @@ -214,61 +240,61 @@ public final class HostUpdateFlow implements TransactionalFlow { if (willBeExternal && commandAddsIps) { throw new CannotAddIpToExternalHostException(); } - if (wasSubordinate && willBeExternal && newResourceHasIps) { + if (wasSubordinate && willBeExternal && newHostHasIps) { throw new RenameHostToExternalRemoveIpException(); } if (wasExternal && willBeSubordinate && !commandAddsIps) { throw new RenameHostToSubordinateRequiresIpException(); } - if (willBeSubordinate && !newResourceHasIps) { + if (willBeSubordinate && !newHostHasIps) { throw new CannotRemoveSubordinateHostLastIpException(); } } - private void enqueueTasks(HostResource existingResource, HostResource newResource) { + private void enqueueTasks(HostResource existingHost, HostResource newHost) { // 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.isSubordinate()) { - dnsQueue.addHostRefreshTask(existingResource.getFullyQualifiedHostName()); + if (existingHost.isSubordinate()) { + dnsQueue.addHostRefreshTask(existingHost.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.isSubordinate()) { - dnsQueue.addHostRefreshTask(newResource.getFullyQualifiedHostName()); + if (newHost.isSubordinate()) { + dnsQueue.addHostRefreshTask(newHost.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); + asyncFlowEnqueuer.enqueueAsyncDnsRefresh(existingHost); } } - private void updateSuperordinateDomains(HostResource existingResource, HostResource newResource) { - if (existingResource.isSubordinate() - && newResource.isSubordinate() + private static void updateSuperordinateDomains(HostResource existingHost, HostResource newHost) { + if (existingHost.isSubordinate() + && newHost.isSubordinate() && Objects.equals( - existingResource.getSuperordinateDomain(), - newResource.getSuperordinateDomain())) { + existingHost.getSuperordinateDomain(), + newHost.getSuperordinateDomain())) { ofy().save().entity( - ofy().load().key(existingResource.getSuperordinateDomain()).now().asBuilder() - .removeSubordinateHost(existingResource.getFullyQualifiedHostName()) - .addSubordinateHost(newResource.getFullyQualifiedHostName()) + ofy().load().key(existingHost.getSuperordinateDomain()).now().asBuilder() + .removeSubordinateHost(existingHost.getFullyQualifiedHostName()) + .addSubordinateHost(newHost.getFullyQualifiedHostName()) .build()); return; } - if (existingResource.isSubordinate()) { + if (existingHost.isSubordinate()) { ofy().save().entity( - ofy().load().key(existingResource.getSuperordinateDomain()).now() + ofy().load().key(existingHost.getSuperordinateDomain()).now() .asBuilder() - .removeSubordinateHost(existingResource.getFullyQualifiedHostName()) + .removeSubordinateHost(existingHost.getFullyQualifiedHostName()) .build()); } - if (newResource.isSubordinate()) { + if (newHost.isSubordinate()) { ofy().save().entity( - ofy().load().key(newResource.getSuperordinateDomain()).now() + ofy().load().key(newHost.getSuperordinateDomain()).now() .asBuilder() - .addSubordinateHost(newResource.getFullyQualifiedHostName()) + .addSubordinateHost(newHost.getFullyQualifiedHostName()) .build()); } } diff --git a/java/google/registry/model/EppResource.java b/java/google/registry/model/EppResource.java index fe0ad5ad8..c80d16c27 100644 --- a/java/google/registry/model/EppResource.java +++ b/java/google/registry/model/EppResource.java @@ -130,7 +130,13 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable { return lastEppUpdateClientId; } - public final String getCurrentSponsorClientId() { + /** + * Get the stored value of {@link #currentSponsorClientId}. + * + *

For subordinate hosts, this value may not represent the actual current client id, which is + * the client id of the superordinate host. For all other resources this is the true client id. + */ + public final String getPersistedCurrentSponsorClientId() { return currentSponsorClientId; } @@ -218,7 +224,7 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable { } /** Set the current sponsoring registrar. */ - public B setCurrentSponsorClientId(String currentSponsorClientId) { + public B setPersistedCurrentSponsorClientId(String currentSponsorClientId) { getInstance().currentSponsorClientId = currentSponsorClientId; return thisCastToDerived(); } diff --git a/java/google/registry/model/EppResourceUtils.java b/java/google/registry/model/EppResourceUtils.java index f7dc445e6..89045bcf5 100644 --- a/java/google/registry/model/EppResourceUtils.java +++ b/java/google/registry/model/EppResourceUtils.java @@ -207,7 +207,7 @@ public final class EppResourceUtils { .setServerApproveAutorenewPollMessage(null) .build()) .setLastTransferTime(transferData.getPendingTransferExpirationTime()) - .setCurrentSponsorClientId(transferData.getGainingClientId()); + .setPersistedCurrentSponsorClientId(transferData.getGainingClientId()); } /** diff --git a/java/google/registry/model/contact/ContactResource.java b/java/google/registry/model/contact/ContactResource.java index 56424df78..7664a7b40 100644 --- a/java/google/registry/model/contact/ContactResource.java +++ b/java/google/registry/model/contact/ContactResource.java @@ -47,8 +47,8 @@ import org.joda.time.DateTime; @ReportedOn @Entity @ExternalMessagingName("contact") -public class ContactResource extends EppResource - implements ForeignKeyedEppResource, ResourceWithTransferData { +public class ContactResource extends EppResource implements + ForeignKeyedEppResource, ResourceWithTransferData { /** * Unique identifier for this contact. @@ -144,6 +144,10 @@ public class ContactResource extends EppResource return disclose; } + public final String getCurrentSponsorClientId() { + return getPersistedCurrentSponsorClientId(); + } + @Override public final TransferData getTransferData() { return Optional.fromNullable(transferData).or(TransferData.EMPTY); diff --git a/java/google/registry/model/domain/DomainBase.java b/java/google/registry/model/domain/DomainBase.java index 559fb1ef9..d96bbcc6b 100644 --- a/java/google/registry/model/domain/DomainBase.java +++ b/java/google/registry/model/domain/DomainBase.java @@ -128,6 +128,10 @@ public abstract class DomainBase extends EppResource { return nullToEmptyImmutableCopy(nsHosts); } + public final String getCurrentSponsorClientId() { + return getPersistedCurrentSponsorClientId(); + } + /** Loads and returns the fully qualified host names of all linked nameservers. */ public ImmutableSortedSet loadNameserverFullyQualifiedHostNames() { return FluentIterable.from(ofy().load().keys(getNameservers()).values()) diff --git a/java/google/registry/model/host/HostResource.java b/java/google/registry/model/host/HostResource.java index 7caaba54c..9621af625 100644 --- a/java/google/registry/model/host/HostResource.java +++ b/java/google/registry/model/host/HostResource.java @@ -17,7 +17,6 @@ package google.registry.model.host; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Sets.difference; import static com.google.common.collect.Sets.union; -import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION; import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; import static google.registry.util.DateTimeUtils.START_OF_TIME; @@ -36,11 +35,10 @@ import google.registry.model.EppResource.ForeignKeyedEppResource; import google.registry.model.annotations.ExternalMessagingName; import google.registry.model.annotations.ReportedOn; import google.registry.model.domain.DomainResource; -import google.registry.model.eppcommon.StatusValue; import google.registry.model.transfer.TransferData; -import google.registry.model.transfer.TransferStatus; import java.net.InetAddress; import java.util.Set; +import javax.annotation.Nullable; import org.joda.time.DateTime; /** @@ -122,37 +120,41 @@ public class HostResource extends EppResource implements ForeignKeyedEppResource return fullyQualifiedHostName; } + @Deprecated @Override public HostResource cloneProjectedAtTime(DateTime now) { - Builder builder = this.asBuilder(); + return this; + } - if (superordinateDomain == null) { - // If this was a subordinate host to a domain that was being transferred, there might be a - // pending transfer still extant, so remove it. - builder.removeStatusValue(StatusValue.PENDING_TRANSFER); - } else { - // For hosts with superordinate domains, the client id, last transfer time, and transfer - // status value need to be read off the domain projected to the correct time. - DomainResource domainAtTime = ofy().load().key(superordinateDomain).now() - .cloneProjectedAtTime(now); - builder.setCurrentSponsorClientId(domainAtTime.getCurrentSponsorClientId()); - // If the superordinate domain's last transfer time is what is relevant, because the host's - // superordinate domain was last changed less recently than the domain's last transfer, then - // use the last transfer time on the domain. - if (Optional.fromNullable(lastSuperordinateChange).or(START_OF_TIME) - .isBefore(Optional.fromNullable(domainAtTime.getLastTransferTime()).or(START_OF_TIME))) { - builder.setLastTransferTime(domainAtTime.getLastTransferTime()); - } - // Copy the transfer status from the superordinate domain onto the host, because the host's - // doesn't matter and the superordinate domain always has the canonical data. - TransferStatus domainTransferStatus = domainAtTime.getTransferData().getTransferStatus(); - if (TransferStatus.PENDING.equals(domainTransferStatus)) { - builder.addStatusValue(StatusValue.PENDING_TRANSFER); - } else { - builder.removeStatusValue(StatusValue.PENDING_TRANSFER); - } + /** + * Compute the correct last transfer time for this host given its loaded superordinate domain. + * + *

Hosts can move between superordinate domains, so to know which lastTransferTime is correct + * we need to know if the host was attached to this superordinate the last time that the + * superordinate was transferred. If the last superordinate change was before this time, then the + * host was attached to this superordinate domain during that transfer. + * + *

If the host is not subordinate the domain can be null and we just return last transfer time. + * + * @param superordinateDomain the loaded superordinate domain, which must match the key in + * the {@link #superordinateDomain} field. Passing it as a parameter allows the caller to + * control the degree of consistency used to load it. + */ + public DateTime computeLastTransferTime(@Nullable DomainResource superordinateDomain) { + if (!isSubordinate()) { + checkArgument(superordinateDomain == null); + return getLastTransferTime(); } - return builder.build(); + checkArgument( + superordinateDomain != null + && Key.create(superordinateDomain).equals(getSuperordinateDomain())); + DateTime lastSuperordinateChange = + Optional.fromNullable(getLastSuperordinateChange()).or(getCreationTime()); + DateTime lastTransferOfCurrentSuperordinate = + Optional.fromNullable(superordinateDomain.getLastTransferTime()).or(START_OF_TIME); + return (lastSuperordinateChange.isBefore(lastTransferOfCurrentSuperordinate)) + ? superordinateDomain.getLastTransferTime() + : getLastTransferTime(); } @Override diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index 46e1b3a62..fc30f323d 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -553,10 +553,19 @@ public class RdapJsonFormatter { if (hasUnicodeComponents(hostResource.getFullyQualifiedHostName())) { jsonBuilder.put("unicodeName", Idn.toUnicode(hostResource.getFullyQualifiedHostName())); } - jsonBuilder.put("status", makeStatusValueList( - isLinked(Key.create(hostResource), now) - ? union(hostResource.getStatusValues(), StatusValue.LINKED) - : hostResource.getStatusValues())); + + ImmutableSet.Builder statuses = new ImmutableSet.Builder<>(); + statuses.addAll(hostResource.getStatusValues()); + if (isLinked(Key.create(hostResource), now)) { + statuses.add(StatusValue.LINKED); + } + if (hostResource.isSubordinate() + && ofy().load().key(hostResource.getSuperordinateDomain()).now().cloneProjectedAtTime(now) + .getStatusValues() + .contains(StatusValue.PENDING_TRANSFER)) { + statuses.add(StatusValue.PENDING_TRANSFER); + } + jsonBuilder.put("status", makeStatusValueList(statuses.build())); jsonBuilder.put("links", ImmutableList.of( makeLink("nameserver", hostResource.getFullyQualifiedHostName(), linkBase))); List> remarks; diff --git a/java/google/registry/rde/HostResourceToXjcConverter.java b/java/google/registry/rde/HostResourceToXjcConverter.java index d2bf6ccc6..b699d246f 100644 --- a/java/google/registry/rde/HostResourceToXjcConverter.java +++ b/java/google/registry/rde/HostResourceToXjcConverter.java @@ -14,7 +14,11 @@ package google.registry.rde; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.common.net.InetAddresses; +import com.googlecode.objectify.Key; +import google.registry.model.domain.DomainResource; import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.HostResource; import google.registry.xjc.host.XjcHostAddrType; @@ -25,28 +29,62 @@ import google.registry.xjc.rdehost.XjcRdeHost; import google.registry.xjc.rdehost.XjcRdeHostElement; import java.net.Inet6Address; import java.net.InetAddress; +import org.joda.time.DateTime; /** Utility class that turns {@link HostResource} as {@link XjcRdeHostElement}. */ final class HostResourceToXjcConverter { - /** Converts {@link HostResource} to {@link XjcRdeHostElement}. */ - static XjcRdeHostElement convert(HostResource host) { - return new XjcRdeHostElement(convertHost(host)); + /** Converts a subordinate {@link HostResource} to {@link XjcRdeHostElement}. */ + static XjcRdeHostElement convertSubordinate( + HostResource host, DomainResource superordinateDomain) { + checkArgument(Key.create(superordinateDomain).equals(host.getSuperordinateDomain())); + return new XjcRdeHostElement(convertSubordinateHost(host, superordinateDomain)); + } + + /** Converts an external {@link HostResource} to {@link XjcRdeHostElement}. */ + static XjcRdeHostElement convertExternal(HostResource host) { + checkArgument(!host.isSubordinate()); + return new XjcRdeHostElement(convertExternalHost(host)); } /** Converts {@link HostResource} to {@link XjcRdeHost}. */ - static XjcRdeHost convertHost(HostResource model) { + static XjcRdeHost convertSubordinateHost(HostResource model, DomainResource superordinateDomain) { + XjcRdeHost bean = convertHostCommon( + model, + superordinateDomain.getCurrentSponsorClientId(), + model.computeLastTransferTime(superordinateDomain)); + if (superordinateDomain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) { + bean.getStatuses().add(convertStatusValue(StatusValue.PENDING_TRANSFER)); + } + return bean; + } + + /** Converts {@link HostResource} to {@link XjcRdeHost}. */ + static XjcRdeHost convertExternalHost(HostResource model) { + return convertHostCommon( + model, + model.getPersistedCurrentSponsorClientId(), + model.getLastTransferTime()); + } + + private static XjcRdeHost convertHostCommon( + HostResource model, String clientId, DateTime lastTransferTime) { XjcRdeHost bean = new XjcRdeHost(); bean.setName(model.getFullyQualifiedHostName()); bean.setRoid(model.getRepoId()); - bean.setClID(model.getCurrentSponsorClientId()); - bean.setTrDate(model.getLastTransferTime()); bean.setCrDate(model.getCreationTime()); bean.setUpDate(model.getLastEppUpdateTime()); bean.setCrRr(RdeAdapter.convertRr(model.getCreationClientId(), null)); bean.setUpRr(RdeAdapter.convertRr(model.getLastEppUpdateClientId(), null)); bean.setCrRr(RdeAdapter.convertRr(model.getCreationClientId(), null)); + bean.setClID(clientId); + bean.setTrDate(lastTransferTime); for (StatusValue status : model.getStatusValues()) { + // TODO(b/34844887): Remove when PENDING_TRANSFER is not persisted on host resources. + if (status.equals(StatusValue.PENDING_TRANSFER)) { + continue; + } + // TODO(cgoldfeder): Add in LINKED status if applicable. bean.getStatuses().add(convertStatusValue(status)); } for (InetAddress addr : model.getInetAddresses()) { diff --git a/java/google/registry/rde/RdeMarshaller.java b/java/google/registry/rde/RdeMarshaller.java index 227e7ada4..4a9c20907 100644 --- a/java/google/registry/rde/RdeMarshaller.java +++ b/java/google/registry/rde/RdeMarshaller.java @@ -117,9 +117,16 @@ public final class RdeMarshaller implements Serializable { } /** Turns {@link HostResource} object into an XML fragment. */ - public DepositFragment marshalHost(HostResource host) { + public DepositFragment marshalSubordinateHost( + HostResource host, DomainResource superordinateDomain) { return marshalResource(RdeResourceType.HOST, host, - HostResourceToXjcConverter.convert(host)); + HostResourceToXjcConverter.convertSubordinate(host, superordinateDomain)); + } + + /** Turns {@link HostResource} object into an XML fragment. */ + public DepositFragment marshalExternalHost(HostResource host) { + return marshalResource(RdeResourceType.HOST, host, + HostResourceToXjcConverter.convertExternal(host)); } /** Turns {@link Registrar} object into an XML fragment. */ diff --git a/java/google/registry/rde/RdeStagingMapper.java b/java/google/registry/rde/RdeStagingMapper.java index 91ecebaff..6385742ee 100644 --- a/java/google/registry/rde/RdeStagingMapper.java +++ b/java/google/registry/rde/RdeStagingMapper.java @@ -15,6 +15,7 @@ package google.registry.rde; import static com.google.common.base.Strings.nullToEmpty; +import static google.registry.model.EppResourceUtils.loadAtPointInTime; import static google.registry.model.ofy.ObjectifyService.ofy; import com.google.appengine.tools.mapreduce.Mapper; @@ -28,7 +29,6 @@ import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Maps; import com.googlecode.objectify.Result; import google.registry.model.EppResource; -import google.registry.model.EppResourceUtils; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DomainResource; import google.registry.model.host.HostResource; @@ -77,7 +77,7 @@ public final class RdeStagingMapper extends Mapper>() { @Override public Result apply(DateTime input) { - return EppResourceUtils.loadAtPointInTime(resource, input); + return loadAtPointInTime(resource, input); }})); // Convert resource to an XML fragment for each watermark/mode pair lazily and cache the result. @@ -167,7 +167,14 @@ public final class RdeStagingMapper extends Mapper domain = new DomainResource.Builder() .setRepoId("1-".concat(Ascii.toUpperCase(tld))) .setFullyQualifiedDomainName(label + "." + tld) - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setCreationClientId("TheRegistrar") .setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z")) .setRegistrationExpirationTime(REGISTRATION_EXPIRATION_TIME) @@ -158,7 +157,7 @@ public class DomainTransferFlowTestCase new HostResource.Builder() .setRepoId("2-".concat(Ascii.toUpperCase(tld))) .setFullyQualifiedHostName("ns1." + label + "." + tld) - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setCreationClientId("TheRegistrar") .setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z")) .setSuperordinateDomain(Key.create(domain)) @@ -220,12 +219,6 @@ public class DomainTransferFlowTestCase assertThat(transferData.getServerApproveEntities()).isEmpty(); } - protected void assertTransferFailed(HostResource resource) { - assertAboutEppResources().that(resource) - .doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER).and() - .hasCurrentSponsorClientId("TheRegistrar"); - } - /** Adds a domain that has a pending transfer on it from TheRegistrar to NewRegistrar. */ protected void setupDomainWithPendingTransfer(String label, String tld) throws Exception { setupDomain(label, tld); diff --git a/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java index c0c90a299..2da17830b 100644 --- a/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferRejectFlowTest.java @@ -84,7 +84,6 @@ public class DomainTransferRejectFlowTest // Transfer should have been rejected. Verify correct fields were set. domain = reloadResourceByForeignKey(); assertTransferFailed(domain, TransferStatus.CLIENT_REJECTED); - assertTransferFailed(reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc())); assertAboutDomains().that(domain) .hasRegistrationExpirationTime(originalExpirationTime).and() .hasLastTransferTimeNotEqualTo(clock.nowUtc()).and() diff --git a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java index 27349d3ce..85d7f1f0e 100644 --- a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -24,7 +24,6 @@ import static google.registry.testing.DatastoreHelper.getPollMessages; import static google.registry.testing.DatastoreHelper.persistActiveContact; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.DomainResourceSubject.assertAboutDomains; -import static google.registry.testing.GenericEppResourceSubject.assertAboutEppResources; import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries; import static google.registry.testing.HostResourceSubject.assertAboutHosts; import static google.registry.util.DateTimeUtils.START_OF_TIME; @@ -64,7 +63,6 @@ import google.registry.model.domain.GracePeriod; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.StatusValue; -import google.registry.model.host.HostResource; import google.registry.model.poll.PendingActionNotificationResponse; import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; @@ -126,12 +124,6 @@ public class DomainTransferRequestFlowTest .hasStatusValue(StatusValue.PENDING_TRANSFER); } - private void assertTransferRequested(HostResource host) throws Exception { - assertAboutEppResources().that(host) - .hasCurrentSponsorClientId("TheRegistrar").and() - .hasStatusValue(StatusValue.PENDING_TRANSFER); - } - private void assertTransferApproved(DomainResource domain) { DateTime afterAutoAck = clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength()); @@ -143,15 +135,6 @@ public class DomainTransferRequestFlowTest .doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER); } - private void assertTransferApproved(HostResource host) { - DateTime afterAutoAck = - clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength()); - assertAboutHosts().that(host) - .hasCurrentSponsorClientId("NewRegistrar").and() - .hasLastTransferTime(afterAutoAck).and() - .doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER); - } - /** * Runs a successful test. The extraExpectedBillingEvents parameter consists of cancellation * billing event builders that have had all of their attributes set except for the parent history @@ -183,7 +166,6 @@ public class DomainTransferRequestFlowTest int registrationYears = domain.getTransferData().getExtendedRegistrationYears(); subordinateHost = reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc()); assertTransferRequested(domain); - assertTransferRequested(subordinateHost); assertAboutDomains().that(domain) .hasPendingTransferExpirationTime(implicitTransferTime).and() .hasOneHistoryEntryEachOfTypes( @@ -252,8 +234,6 @@ public class DomainTransferRequestFlowTest null), transferBillingEvent)); assertTransferApproved(domainAfterAutomaticTransfer); - assertTransferApproved( - subordinateHost.cloneProjectedAtTime(implicitTransferTime)); // Two poll messages on the gaining registrar's side at the expected expiration time: a // (OneTime) transfer approved message, and an Autorenew poll message. diff --git a/javatests/google/registry/flows/host/HostDeleteFlowTest.java b/javatests/google/registry/flows/host/HostDeleteFlowTest.java index 4dfd0f509..5b534a65f 100644 --- a/javatests/google/registry/flows/host/HostDeleteFlowTest.java +++ b/javatests/google/registry/flows/host/HostDeleteFlowTest.java @@ -35,9 +35,14 @@ import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException; import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException; import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException; import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException; +import google.registry.model.domain.DomainResource; import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.HostResource; +import google.registry.model.registry.Registry; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferData; +import google.registry.model.transfer.TransferStatus; +import org.joda.time.DateTime; import org.junit.Before; import org.junit.Test; @@ -52,7 +57,7 @@ public class HostDeleteFlowTest extends ResourceFlowTestCase exception) throws Exception { persistResource( - newHostResource(getUniqueIdFromCommand()).asBuilder() + newHostResource("ns1.example.tld").asBuilder() .setStatusValues(ImmutableSet.of(statusValue)) .build()); thrown.expect(exception); @@ -61,13 +66,13 @@ public class HostDeleteFlowTest extends ResourceFlowTestCaseThe transfer is from "TheRegistrar" to "NewRegistrar". + */ + private DomainResource createDomainWithServerApprovedTransfer(String domainName) { + DateTime now = clock.nowUtc(); + DateTime requestTime = now.minusDays(1).minus(Registry.DEFAULT_AUTOMATIC_TRANSFER_LENGTH); + DateTime transferExpirationTime = now.minusDays(1); + return newDomainResource(domainName).asBuilder() + .setPersistedCurrentSponsorClientId("TheRegistrar") + .addStatusValue(StatusValue.PENDING_TRANSFER) + .setTransferData(new TransferData.Builder() + .setTransferStatus(TransferStatus.PENDING) + .setGainingClientId("NewRegistrar") + .setTransferRequestTime(requestTime) + .setLosingClientId("TheRegistrar") + .setPendingTransferExpirationTime(transferExpirationTime) + .setExtendedRegistrationYears(1) + .build()) + .build(); + } + /** Alias for better readability. */ private String oldHostName() throws Exception { return getUniqueIdFromCommand(); @@ -149,8 +179,7 @@ public class HostUpdateFlowTest extends ResourceFlowTestCase oldFkiAfterRename = - ForeignKeyIndex.load( - HostResource.class, oldHostName(), clock.nowUtc()); + ForeignKeyIndex.load(HostResource.class, oldHostName(), clock.nowUtc()); assertThat(oldFkiAfterRename).isNull(); } @@ -171,18 +200,40 @@ public class HostUpdateFlowTest extends ResourceFlowTestCase192.0.2.22", "1080:0:0:0:8:800:200C:417A"); createTld("tld"); + DateTime now = clock.nowUtc(); + DateTime oneDayAgo = now.minusDays(1); DomainResource domain = persistResource(newDomainResource("example.tld") .asBuilder() .setSubordinateHosts(ImmutableSet.of(oldHostName())) + .setLastTransferTime(oneDayAgo) .build()); HostResource oldHost = persistActiveSubordinateHost(oldHostName(), domain); assertThat(domain.getSubordinateHosts()).containsExactly("ns1.example.tld"); HostResource renamedHost = doSuccessfulTest(); assertAboutHosts().that(renamedHost) .hasSuperordinateDomain(Key.create(domain)).and() - .hasLastSuperordinateChange(oldHost.getLastSuperordinateChange()); + .hasLastSuperordinateChange(oldHost.getLastSuperordinateChange()).and() + .hasPersistedCurrentSponsorClientId("TheRegistrar").and() + .hasLastTransferTime(oneDayAgo); DomainResource reloadedDomain = - ofy().load().entity(domain).now().cloneProjectedAtTime(clock.nowUtc()); + ofy().load().entity(domain).now().cloneProjectedAtTime(now); assertThat(reloadedDomain.getSubordinateHosts()).containsExactly("ns2.example.tld"); assertDnsTasksEnqueued("ns1.example.tld", "ns2.example.tld"); } @@ -231,7 +287,9 @@ public class HostUpdateFlowTest extends ResourceFlowTestCase192.0.2.22", + null); + sessionMetadata.setClientId("TheRegistrar"); + createTld("tld"); + persistResource(newDomainResource("example.tld").asBuilder() + .setPersistedCurrentSponsorClientId("NewRegistar") + .build()); + HostResource host = persistActiveHost("ns1.example.foo"); + assertAboutHosts().that(host).hasPersistedCurrentSponsorClientId("TheRegistrar"); + + thrown.expect(HostDomainNotOwnedException.class); + runFlow(); + } + + @Test + public void testFailure_newSuperordinateWasTransferredToDifferentRegistrar() throws Exception { + setEppHostUpdateInput( + "ns1.example.foo", + "ns2.example.tld", + "192.0.2.22", + null); + sessionMetadata.setClientId("TheRegistrar"); + createTld("tld"); + // The domain will belong to NewRegistrar after cloneProjectedAtTime is called. + DomainResource domain = persistResource(createDomainWithServerApprovedTransfer("example.tld")); + assertAboutDomains().that(domain).hasPersistedCurrentSponsorClientId("TheRegistrar"); + HostResource host = persistActiveHost("ns1.example.foo"); + assertAboutHosts().that(host).hasPersistedCurrentSponsorClientId("TheRegistrar"); + + thrown.expect(HostDomainNotOwnedException.class); + runFlow(); + } + + @Test + public void testSuccess_newSuperordinateWasTransferredToCorrectRegistrar() throws Exception { + setEppHostUpdateInput( + "ns1.example.foo", + "ns2.example.tld", + "192.0.2.22", + null); + sessionMetadata.setClientId("NewRegistrar"); + createTld("tld"); + // The domain will belong to NewRegistrar after cloneProjectedAtTime is called. + DomainResource domain = persistResource(createDomainWithServerApprovedTransfer("example.tld")); + assertAboutDomains().that(domain).hasPersistedCurrentSponsorClientId("TheRegistrar"); + persistResource(newHostResource("ns1.example.foo").asBuilder() + .setPersistedCurrentSponsorClientId("NewRegistrar") + .build()); + + clock.advanceOneMilli(); + runFlowAssertResponse(readFile("host_update_response.xml")); + } + private void doFailingHostNameTest( String hostName, Class exception) throws Exception { @@ -941,6 +1141,19 @@ public class HostUpdateFlowTest extends ResourceFlowTestCase element that contains the identifier of the registrar + // that created the domain name object. An OPTIONAL client attribute + // is used to specify the client that performed the operation. + // This will always be null for us since we track each registrar as a separate client. + assertThat(bean.getCrRr().getValue()).isEqualTo("LawyerCat"); + assertThat(bean.getCrRr().getClient()).isNull(); + + assertThat(bean.getName()).isEqualTo("ns1.love.foobar"); + + assertThat(bean.getRoid()).isEqualTo("2-roid"); + + assertThat(bean.getStatuses()).hasSize(2); + XjcHostStatusType status0 = bean.getStatuses().get(0); + assertThat(status0.getS()).isEqualTo(XjcHostStatusValueType.OK); + assertThat(status0.getValue()).isNull(); + assertThat(status0.getLang()).isEqualTo("en"); + + assertThat(bean.getUpDate()).isEqualTo(DateTime.parse("1920-01-01T00:00:00Z")); + + assertThat(bean.getUpRr().getValue()).isEqualTo("CeilingCat"); + assertThat(bean.getUpRr().getClient()).isNull(); + + // Values that should have been copied from the superordinate domain. + assertThat(bean.getClID()).isEqualTo("LeisureDog"); + assertThat(bean.getTrDate()).isEqualTo(DateTime.parse("2010-01-01T00:00:00Z")); + XjcHostStatusType status1 = bean.getStatuses().get(1); + assertThat(status1.getS()).isEqualTo(XjcHostStatusValueType.PENDING_TRANSFER); + assertThat(status1.getValue()).isNull(); + assertThat(status1.getLang()).isEqualTo("en"); + } + + @Test + public void testConvertExternalHost() throws Exception { + XjcRdeHost bean = HostResourceToXjcConverter.convertExternalHost( + new HostResource.Builder() + .setCreationClientId("LawyerCat") + .setCreationTimeForTest(DateTime.parse("1900-01-01T00:00:00Z")) + .setPersistedCurrentSponsorClientId("BusinessCat") .setFullyQualifiedHostName("ns1.love.lol") .setInetAddresses(ImmutableSet.of(InetAddresses.forString("127.0.0.1"))) .setLastTransferTime(DateTime.parse("1910-01-01T00:00:00Z")) @@ -95,7 +159,6 @@ public class HostResourceToXjcConverterTest { assertThat(bean.getStatuses()).hasSize(1); assertThat(bean.getStatuses().get(0).getS()).isEqualTo(XjcHostStatusValueType.OK); - assertThat(bean.getStatuses().get(0).getS().toString()).isEqualTo("OK"); assertThat(bean.getStatuses().get(0).getValue()).isNull(); assertThat(bean.getStatuses().get(0).getLang()).isEqualTo("en"); @@ -108,12 +171,12 @@ public class HostResourceToXjcConverterTest { } @Test - public void testConvertIpv6() throws Exception { - XjcRdeHost bean = HostResourceToXjcConverter.convertHost( + public void testConvertExternalHost_ipv6() throws Exception { + XjcRdeHost bean = HostResourceToXjcConverter.convertExternalHost( new HostResource.Builder() .setCreationClientId("LawyerCat") .setCreationTimeForTest(DateTime.parse("1900-01-01T00:00:00Z")) - .setCurrentSponsorClientId("BusinessCat") + .setPersistedCurrentSponsorClientId("BusinessCat") .setFullyQualifiedHostName("ns1.love.lol") .setInetAddresses(ImmutableSet.of(InetAddresses.forString("cafe::abba"))) .setLastTransferTime(DateTime.parse("1910-01-01T00:00:00Z")) @@ -130,11 +193,11 @@ public class HostResourceToXjcConverterTest { @Test public void testHostStatusValueIsInvalid() throws Exception { thrown.expect(IllegalArgumentException.class); - HostResourceToXjcConverter.convertHost( + HostResourceToXjcConverter.convertExternalHost( new HostResource.Builder() .setCreationClientId("LawyerCat") .setCreationTimeForTest(DateTime.parse("1900-01-01T00:00:00Z")) - .setCurrentSponsorClientId("BusinessCat") + .setPersistedCurrentSponsorClientId("BusinessCat") .setFullyQualifiedHostName("ns1.love.lol") .setInetAddresses(ImmutableSet.of(InetAddresses.forString("cafe::abba"))) .setLastTransferTime(DateTime.parse("1910-01-01T00:00:00Z")) @@ -148,11 +211,11 @@ public class HostResourceToXjcConverterTest { @Test public void testMarshal() throws Exception { // Bean! Bean! Bean! - XjcRdeHostElement bean = HostResourceToXjcConverter.convert( + XjcRdeHostElement bean = HostResourceToXjcConverter.convertExternal( new HostResource.Builder() .setCreationClientId("LawyerCat") .setCreationTimeForTest(DateTime.parse("1900-01-01T00:00:00Z")) - .setCurrentSponsorClientId("BusinessCat") + .setPersistedCurrentSponsorClientId("BusinessCat") .setFullyQualifiedHostName("ns1.love.lol") .setInetAddresses(ImmutableSet.of(InetAddresses.forString("cafe::abba"))) .setLastTransferTime(DateTime.parse("1910-01-01T00:00:00Z")) @@ -163,5 +226,4 @@ public class HostResourceToXjcConverterTest { .build()); marshalStrict(bean, new ByteArrayOutputStream(), UTF_8); } - } diff --git a/javatests/google/registry/rde/RdeFixtures.java b/javatests/google/registry/rde/RdeFixtures.java index 46d6f0259..f2386e835 100644 --- a/javatests/google/registry/rde/RdeFixtures.java +++ b/javatests/google/registry/rde/RdeFixtures.java @@ -90,7 +90,7 @@ final class RdeFixtures { makeContactResource(clock, "5372808-TRL", "bird or fiend!? i shrieked upstarting", "bog@cat.みんな"))))) .setCreationClientId("TheRegistrar") - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setCreationTimeForTest(clock.nowUtc()) .setDsData(ImmutableSet.of(DelegationSignerData.create( 123, 200, 230, base16().decode("1234567890")))) @@ -194,7 +194,7 @@ final class RdeFixtures { .setRepoId(generateNewContactHostRoid()) .setEmailAddress(email) .setStatusValues(ImmutableSet.of(StatusValue.OK)) - .setCurrentSponsorClientId("GetTheeBack") + .setPersistedCurrentSponsorClientId("GetTheeBack") .setCreationClientId("GetTheeBack") .setCreationTimeForTest(clock.nowUtc()) .setInternationalizedPostalInfo(new PostalInfo.Builder() @@ -227,7 +227,7 @@ final class RdeFixtures { .setRepoId(generateNewContactHostRoid()) .setCreationClientId("LawyerCat") .setCreationTimeForTest(clock.nowUtc()) - .setCurrentSponsorClientId("BusinessCat") + .setPersistedCurrentSponsorClientId("BusinessCat") .setFullyQualifiedHostName(Idn.toASCII(fqhn)) .setInetAddresses(ImmutableSet.of(InetAddresses.forString(ip))) .setLastTransferTime(DateTime.parse("1910-01-01T00:00:00Z")) diff --git a/javatests/google/registry/rde/imports/RdeHostImportActionTest.java b/javatests/google/registry/rde/imports/RdeHostImportActionTest.java index b26b9d8a6..8ad0f5824 100644 --- a/javatests/google/registry/rde/imports/RdeHostImportActionTest.java +++ b/javatests/google/registry/rde/imports/RdeHostImportActionTest.java @@ -16,7 +16,6 @@ package google.registry.rde.imports; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.getHistoryEntries; import static google.registry.testing.DatastoreHelper.newHostResource; import static google.registry.testing.DatastoreHelper.persistResource; @@ -67,7 +66,6 @@ public class RdeHostImportActionTest extends MapreduceTestCaseabsent(), Optional.absent()); action = new RdeHostImportAction( @@ -106,12 +104,12 @@ public class RdeHostImportActionTest extends MapreduceTestCase hasCurrentSponsorClientId(String clientId) { + return hasValue( + clientId, + actual().getCurrentSponsorClientId(), + "has currentSponsorClientId"); + } } diff --git a/javatests/google/registry/testing/AbstractEppResourceSubject.java b/javatests/google/registry/testing/AbstractEppResourceSubject.java index 75b648142..0b514708f 100644 --- a/javatests/google/registry/testing/AbstractEppResourceSubject.java +++ b/javatests/google/registry/testing/AbstractEppResourceSubject.java @@ -172,11 +172,12 @@ abstract class AbstractEppResourceSubject "has lastEppUpdateClientId"); } - public And hasCurrentSponsorClientId(String clientId) { + + public And hasPersistedCurrentSponsorClientId(String clientId) { return hasValue( clientId, - actual().getCurrentSponsorClientId(), - "has currentSponsorClientId"); + actual().getPersistedCurrentSponsorClientId(), + "has persisted currentSponsorClientId"); } public And isActiveAt(DateTime time) { diff --git a/javatests/google/registry/testing/ContactResourceSubject.java b/javatests/google/registry/testing/ContactResourceSubject.java index 0991e63da..3fc7dbf0a 100644 --- a/javatests/google/registry/testing/ContactResourceSubject.java +++ b/javatests/google/registry/testing/ContactResourceSubject.java @@ -175,6 +175,14 @@ public final class ContactResourceSubject actual().getLastTransferTime(), "lastTransferTime"); } + + public And hasCurrentSponsorClientId(String clientId) { + return hasValue( + clientId, + actual().getCurrentSponsorClientId(), + "has currentSponsorClientId"); + } + public static DelegatedVerb assertAboutContacts() { return assertAbout(new SubjectFactory()); } diff --git a/javatests/google/registry/testing/DatastoreHelper.java b/javatests/google/registry/testing/DatastoreHelper.java index 037ff46cd..9c2963da9 100644 --- a/javatests/google/registry/testing/DatastoreHelper.java +++ b/javatests/google/registry/testing/DatastoreHelper.java @@ -116,7 +116,7 @@ public class DatastoreHelper { return new HostResource.Builder() .setFullyQualifiedHostName(hostName) .setCreationClientId("TheRegistrar") - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setCreationTimeForTest(START_OF_TIME) .setRepoId(generateNewContactHostRoid()) .build(); @@ -146,7 +146,7 @@ public class DatastoreHelper { .setRepoId(repoId) .setFullyQualifiedDomainName(domainName) .setCreationClientId("TheRegistrar") - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setCreationTimeForTest(START_OF_TIME) .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR"))) .setRegistrant(contactKey) @@ -186,7 +186,7 @@ public class DatastoreHelper { return new DomainApplication.Builder() .setRepoId(repoId) .setFullyQualifiedDomainName(domainName) - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR"))) .setRegistrant(contactKey) .setContacts(ImmutableSet.of( @@ -223,7 +223,7 @@ public class DatastoreHelper { .setRepoId(repoId) .setContactId(contactId) .setCreationClientId("TheRegistrar") - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("2fooBAR"))) .setCreationTimeForTest(START_OF_TIME) .build(); @@ -486,7 +486,7 @@ public class DatastoreHelper { .build()); return persistResource( contact.asBuilder() - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .addStatusValue(StatusValue.PENDING_TRANSFER) .setTransferData(createTransferDataBuilder(requestTime, expirationTime) .setPendingTransferExpirationTime(now.plus(getContactAutomaticTransferLength())) @@ -571,7 +571,7 @@ public class DatastoreHelper { Builder transferDataBuilder = createTransferDataBuilder( requestTime, expirationTime, extendedRegistrationYears); return persistResource(domain.asBuilder() - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .addStatusValue(StatusValue.PENDING_TRANSFER) .setTransferData(transferDataBuilder .setPendingTransferExpirationTime(expirationTime) diff --git a/javatests/google/registry/testing/FullFieldsTestEntityHelper.java b/javatests/google/registry/testing/FullFieldsTestEntityHelper.java index daa0a3b65..eff412fb7 100644 --- a/javatests/google/registry/testing/FullFieldsTestEntityHelper.java +++ b/javatests/google/registry/testing/FullFieldsTestEntityHelper.java @@ -225,7 +225,7 @@ public final class FullFieldsTestEntityHelper { .setLastEppUpdateTime(DateTime.parse("2009-05-29T20:13:00Z")) .setCreationTimeForTest(DateTime.parse("2000-10-08T00:45:00Z")) .setRegistrationExpirationTime(DateTime.parse("2110-10-08T00:44:59Z")) - .setCurrentSponsorClientId(registrar.getClientId()) + .setPersistedCurrentSponsorClientId(registrar.getClientId()) .setStatusValues(ImmutableSet.of( StatusValue.CLIENT_DELETE_PROHIBITED, StatusValue.CLIENT_RENEW_PROHIBITED, diff --git a/javatests/google/registry/tools/GenerateAuctionDataCommandTest.java b/javatests/google/registry/tools/GenerateAuctionDataCommandTest.java index 7e7cb8a8d..9dc63dfd6 100644 --- a/javatests/google/registry/tools/GenerateAuctionDataCommandTest.java +++ b/javatests/google/registry/tools/GenerateAuctionDataCommandTest.java @@ -167,7 +167,7 @@ public class GenerateAuctionDataCommandTest extends CommandTestCase { createTld("xn--q9jyb4c"); domainApplication = persistResource(newDomainApplication("test-validate.xn--q9jyb4c") .asBuilder() - .setCurrentSponsorClientId("TheRegistrar") + .setPersistedCurrentSponsorClientId("TheRegistrar") .setEncodedSignedMarks(ImmutableList.of(EncodedSignedMark.create("base64", "garbage"))) .build()); command.tmchUtils = diff --git a/javatests/google/registry/whois/DomainWhoisResponseTest.java b/javatests/google/registry/whois/DomainWhoisResponseTest.java index f83cd3a5e..a458acba2 100644 --- a/javatests/google/registry/whois/DomainWhoisResponseTest.java +++ b/javatests/google/registry/whois/DomainWhoisResponseTest.java @@ -218,7 +218,7 @@ public class DomainWhoisResponseTest { .setLastEppUpdateTime(DateTime.parse("2009-05-29T20:13:00Z")) .setCreationTimeForTest(DateTime.parse("2000-10-08T00:45:00Z")) .setRegistrationExpirationTime(DateTime.parse("2010-10-08T00:44:59Z")) - .setCurrentSponsorClientId("NewRegistrar") + .setPersistedCurrentSponsorClientId("NewRegistrar") .setStatusValues(ImmutableSet.of( StatusValue.CLIENT_DELETE_PROHIBITED, StatusValue.CLIENT_RENEW_PROHIBITED, diff --git a/javatests/google/registry/whois/NameserverWhoisResponseTest.java b/javatests/google/registry/whois/NameserverWhoisResponseTest.java index 070050247..ef6e9ab01 100644 --- a/javatests/google/registry/whois/NameserverWhoisResponseTest.java +++ b/javatests/google/registry/whois/NameserverWhoisResponseTest.java @@ -61,7 +61,7 @@ public class NameserverWhoisResponseTest { hostResource1 = new HostResource.Builder() .setFullyQualifiedHostName("ns1.example.tld") - .setCurrentSponsorClientId("example") + .setPersistedCurrentSponsorClientId("example") .setInetAddresses(ImmutableSet.of( InetAddresses.forString("192.0.2.123"), InetAddresses.forString("2001:0DB8::1"))) @@ -70,7 +70,7 @@ public class NameserverWhoisResponseTest { hostResource2 = new HostResource.Builder() .setFullyQualifiedHostName("ns2.example.tld") - .setCurrentSponsorClientId("example") + .setPersistedCurrentSponsorClientId("example") .setInetAddresses(ImmutableSet.of( InetAddresses.forString("192.0.2.123"), InetAddresses.forString("2001:0DB8::1")))