google-nomulus/java/google/registry/flows/domain/DomainTransferRequestFlow.java
mcilwain 6f2e663b72 Add asynchronous scheduled actions to re-save entities
This is used in the domain transfer and delete flows, both of which are
asynchronous flows that have implicit default actions that will be taken at some
point in the future. This CL adds scheduled re-saves to take place soon after
those default actions would become effective, so that they can be re-saved
quickly if so.

Unfortunately the redemption grace period on our TLDs is 35 days, which exceeds
the 30 day maximum task ETA in App Engine, so these won't actually fire.  That's
fine though; the deletion is actually effective as of 5 days, and this is just
removing the grace period.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=201345274
2018-06-27 15:28:52 -04:00

350 lines
18 KiB
Java

// 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.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.
*
* <p>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.
*
* <p>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.UnsupportedFeeAttributeException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
public final class DomainTransferRequestFlow implements TransactionalFlow {
private static final ImmutableSet<StatusValue> 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> 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);
DateTime now = ofy().getTransactionTime();
DomainResource existingDomain = loadAndVerifyExistence(DomainResource.class, targetId, now);
Optional<DomainTransferRequestSuperuserExtension> 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<FeeTransferCommandExtension> 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> 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<TransferServerApproveEntity> 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<DomainTransferRequestSuperuserExtension> 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.
*
* <p>Restricting transfers to one year is seemingly required by ICANN's <a
* href="https://www.icann.org/resources/pages/policy-2012-03-07-en">Policy on Transfer of
* Registrations between Registrars</a>, 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."
*
* <p>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 <a href="https://tools.ietf.org/html/rfc5731#section-3.2.4">EPP Domain RFC,
* section 3.2.4</a> 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.
*
* <p>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<DomainTransferRequestSuperuserExtension> 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<FeeTransformResponseExtension> createResponseExtensions(
Optional<FeesAndCredits> feesAndCredits, Optional<FeeTransferCommandExtension> 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();
}
}