// Copyright 2017 The Nomulus Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package google.registry.flows.domain; import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; import static google.registry.flows.ResourceFlowUtils.verifyAuthInfo; import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoPresentForResourceTransfer; import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime; import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge; import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked; import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive; import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears; import static google.registry.flows.domain.DomainTransferUtils.createLosingTransferPollMessage; import static google.registry.flows.domain.DomainTransferUtils.createPendingTransferData; import static google.registry.flows.domain.DomainTransferUtils.createTransferResponse; import static google.registry.flows.domain.DomainTransferUtils.createTransferServerApproveEntities; import static google.registry.model.domain.DomainResource.extendRegistrationWithCap; import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING; import static google.registry.model.ofy.ObjectifyService.ofy; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.flows.EppException; import google.registry.flows.ExtensionManager; import google.registry.flows.FlowModule.ClientId; import google.registry.flows.FlowModule.Superuser; import google.registry.flows.FlowModule.TargetId; import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.async.AsyncFlowEnqueuer; import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.InvalidTransferPeriodValueException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException; import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException; import google.registry.model.domain.DomainCommand.Transfer; import google.registry.model.domain.DomainResource; import google.registry.model.domain.Period; import google.registry.model.domain.fee.FeeTransferCommandExtension; import google.registry.model.domain.fee.FeeTransformResponseExtension; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.Trid; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppResponse; import google.registry.model.poll.PollMessage; import google.registry.model.registry.Registry; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.IcannReportingTypes.ActivityReportField; import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferData.TransferServerApproveEntity; import google.registry.model.transfer.TransferResponse.DomainTransferResponse; import google.registry.model.transfer.TransferStatus; import java.util.Optional; import javax.inject.Inject; import org.joda.time.DateTime; /** * An EPP flow that requests a transfer on a domain. * *

The "gaining" registrar requests a transfer from the "losing" (aka current) registrar. The * losing registrar has a "transfer" time period to respond (by default five days) after which the * transfer is automatically approved. Within that window, the transfer might be approved explicitly * by the losing registrar or rejected, and the gaining registrar can also cancel the transfer * request. * *

When a transfer is requested, poll messages and billing events are saved to Datastore with * timestamps such that they only become active when the server-approval period passes. Keys to * these speculative objects are saved in the domain's transfer data, and on explicit approval, * rejection or cancellation of the request, they will be deleted (and in the approval case, * replaced with new ones with the correct approval time). * * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException} * @error {@link google.registry.flows.exceptions.AlreadyPendingTransferException} * @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException} * @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException} * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} * @error {@link google.registry.flows.exceptions.TransferPeriodMustBeOneYearException} * @error {@link InvalidTransferPeriodValueException} * @error {@link google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException} * @error {@link DomainFlowUtils.BadPeriodUnitException} * @error {@link DomainFlowUtils.CurrencyUnitMismatchException} * @error {@link DomainFlowUtils.CurrencyValueScaleException} * @error {@link DomainFlowUtils.FeesMismatchException} * @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException} * @error {@link DomainFlowUtils.NotAuthorizedForTldException} * @error {@link DomainFlowUtils.PremiumNameBlockedException} * @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException} * @error {@link DomainFlowUtils.UnsupportedFeeAttributeException} */ @ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST) public final class DomainTransferRequestFlow implements TransactionalFlow { private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( StatusValue.CLIENT_TRANSFER_PROHIBITED, StatusValue.PENDING_DELETE, StatusValue.SERVER_TRANSFER_PROHIBITED); @Inject ResourceCommand resourceCommand; @Inject ExtensionManager extensionManager; @Inject EppInput eppInput; @Inject Optional authInfo; @Inject @ClientId String gainingClientId; @Inject @TargetId String targetId; @Inject @Superuser boolean isSuperuser; @Inject HistoryEntry.Builder historyBuilder; @Inject Trid trid; @Inject AsyncFlowEnqueuer asyncFlowEnqueuer; @Inject EppResponse.Builder responseBuilder; @Inject DomainPricingLogic pricingLogic; @Inject DomainTransferRequestFlow() {} @Override public final EppResponse run() throws EppException { extensionManager.register( DomainTransferRequestSuperuserExtension.class, FeeTransferCommandExtension.class, MetadataExtension.class); extensionManager.validate(); validateClientIsLoggedIn(gainingClientId); verifyRegistrarIsActive(gainingClientId); DateTime now = ofy().getTransactionTime(); DomainResource existingDomain = loadAndVerifyExistence(DomainResource.class, targetId, now); Optional superuserExtension = eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class); Period period = superuserExtension.isPresent() ? superuserExtension.get().getRenewalPeriod() : ((Transfer) resourceCommand).getPeriod(); verifyTransferAllowed(existingDomain, period, now, superuserExtension); String tld = existingDomain.getTld(); Registry registry = Registry.get(tld); // An optional extension from the client specifying what they think the transfer should cost. Optional feeTransfer = eppInput.getSingleExtension(FeeTransferCommandExtension.class); if (period.getValue() == 0 && feeTransfer.isPresent()) { // If the period is zero, then there is no transfer billing event, so using the fee transfer // extension does not make sense. throw new TransferPeriodZeroAndFeeTransferExtensionException(); } // If the period is zero, then there is no fee for the transfer. Optional feesAndCredits = (period.getValue() == 0) ? Optional.empty() : Optional.of(pricingLogic.getTransferPrice(registry, targetId, now)); if (feesAndCredits.isPresent()) { validateFeeChallenge(targetId, tld, gainingClientId, now, feeTransfer, feesAndCredits.get()); } HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, period); DateTime automaticTransferTime = superuserExtension.isPresent() ? now.plusDays(superuserExtension.get().getAutomaticTransferLength()) : now.plus(registry.getAutomaticTransferLength()); // If the domain will be in the auto-renew grace period at the moment of transfer, the transfer // will subsume the autorenew, so we don't add the normal extra year from the transfer. // The gaining registrar is still billed for the extra year; the losing registrar will get a // cancellation for the autorenew written out within createTransferServerApproveEntities(). // // See b/19430703#comment17 and https://www.icann.org/news/advisory-2002-06-06-en for the // policy documentation for transfers subsuming autorenews within the autorenew grace period. int extraYears = period.getValue(); DomainResource domainAtTransferTime = existingDomain.cloneProjectedAtTime(automaticTransferTime); if (!domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW).isEmpty()) { extraYears = 0; } // The new expiration time if there is a server approval. DateTime serverApproveNewExpirationTime = extendRegistrationWithCap( automaticTransferTime, domainAtTransferTime.getRegistrationExpirationTime(), extraYears); // Create speculative entities in anticipation of an automatic server approval. ImmutableSet serverApproveEntities = createTransferServerApproveEntities( automaticTransferTime, serverApproveNewExpirationTime, historyEntry, existingDomain, trid, gainingClientId, feesAndCredits.map(FeesAndCredits::getTotalCost), now); // Create the transfer data that represents the pending transfer. TransferData pendingTransferData = createPendingTransferData( new TransferData.Builder() .setTransferRequestTrid(trid) .setTransferRequestTime(now) .setGainingClientId(gainingClientId) .setLosingClientId(existingDomain.getCurrentSponsorClientId()) .setPendingTransferExpirationTime(automaticTransferTime) .setTransferredRegistrationExpirationTime(serverApproveNewExpirationTime), serverApproveEntities, period); // Create a poll message to notify the losing registrar that a transfer was requested. PollMessage requestPollMessage = createLosingTransferPollMessage( targetId, pendingTransferData, serverApproveNewExpirationTime, historyEntry) .asBuilder().setEventTime(now).build(); // End the old autorenew event and poll message at the implicit transfer time. This may delete // the poll message if it has no events left. Note that if the automatic transfer succeeds, then // cloneProjectedAtTime() will replace these old autorenew entities with the server approve ones // that we've created in this flow and stored in pendingTransferData. updateAutorenewRecurrenceEndTime(existingDomain, automaticTransferTime); DomainResource newDomain = existingDomain.asBuilder() .setTransferData(pendingTransferData) .addStatusValue(StatusValue.PENDING_TRANSFER) .build(); asyncFlowEnqueuer.enqueueAsyncResave(newDomain, now, automaticTransferTime); ofy().save() .entities(new ImmutableSet.Builder<>() .add(newDomain, historyEntry, requestPollMessage) .addAll(serverApproveEntities) .build()) .now(); return responseBuilder .setResultFromCode(SUCCESS_WITH_ACTION_PENDING) .setResData(createResponse(period, existingDomain, newDomain, now)) .setExtensions(createResponseExtensions(feesAndCredits, feeTransfer)) .build(); } private void verifyTransferAllowed( DomainResource existingDomain, Period period, DateTime now, Optional superuserExtension) throws EppException { verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES); verifyAuthInfoPresentForResourceTransfer(authInfo); verifyAuthInfo(authInfo.get(), existingDomain); // Verify that the resource does not already have a pending transfer. if (TransferStatus.PENDING.equals(existingDomain.getTransferData().getTransferStatus())) { throw new AlreadyPendingTransferException(targetId); } // Verify that this client doesn't already sponsor this resource. if (gainingClientId.equals(existingDomain.getCurrentSponsorClientId())) { throw new ObjectAlreadySponsoredException(); } verifyTransferPeriod(period, superuserExtension); if (!isSuperuser) { checkAllowedAccessToTld(gainingClientId, existingDomain.getTld()); verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId); } } /** * Verify that the transfer period is one year. If the superuser extension is being used, then it * can be zero. * *

Restricting transfers to one year is seemingly required by ICANN's Policy on Transfer of * Registrations between Registrars, section A.8. It states that "the completion by Registry * Operator of a holder-authorized transfer under this Part A shall result in a one-year extension * of the existing registration, provided that in no event shall the total unexpired term of a * registration exceed ten (10) years." * *

Even if not required, this policy is desirable because it dramatically simplifies the logic * in transfer flows. Registrars appear to never request 2+ year transfers in practice, and they * can always decompose an multi-year transfer into a 1-year transfer followed by a manual renewal * afterwards. The EPP Domain RFC, * section 3.2.4 says about EPP transfer periods that "the number of units available MAY be * subject to limits imposed by the server" so we're just limiting the units to one. * *

Note that clients can omit the period element from the transfer EPP entirely, but then it * will simply default to one year. */ private static void verifyTransferPeriod( Period period, Optional superuserExtension) throws EppException { verifyUnitIsYears(period); if (superuserExtension.isPresent()) { // If the superuser extension is being used, then the period can be one or zero. if (period.getValue() != 1 && period.getValue() != 0) { throw new InvalidTransferPeriodValueException(); } } else { // If the superuser extension is not being used, then the period can only be one. if (period.getValue() != 1) { throw new TransferPeriodMustBeOneYearException(); } } } private HistoryEntry buildHistoryEntry( DomainResource existingDomain, Registry registry, DateTime now, Period period) { return historyBuilder .setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST) .setOtherClientId(existingDomain.getCurrentSponsorClientId()) .setPeriod(period) .setModificationTime(now) .setParent(Key.create(existingDomain)) .setDomainTransactionRecords( ImmutableSet.of( DomainTransactionRecord.create( registry.getTldStr(), now.plus(registry.getAutomaticTransferLength()) .plus(registry.getTransferGracePeriodLength()), TransactionReportField.TRANSFER_SUCCESSFUL, 1))) .build(); } private DomainTransferResponse createResponse( Period period, DomainResource existingDomain, DomainResource newDomain, DateTime now) { // If the registration were approved this instant, this is what the new expiration would be, // because we cap at 10 years from the moment of approval. This is different than the server // approval new expiration time, which is capped at 10 years from the server approve time. DateTime approveNowExtendedRegistrationTime = extendRegistrationWithCap( now, existingDomain.getRegistrationExpirationTime(), period.getValue()); return createTransferResponse( targetId, newDomain.getTransferData(), approveNowExtendedRegistrationTime); } private static ImmutableList createResponseExtensions( Optional feesAndCredits, Optional feeTransfer) { return (feeTransfer.isPresent() && feesAndCredits.isPresent()) ? ImmutableList.of( feeTransfer .get() .createResponseBuilder() .setFees(feesAndCredits.get().getFees()) .setCredits(feesAndCredits.get().getCredits()) .setCurrency(feesAndCredits.get().getCurrency()) .build()) : ImmutableList.of(); } }