diff --git a/docs/flows.md b/docs/flows.md index 8d2bd1add..7198970d7 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -896,10 +896,15 @@ new ones with the correct approval time). * Resource with this id does not exist. * 2304 * Resource status prohibits this operation. + * Superuser extensions cannot be used during autorenew grace periods. + * Domain transfer period cannot be zero when using the fee transfer + extension. * The requested domain name is on the premium price list, and this registrar has blocked premium registrations. * 2306 * Domain transfer period must be one year. + * Domain transfer period must be zero or one year when using the superuser + EPP extension. * Periods for domain registrations must be specified in years. * The requested fees cannot be provided in the requested currency. diff --git a/java/google/registry/flows/EppXmlTransformer.java b/java/google/registry/flows/EppXmlTransformer.java index 3e04c4742..02ec1ff41 100644 --- a/java/google/registry/flows/EppXmlTransformer.java +++ b/java/google/registry/flows/EppXmlTransformer.java @@ -63,7 +63,8 @@ public class EppXmlTransformer { "dsig.xsd", "smd.xsd", "launch.xsd", - "allocate.xsd"); + "allocate.xsd", + "superuser.xsd"); private static final XmlTransformer INPUT_TRANSFORMER = new XmlTransformer(SCHEMAS, EppInput.class); diff --git a/java/google/registry/flows/ExtensionManager.java b/java/google/registry/flows/ExtensionManager.java index 033215534..159d7d440 100644 --- a/java/google/registry/flows/ExtensionManager.java +++ b/java/google/registry/flows/ExtensionManager.java @@ -29,8 +29,11 @@ import google.registry.flows.EppException.CommandUseErrorException; import google.registry.flows.EppException.SyntaxErrorException; import google.registry.flows.EppException.UnimplementedExtensionException; import google.registry.flows.FlowModule.ClientId; +import google.registry.flows.FlowModule.Superuser; import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; +import google.registry.flows.exceptions.UnauthorizedForSuperuserExtensionException; import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.domain.superuser.SuperuserExtension; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput.CommandExtension; import google.registry.util.FormattingLogger; @@ -56,6 +59,7 @@ public final class ExtensionManager { @Inject EppInput eppInput; @Inject SessionMetadata sessionMetadata; @Inject @ClientId String clientId; + @Inject @Superuser boolean isSuperuser; @Inject Class flowClass; @Inject EppRequestSource eppRequestSource; @Inject ExtensionManager() {} @@ -107,11 +111,18 @@ public final class ExtensionManager { private void checkForRestrictedExtensions( ImmutableSet> suppliedExtensions) - throws OnlyToolCanPassMetadataException { + throws OnlyToolCanPassMetadataException, UnauthorizedForSuperuserExtensionException { if (suppliedExtensions.contains(MetadataExtension.class) && !eppRequestSource.equals(EppRequestSource.TOOL)) { throw new OnlyToolCanPassMetadataException(); } + // Can't use suppliedExtension.contains() here because the SuperuserExtension has child classes. + for (Class suppliedExtension : suppliedExtensions) { + if (SuperuserExtension.class.isAssignableFrom(suppliedExtension) + && (!eppRequestSource.equals(EppRequestSource.TOOL) || !isSuperuser)) { + throw new UnauthorizedForSuperuserExtensionException(); + } + } } private static void checkForDuplicateExtensions( diff --git a/java/google/registry/flows/domain/DomainTransferApproveFlow.java b/java/google/registry/flows/domain/DomainTransferApproveFlow.java index 491d39545..ae3db2e52 100644 --- a/java/google/registry/flows/domain/DomainTransferApproveFlow.java +++ b/java/google/registry/flows/domain/DomainTransferApproveFlow.java @@ -115,24 +115,29 @@ public final class DomainTransferApproveFlow implements TransactionalFlow { String gainingClientId = transferData.getGainingClientId(); Registry registry = Registry.get(existingDomain.getTld()); HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, gainingClientId); - // Bill for the transfer. - BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder() - .setReason(Reason.TRANSFER) - .setTargetId(targetId) - .setClientId(gainingClientId) - .setPeriodYears(1) - .setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), 1)) - .setEventTime(now) - .setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength())) - .setParent(historyEntry) - .build(); + // Create a transfer billing event for 1 year, unless the superuser extension was used to set + // the transfer period to zero. There is not a transfer cost if the transfer period is zero. + Optional billingEvent = + (transferData.getTransferPeriod().getValue() == 0) + ? Optional.absent() + : Optional.of( + new BillingEvent.OneTime.Builder() + .setReason(Reason.TRANSFER) + .setTargetId(targetId) + .setClientId(gainingClientId) + .setPeriodYears(1) + .setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), 1)) + .setEventTime(now) + .setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength())) + .setParent(historyEntry) + .build()); // If we are within an autorenew grace period, cancel the autorenew billing event and don't // increase the registration time, since the transfer subsumes the autorenew's extra year. - int extraYears = 1; // All transfers are one year. GracePeriod autorenewGrace = getOnlyElement(existingDomain.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null); + int extraYears = transferData.getTransferPeriod().getValue(); if (autorenewGrace != null) { - extraYears--; + extraYears = 0; ofy().save().entity( BillingEvent.Cancellation.forGracePeriod(autorenewGrace, historyEntry, targetId)); } @@ -167,8 +172,11 @@ public final class DomainTransferApproveFlow implements TransactionalFlow { .setAutorenewBillingEvent(Key.create(autorenewEvent)) .setAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage)) // Remove all the old grace periods and add a new one for the transfer. - .setGracePeriods(ImmutableSet.of( - GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent))) + .setGracePeriods( + (billingEvent.isPresent()) + ? ImmutableSet.of( + GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent.get())) + : ImmutableSet.of()) .build(); // Create a poll message for the gaining client. PollMessage gainingClientPollMessage = createGainingTransferPollMessage( @@ -176,13 +184,17 @@ public final class DomainTransferApproveFlow implements TransactionalFlow { newDomain.getTransferData(), newExpirationTime, historyEntry); - ofy().save().entities( + ImmutableSet.Builder entitiesToSave = new ImmutableSet.Builder<>(); + entitiesToSave.add( newDomain, historyEntry, - billingEvent, autorenewEvent, gainingClientPollMessage, gainingClientAutorenewPollMessage); + if (billingEvent.isPresent()) { + entitiesToSave.add(billingEvent.get()); + } + ofy().save().entities(entitiesToSave.build()); // Delete the billing event and poll messages that were written in case the transfer would have // been implicitly server approved. ofy().delete().keys(existingDomain.getTransferData().getServerApproveEntities()); diff --git a/java/google/registry/flows/domain/DomainTransferRequestFlow.java b/java/google/registry/flows/domain/DomainTransferRequestFlow.java index 46b9f4158..9a06e6a09 100644 --- a/java/google/registry/flows/domain/DomainTransferRequestFlow.java +++ b/java/google/registry/flows/domain/DomainTransferRequestFlow.java @@ -44,8 +44,11 @@ import google.registry.flows.FlowModule.TargetId; import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.exceptions.AlreadyPendingTransferException; +import google.registry.flows.exceptions.InvalidTransferPeriodValueException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException; +import google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException; 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; @@ -53,6 +56,7 @@ 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; @@ -94,7 +98,10 @@ import org.joda.time.DateTime; * @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.SuperuserExtensionAndAutorenewGracePeriodException} * @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} @@ -128,24 +135,43 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { @Override public final EppResponse run() throws EppException { extensionManager.register( + DomainTransferRequestSuperuserExtension.class, FeeTransferCommandExtension.class, MetadataExtension.class); extensionManager.validate(); validateClientIsLoggedIn(gainingClientId); - Period period = ((Transfer) resourceCommand).getPeriod(); DateTime now = ofy().getTransactionTime(); DomainResource existingDomain = loadAndVerifyExistence(DomainResource.class, targetId, now); - verifyTransferAllowed(existingDomain, period, now); + DomainTransferRequestSuperuserExtension superuserExtension = + eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class); + Period period = + (superuserExtension == null) + ? ((Transfer) resourceCommand).getPeriod() + : superuserExtension.getRenewalPeriod(); + 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. FeeTransferCommandExtension feeTransfer = eppInput.getSingleExtension(FeeTransferCommandExtension.class); - FeesAndCredits feesAndCredits = pricingLogic.getTransferPrice(registry, targetId, now); - validateFeeChallenge(targetId, tld, now, feeTransfer, feesAndCredits); + if (period.getValue() == 0 && feeTransfer != null) { + // 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.absent() + : Optional.of(pricingLogic.getTransferPrice(registry, targetId, now)); + if (feesAndCredits.isPresent()) { + validateFeeChallenge(targetId, tld, now, feeTransfer, feesAndCredits.get()); + } HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, period); - DateTime automaticTransferTime = now.plus(registry.getAutomaticTransferLength()); - + DateTime automaticTransferTime = + (superuserExtension == null) + ? now.plus(registry.getAutomaticTransferLength()) + : now.plusDays(superuserExtension.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 @@ -153,11 +179,15 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { // // 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 = 1; + int extraYears = period.getValue(); DomainResource domainAtTransferTime = existingDomain.cloneProjectedAtTime(automaticTransferTime); if (!domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW).isEmpty()) { - extraYears--; + if (superuserExtension != null) { + // We don't allow the superuser extension for domains in the auto renew grace period + throw new SuperuserExtensionAndAutorenewGracePeriodException(); + } + extraYears = 0; } // The new expiration time if there is a server approval. DateTime serverApproveNewExpirationTime = @@ -174,12 +204,16 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { existingDomain, trid, gainingClientId, - feesAndCredits.getTotalCost(), + (feesAndCredits.isPresent()) + ? Optional.of(feesAndCredits.get().getTotalCost()) + : Optional.absent(), now); // Create the transfer data that represents the pending transfer. - TransferData pendingTransferData = createPendingTransferData( - createTransferDataBuilder(existingDomain, automaticTransferTime, now), - serverApproveEntities); + TransferData pendingTransferData = + createPendingTransferData( + createTransferDataBuilder(existingDomain, automaticTransferTime, now), + serverApproveEntities, + period); // Create a poll message to notify the losing registrar that a transfer was requested. PollMessage requestPollMessage = createLosingTransferPollMessage( targetId, pendingTransferData, serverApproveNewExpirationTime, historyEntry) @@ -206,7 +240,11 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { .build(); } - private void verifyTransferAllowed(DomainResource existingDomain, Period period, DateTime now) + private void verifyTransferAllowed( + DomainResource existingDomain, + Period period, + DateTime now, + final DomainTransferRequestSuperuserExtension superuserExtension) throws EppException { verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES); verifyAuthInfoPresentForResourceTransfer(authInfo); @@ -219,7 +257,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { if (gainingClientId.equals(existingDomain.getCurrentSponsorClientId())) { throw new ObjectAlreadySponsoredException(); } - verifyTransferPeriodIsOneYear(period); + verifyTransferPeriod(period, superuserExtension); if (!isSuperuser) { checkAllowedAccessToTld(gainingClientId, existingDomain.getTld()); verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId); @@ -227,7 +265,8 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { } /** - * Verify that the transfer period is one year. + * 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 @@ -246,10 +285,20 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { *

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 verifyTransferPeriodIsOneYear(Period period) throws EppException { + private static void verifyTransferPeriod( + Period period, DomainTransferRequestSuperuserExtension superuserExtension) + throws EppException { verifyUnitIsYears(period); - if (period.getValue() != 1) { - throw new TransferPeriodMustBeOneYearException(); + if (superuserExtension == null) { + // If the superuser extension is not being used, then the period can only be one. + if (period.getValue() != 1) { + throw new TransferPeriodMustBeOneYearException(); + } + } else { + // 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(); + } } } @@ -296,13 +345,13 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { } private static ImmutableList createResponseExtensions( - FeesAndCredits feesAndCredits, FeeTransferCommandExtension feeTransfer) { - return feeTransfer == null + Optional feesAndCredits, FeeTransferCommandExtension feeTransfer) { + return (feeTransfer == null || !feesAndCredits.isPresent()) ? ImmutableList.of() : ImmutableList.of(feeTransfer.createResponseBuilder() - .setFees(feesAndCredits.getFees()) - .setCredits(feesAndCredits.getCredits()) - .setCurrency(feesAndCredits.getCurrency()) + .setFees(feesAndCredits.get().getFees()) + .setCredits(feesAndCredits.get().getCredits()) + .setCurrency(feesAndCredits.get().getCurrency()) .build()); } } diff --git a/java/google/registry/flows/domain/DomainTransferUtils.java b/java/google/registry/flows/domain/DomainTransferUtils.java index 590fdd26c..959426610 100644 --- a/java/google/registry/flows/domain/DomainTransferUtils.java +++ b/java/google/registry/flows/domain/DomainTransferUtils.java @@ -27,6 +27,7 @@ import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.domain.DomainResource; import google.registry.model.domain.GracePeriod; +import google.registry.model.domain.Period; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.Trid; import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse; @@ -47,26 +48,29 @@ import org.joda.time.DateTime; */ public final class DomainTransferUtils { - /** - * Sets up {@link TransferData} for a domain with links to entities for server approval. - */ + /** Sets up {@link TransferData} for a domain with links to entities for server approval. */ public static TransferData createPendingTransferData( TransferData.Builder transferDataBuilder, - ImmutableSet serverApproveEntities) { + ImmutableSet serverApproveEntities, + Period transferPeriod) { ImmutableSet.Builder> serverApproveEntityKeys = new ImmutableSet.Builder<>(); for (TransferServerApproveEntity entity : serverApproveEntities) { serverApproveEntityKeys.add(Key.create(entity)); } + if (transferPeriod.getValue() != 0) { + // Unless superuser sets period to 0, add a transfer billing event. + transferDataBuilder.setServerApproveBillingEvent( + Key.create(getOnlyElement(filter(serverApproveEntities, BillingEvent.OneTime.class)))); + } return transferDataBuilder .setTransferStatus(TransferStatus.PENDING) - .setServerApproveBillingEvent(Key.create( - getOnlyElement(filter(serverApproveEntities, BillingEvent.OneTime.class)))) .setServerApproveAutorenewEvent(Key.create( getOnlyElement(filter(serverApproveEntities, BillingEvent.Recurring.class)))) .setServerApproveAutorenewPollMessage(Key.create( getOnlyElement(filter(serverApproveEntities, PollMessage.Autorenew.class)))) .setServerApproveEntities(serverApproveEntityKeys.build()) + .setTransferPeriod(transferPeriod) .build(); } @@ -91,7 +95,7 @@ public final class DomainTransferUtils { DomainResource existingDomain, Trid trid, String gainingClientId, - Money transferCost, + Optional transferCost, DateTime now) { String targetId = existingDomain.getFullyQualifiedDomainName(); // Create a TransferData for the server-approve case to use for the speculative poll messages. @@ -101,15 +105,18 @@ public final class DomainTransferUtils { .setTransferStatus(TransferStatus.SERVER_APPROVED) .build(); Registry registry = Registry.get(existingDomain.getTld()); - return new ImmutableSet.Builder() - .add( - createTransferBillingEvent( - automaticTransferTime, - historyEntry, - targetId, - gainingClientId, - registry, - transferCost)) + ImmutableSet.Builder builder = new ImmutableSet.Builder<>(); + if (transferCost.isPresent()) { + builder.add( + createTransferBillingEvent( + automaticTransferTime, + historyEntry, + targetId, + gainingClientId, + registry, + transferCost.get())); + } + return builder .addAll( createOptionalAutorenewCancellation( automaticTransferTime, historyEntry, targetId, existingDomain) diff --git a/java/google/registry/flows/exceptions/InvalidTransferPeriodValueException.java b/java/google/registry/flows/exceptions/InvalidTransferPeriodValueException.java new file mode 100644 index 000000000..5d923a680 --- /dev/null +++ b/java/google/registry/flows/exceptions/InvalidTransferPeriodValueException.java @@ -0,0 +1,25 @@ +// 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.exceptions; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** Domain transfer period must be zero or one year when using the superuser EPP extension. */ +public class InvalidTransferPeriodValueException extends ParameterValuePolicyErrorException { + public InvalidTransferPeriodValueException() { + super( + "Domain transfer period must be zero or one year when using the superuser EPP extension."); + } +} diff --git a/java/google/registry/flows/exceptions/SuperuserExtensionAndAutorenewGracePeriodException.java b/java/google/registry/flows/exceptions/SuperuserExtensionAndAutorenewGracePeriodException.java new file mode 100644 index 000000000..6309d350b --- /dev/null +++ b/java/google/registry/flows/exceptions/SuperuserExtensionAndAutorenewGracePeriodException.java @@ -0,0 +1,25 @@ +// 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.exceptions; + +import google.registry.flows.EppException.StatusProhibitsOperationException; + +/** Superuser extensions cannot be used during autorenew grace periods. */ +public class SuperuserExtensionAndAutorenewGracePeriodException extends + StatusProhibitsOperationException { + public SuperuserExtensionAndAutorenewGracePeriodException() { + super("Superuser extensions cannot be used during autorenew grace periods."); + } +} diff --git a/java/google/registry/flows/exceptions/TransferPeriodZeroAndFeeTransferExtensionException.java b/java/google/registry/flows/exceptions/TransferPeriodZeroAndFeeTransferExtensionException.java new file mode 100644 index 000000000..8f77c17d2 --- /dev/null +++ b/java/google/registry/flows/exceptions/TransferPeriodZeroAndFeeTransferExtensionException.java @@ -0,0 +1,25 @@ +// 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.exceptions; + +import google.registry.flows.EppException.StatusProhibitsOperationException; + +/** Domain transfer period cannot be zero when using the fee transfer extension. */ +public class TransferPeriodZeroAndFeeTransferExtensionException + extends StatusProhibitsOperationException { + public TransferPeriodZeroAndFeeTransferExtensionException() { + super("Domain transfer period cannot be zero when using the fee transfer extension."); + } +} diff --git a/java/google/registry/flows/exceptions/UnauthorizedForSuperuserExtensionException.java b/java/google/registry/flows/exceptions/UnauthorizedForSuperuserExtensionException.java new file mode 100644 index 000000000..44764b014 --- /dev/null +++ b/java/google/registry/flows/exceptions/UnauthorizedForSuperuserExtensionException.java @@ -0,0 +1,24 @@ +// 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.exceptions; + +import google.registry.flows.EppException.AuthorizationErrorException; + +/** Superuser extension used by non-superuser or not passed by tool. */ +public class UnauthorizedForSuperuserExtensionException extends AuthorizationErrorException { + public UnauthorizedForSuperuserExtensionException() { + super("Superuser extension used by non-superuser or not passed by tool."); + } +} diff --git a/java/google/registry/model/domain/DomainResource.java b/java/google/registry/model/domain/DomainResource.java index 08e7fa181..b57a7b7c6 100644 --- a/java/google/registry/model/domain/DomainResource.java +++ b/java/google/registry/model/domain/DomainResource.java @@ -245,9 +245,11 @@ public class DomainResource extends DomainBase // If we are within an autorenew grace period, the transfer will subsume the autorenew. There // will already be a cancellation written in advance by the transfer request flow, so we don't // need to worry about billing, but we do need to cancel out the expiration time increase. - int extraYears = 1; // All transfers are one year. + // The transfer period saved in the transfer data will be one year, unless the superuser + // extension set the transfer period to zero. + int extraYears = transferData.getTransferPeriod().getValue(); if (domainAtTransferTime.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW)) { - extraYears--; + extraYears = 0; } // Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends // all other graces). @@ -261,14 +263,22 @@ public class DomainResource extends DomainBase extraYears)) // Set the speculatively-written new autorenew events as the domain's autorenew events. .setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent()) - .setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage()) - // Set the grace period using a key to the prescheduled transfer billing event. Not using - // GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch. - .setGracePeriods(ImmutableSet.of(GracePeriod.create( - GracePeriodStatus.TRANSFER, - transferExpirationTime.plus(Registry.get(getTld()).getTransferGracePeriodLength()), - transferData.getGainingClientId(), - transferData.getServerApproveBillingEvent()))); + .setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage()); + if (transferData.getTransferPeriod().getValue() == 1) { + // Set the grace period using a key to the prescheduled transfer billing event. Not using + // GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch. + builder.setGracePeriods( + ImmutableSet.of( + GracePeriod.create( + GracePeriodStatus.TRANSFER, + transferExpirationTime.plus( + Registry.get(getTld()).getTransferGracePeriodLength()), + transferData.getGainingClientId(), + transferData.getServerApproveBillingEvent()))); + } else { + // There won't be a billing event, so we don't need a grace period + builder.setGracePeriods(ImmutableSet.of()); + } // Set all remaining transfer properties. setAutomaticTransferSuccessProperties(builder, transferData); // Finish projecting to now. diff --git a/java/google/registry/model/domain/superuser/DomainTransferRequestSuperuserExtension.java b/java/google/registry/model/domain/superuser/DomainTransferRequestSuperuserExtension.java new file mode 100644 index 000000000..bd188d563 --- /dev/null +++ b/java/google/registry/model/domain/superuser/DomainTransferRequestSuperuserExtension.java @@ -0,0 +1,40 @@ +// 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.model.domain.superuser; + +import google.registry.model.domain.Period; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** A superuser extension that may be present on domain transfer request commands. */ +@XmlRootElement(name = "domainTransferRequest") +public class DomainTransferRequestSuperuserExtension extends SuperuserExtension { + // We need to specify the period here because the transfer object's period cannot be set to zero. + @XmlElement(name = "renewalPeriod") + Period renewalPeriod; + + // The number of days before the transfer will be automatically approved by the server. A value of + // zero means the transfer will happen immediately. + @XmlElement(name = "automaticTransferLength") + int automaticTransferLength; + + public Period getRenewalPeriod() { + return renewalPeriod; + } + + public int getAutomaticTransferLength() { + return automaticTransferLength; + } +} diff --git a/java/google/registry/model/domain/superuser/SuperuserExtension.java b/java/google/registry/model/domain/superuser/SuperuserExtension.java new file mode 100644 index 000000000..171925666 --- /dev/null +++ b/java/google/registry/model/domain/superuser/SuperuserExtension.java @@ -0,0 +1,23 @@ +// 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.model.domain.superuser; + +import google.registry.model.ImmutableObject; +import google.registry.model.eppinput.EppInput.CommandExtension; +import javax.xml.bind.annotation.XmlTransient; + +/** Base class for superuser EPP extensions. */ +@XmlTransient +public abstract class SuperuserExtension extends ImmutableObject implements CommandExtension {} diff --git a/java/google/registry/model/domain/superuser/package-info.java b/java/google/registry/model/domain/superuser/package-info.java new file mode 100644 index 000000000..525b1ba8b --- /dev/null +++ b/java/google/registry/model/domain/superuser/package-info.java @@ -0,0 +1,26 @@ +// 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. + +@XmlSchema( + namespace = "urn:google:params:xml:ns:superuser-1.0", + xmlns = @XmlNs(prefix = "superuser", namespaceURI = "urn:google:params:xml:ns:superuser-1.0"), + elementFormDefault = XmlNsForm.QUALIFIED) +@XmlAccessorType(XmlAccessType.FIELD) +package google.registry.model.domain.superuser; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlNs; +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; diff --git a/java/google/registry/model/eppinput/EppInput.java b/java/google/registry/model/eppinput/EppInput.java index e86df37fc..010e7d894 100644 --- a/java/google/registry/model/eppinput/EppInput.java +++ b/java/google/registry/model/eppinput/EppInput.java @@ -51,6 +51,7 @@ import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.rgp.RgpUpdateExtension; import google.registry.model.domain.secdns.SecDnsCreateExtension; import google.registry.model.domain.secdns.SecDnsUpdateExtension; +import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension; import google.registry.model.eppinput.ResourceCommand.ResourceCheck; import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand; import google.registry.model.host.HostCommand; @@ -345,7 +346,8 @@ public class EppInput extends ImmutableObject { @XmlElementRef(type = MetadataExtension.class), @XmlElementRef(type = RgpUpdateExtension.class), @XmlElementRef(type = SecDnsCreateExtension.class), - @XmlElementRef(type = SecDnsUpdateExtension.class) }) + @XmlElementRef(type = SecDnsUpdateExtension.class), + @XmlElementRef(type = DomainTransferRequestSuperuserExtension.class) }) @XmlElementWrapper List extension; diff --git a/java/google/registry/model/transfer/TransferData.java b/java/google/registry/model/transfer/TransferData.java index b56ec843d..cc1fd806a 100644 --- a/java/google/registry/model/transfer/TransferData.java +++ b/java/google/registry/model/transfer/TransferData.java @@ -25,6 +25,8 @@ import com.googlecode.objectify.condition.IfNull; import google.registry.model.Buildable; import google.registry.model.EppResource; import google.registry.model.billing.BillingEvent; +import google.registry.model.domain.Period; +import google.registry.model.domain.Period.Unit; import google.registry.model.eppcommon.Trid; import google.registry.model.poll.PollMessage; import java.util.Set; @@ -81,6 +83,14 @@ public class TransferData extends BaseTransferObject implements Buildable { /** The transaction id of the most recent transfer request (or null if there never was one). */ Trid transferRequestTrid; + /** + * The period to extend the registration upon completion of the transfer. + * + *

By default, domain transfers are for one year. This can be changed to zero by using the + * superuser EPP extension. + */ + Period transferPeriod = Period.create(1, Unit.YEARS); + public ImmutableSet> getServerApproveEntities() { return nullToEmptyImmutableCopy(serverApproveEntities); } @@ -101,6 +111,10 @@ public class TransferData extends BaseTransferObject implements Buildable { return transferRequestTrid; } + public Period getTransferPeriod() { + return transferPeriod; + } + @Override public Builder asBuilder() { return new Builder(clone(this)); @@ -145,6 +159,11 @@ public class TransferData extends BaseTransferObject implements Buildable { getInstance().transferRequestTrid = transferRequestTrid; return this; } + + public Builder setTransferPeriod(Period transferPeriod) { + getInstance().transferPeriod = transferPeriod; + return this; + } } /** diff --git a/java/google/registry/rde/imports/RdeDomainImportAction.java b/java/google/registry/rde/imports/RdeDomainImportAction.java index a249528c3..3f883f1cc 100644 --- a/java/google/registry/rde/imports/RdeDomainImportAction.java +++ b/java/google/registry/rde/imports/RdeDomainImportAction.java @@ -42,6 +42,8 @@ import google.registry.gcs.GcsUtils; import google.registry.mapreduce.MapreduceRunner; import google.registry.model.billing.BillingEvent; import google.registry.model.domain.DomainResource; +import google.registry.model.domain.Period; +import google.registry.model.domain.Period.Unit; import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; import google.registry.model.transfer.TransferData; @@ -203,10 +205,13 @@ public class RdeDomainImportAction implements Runnable { domain, historyEntry.getTrid(), transferData.getGainingClientId(), - transferCost, + Optional.of(transferCost), transferData.getTransferRequestTime()); transferData = - createPendingTransferData(transferData.asBuilder(), serverApproveEntities); + createPendingTransferData( + transferData.asBuilder(), + serverApproveEntities, + Period.create(1, Unit.YEARS)); // Create a poll message to notify the losing registrar that a transfer was requested. PollMessage requestPollMessage = createLosingTransferPollMessage(domain.getRepoId(), transferData, transferData.getPendingTransferExpirationTime(), historyEntry) diff --git a/java/google/registry/xml/xsd/superuser.xsd b/java/google/registry/xml/xsd/superuser.xsd new file mode 100644 index 000000000..8d4660349 --- /dev/null +++ b/java/google/registry/xml/xsd/superuser.xsd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/javatests/google/registry/flows/ExtensionManagerTest.java b/javatests/google/registry/flows/ExtensionManagerTest.java index 952f842fe..312f6b736 100644 --- a/javatests/google/registry/flows/ExtensionManagerTest.java +++ b/javatests/google/registry/flows/ExtensionManagerTest.java @@ -23,10 +23,12 @@ import google.registry.flows.EppException.UnimplementedExtensionException; import google.registry.flows.ExtensionManager.UndeclaredServiceExtensionException; import google.registry.flows.ExtensionManager.UnsupportedRepeatedExtensionException; import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; +import google.registry.flows.exceptions.UnauthorizedForSuperuserExtensionException; import google.registry.flows.session.HelloFlow; import google.registry.model.domain.fee06.FeeInfoCommandExtensionV06; import google.registry.model.domain.launch.LaunchCreateExtension; import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension; import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput.CommandExtension; @@ -133,6 +135,44 @@ public class ExtensionManagerTest { manager.validate(); } + @Test + public void testSuperuserExtension_allowedForToolSource() throws Exception { + ExtensionManager manager = new TestInstanceBuilder() + .setEppRequestSource(EppRequestSource.TOOL) + .setDeclaredUris() + .setSuppliedExtensions(DomainTransferRequestSuperuserExtension.class) + .setIsSuperuser(true) + .build(); + manager.register(DomainTransferRequestSuperuserExtension.class); + manager.validate(); + } + + @Test + public void testSuperuserExtension_forbiddenWhenNotSuperuser() throws Exception { + ExtensionManager manager = new TestInstanceBuilder() + .setEppRequestSource(EppRequestSource.TOOL) + .setDeclaredUris() + .setSuppliedExtensions(DomainTransferRequestSuperuserExtension.class) + .setIsSuperuser(false) + .build(); + manager.register(DomainTransferRequestSuperuserExtension.class); + thrown.expect(UnauthorizedForSuperuserExtensionException.class); + manager.validate(); + } + + @Test + public void testSuperuserExtension_forbiddenWhenNotToolSource() throws Exception { + ExtensionManager manager = new TestInstanceBuilder() + .setEppRequestSource(EppRequestSource.CONSOLE) + .setDeclaredUris() + .setSuppliedExtensions(DomainTransferRequestSuperuserExtension.class) + .setIsSuperuser(true) + .build(); + manager.register(DomainTransferRequestSuperuserExtension.class); + thrown.expect(UnauthorizedForSuperuserExtensionException.class); + manager.validate(); + } + @Test public void testUnimplementedExtensionsForbidden() throws Exception { ExtensionManager manager = new TestInstanceBuilder() @@ -160,6 +200,11 @@ public class ExtensionManagerTest { return this; } + TestInstanceBuilder setIsSuperuser(boolean isSuperuser) { + manager.isSuperuser = isSuperuser; + return this; + } + @SafeVarargs final TestInstanceBuilder setSuppliedExtensions( Class... suppliedExtensionClasses) { diff --git a/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java index dc999a481..4e6d0dac4 100644 --- a/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferApproveFlowTest.java @@ -50,11 +50,14 @@ import google.registry.flows.exceptions.NotPendingTransferException; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Cancellation; import google.registry.model.billing.BillingEvent.Cancellation.Builder; +import google.registry.model.billing.BillingEvent.OneTime; import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainResource; import google.registry.model.domain.GracePeriod; +import google.registry.model.domain.Period; +import google.registry.model.domain.Period.Unit; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.StatusValue; @@ -64,6 +67,7 @@ import google.registry.model.poll.PollMessage; import google.registry.model.registry.Registry; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferResponse.DomainTransferResponse; import google.registry.model.transfer.TransferStatus; import org.joda.money.Money; @@ -128,6 +132,20 @@ public class DomainTransferApproveFlowTest DateTime expectedExpirationTime, int expectedYearsToCharge, BillingEvent.Cancellation.Builder... expectedCancellationBillingEvents) throws Exception { + runSuccessfulFlowWithAssertions( + tld, + commandFilename, + expectedXmlFilename, + expectedExpirationTime); + assertHistoryEntriesContainBillingEventsAndGracePeriods( + tld, expectedYearsToCharge, expectedCancellationBillingEvents); + } + + private void runSuccessfulFlowWithAssertions( + String tld, + String commandFilename, + String expectedXmlFilename, + DateTime expectedExpirationTime) throws Exception { setEppLoader(commandFilename); Registry registry = Registry.get(tld); // Make sure the implicit billing event is there; it will be deleted by the flow. @@ -157,46 +175,6 @@ public class DomainTransferApproveFlowTest assertAboutDomains().that(domain).hasRegistrationExpirationTime(expectedExpirationTime); assertThat(ofy().load().key(domain.getAutorenewBillingEvent()).now().getEventTime()) .isEqualTo(expectedExpirationTime); - // We expect three billing events: one for the transfer, a closed autorenew for the losing - // client and an open autorenew for the gaining client that begins at the new expiration time. - BillingEvent.OneTime transferBillingEvent = new BillingEvent.OneTime.Builder() - .setReason(Reason.TRANSFER) - .setTargetId(domain.getFullyQualifiedDomainName()) - .setEventTime(clock.nowUtc()) - .setBillingTime(clock.nowUtc().plus(registry.getTransferGracePeriodLength())) - .setClientId("NewRegistrar") - .setCost(Money.of(USD, 11).multipliedBy(expectedYearsToCharge)) - .setPeriodYears(expectedYearsToCharge) - .setParent(historyEntryTransferApproved) - .build(); - assertBillingEventsForResource( - domain, - FluentIterable.from(expectedCancellationBillingEvents) - .transform(new Function() { - @Override - public Cancellation apply(Builder builder) { - return builder.setParent(historyEntryTransferApproved).build(); - }}) - .append( - transferBillingEvent, - getLosingClientAutorenewEvent().asBuilder() - .setRecurrenceEndTime(clock.nowUtc()) - .build(), - getGainingClientAutorenewEvent().asBuilder() - .setEventTime(domain.getRegistrationExpirationTime()) - .setParent(historyEntryTransferApproved) - .build()) - .toArray(BillingEvent.class)); - // There should be a grace period for the new transfer billing event. - assertGracePeriods( - domain.getGracePeriods(), - ImmutableMap.of( - GracePeriod.create( - GracePeriodStatus.TRANSFER, - clock.nowUtc().plus(registry.getTransferGracePeriodLength()), - "NewRegistrar", - null), - transferBillingEvent)); // The poll message (in the future) to the losing registrar for implicit ack should be gone. assertThat(getPollMessages(domain, "TheRegistrar", clock.nowUtc().plusMonths(1))).isEmpty(); @@ -236,6 +214,97 @@ public class DomainTransferApproveFlowTest .getGracePeriods()).isEmpty(); } + private void assertHistoryEntriesContainBillingEventsAndGracePeriods( + String tld, + int expectedYearsToCharge, + BillingEvent.Cancellation.Builder... expectedCancellationBillingEvents) + throws Exception { + Registry registry = Registry.get(tld); + domain = reloadResourceByForeignKey(); + final HistoryEntry historyEntryTransferApproved = + getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_APPROVE); + // We expect three billing events: one for the transfer, a closed autorenew for the losing + // client and an open autorenew for the gaining client that begins at the new expiration time. + OneTime transferBillingEvent = + new BillingEvent.OneTime.Builder() + .setReason(Reason.TRANSFER) + .setTargetId(domain.getFullyQualifiedDomainName()) + .setEventTime(clock.nowUtc()) + .setBillingTime(clock.nowUtc().plus(registry.getTransferGracePeriodLength())) + .setClientId("NewRegistrar") + .setCost(Money.of(USD, 11).multipliedBy(expectedYearsToCharge)) + .setPeriodYears(expectedYearsToCharge) + .setParent(historyEntryTransferApproved) + .build(); + assertBillingEventsForResource( + domain, + FluentIterable.from(expectedCancellationBillingEvents) + .transform( + new Function() { + @Override + public Cancellation apply(Builder builder) { + return builder.setParent(historyEntryTransferApproved).build(); + } + }) + .append( + transferBillingEvent, + getLosingClientAutorenewEvent() + .asBuilder() + .setRecurrenceEndTime(clock.nowUtc()) + .build(), + getGainingClientAutorenewEvent() + .asBuilder() + .setEventTime(domain.getRegistrationExpirationTime()) + .setParent(historyEntryTransferApproved) + .build()) + .toArray(BillingEvent.class)); + // There should be a grace period for the new transfer billing event. + assertGracePeriods( + domain.getGracePeriods(), + ImmutableMap.of( + GracePeriod.create( + GracePeriodStatus.TRANSFER, + clock.nowUtc().plus(registry.getTransferGracePeriodLength()), + "NewRegistrar", + null), + transferBillingEvent)); + } + + private void assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods( + BillingEvent.Cancellation.Builder... expectedCancellationBillingEvents) + throws Exception { + domain = reloadResourceByForeignKey(); + final HistoryEntry historyEntryTransferApproved = + getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_APPROVE); + // We expect two billing events: a closed autorenew for the losing client and an open autorenew + // for the gaining client that begins at the new expiration time. + assertBillingEventsForResource( + domain, + FluentIterable.from(expectedCancellationBillingEvents) + .transform( + new Function() { + @Override + public Cancellation apply(Builder builder) { + return builder.setParent(historyEntryTransferApproved).build(); + } + }) + .append( + getLosingClientAutorenewEvent() + .asBuilder() + .setRecurrenceEndTime(clock.nowUtc()) + .build(), + getGainingClientAutorenewEvent() + .asBuilder() + .setEventTime(domain.getRegistrationExpirationTime()) + .setParent(historyEntryTransferApproved) + .build()) + .toArray(BillingEvent.class)); + // There should be no grace period. + assertGracePeriods( + domain.getGracePeriods(), + ImmutableMap.of()); + } + private void doSuccessfulTest(String tld, String commandFilename, String expectedXmlFilename) throws Exception { clock.advanceOneMilli(); @@ -503,4 +572,23 @@ public class DomainTransferApproveFlowTest "tld", clock.nowUtc().plusDays(3), TRANSFER_SUCCESSFUL, 1)); } + + @Test + public void testSuccess_superuserExtension_transferPeriodZero() throws Exception { + DomainResource domain = reloadResourceByForeignKey(); + TransferData.Builder transferDataBuilder = domain.getTransferData().asBuilder(); + persistResource( + domain + .asBuilder() + .setTransferData( + transferDataBuilder.setTransferPeriod(Period.create(0, Unit.YEARS)).build()) + .build()); + clock.advanceOneMilli(); + runSuccessfulFlowWithAssertions( + "tld", + "domain_transfer_approve.xml", + "domain_transfer_approve_response_zero_period.xml", + domain.getRegistrationExpirationTime().plusYears(0)); + assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods(); + } } diff --git a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java index 8b9ee8c50..26dc15028 100644 --- a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -36,6 +36,7 @@ import static org.joda.money.CurrencyUnit.USD; import com.google.common.base.Function; import com.google.common.base.Optional; +import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; @@ -43,6 +44,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; import com.googlecode.objectify.Key; +import google.registry.flows.EppRequestSource; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException; import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException; @@ -54,10 +56,13 @@ import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException import google.registry.flows.domain.DomainFlowUtils.PremiumNameBlockedException; import google.registry.flows.domain.DomainFlowUtils.UnsupportedFeeAttributeException; import google.registry.flows.exceptions.AlreadyPendingTransferException; +import google.registry.flows.exceptions.InvalidTransferPeriodValueException; import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; +import google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException; import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException; +import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Cancellation.Builder; import google.registry.model.billing.BillingEvent.Reason; @@ -65,6 +70,8 @@ import google.registry.model.contact.ContactAuthInfo; import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainResource; import google.registry.model.domain.GracePeriod; +import google.registry.model.domain.Period; +import google.registry.model.domain.Period.Unit; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.StatusValue; @@ -117,26 +124,33 @@ public class DomainTransferRequestFlowTest setClientIdForFlow("NewRegistrar"); } - private void assertTransferRequested(DomainResource domain) throws Exception { + private void assertTransferRequested( + DomainResource domain, Optional expectedAutomaticTransferLength) throws Exception { + DateTime afterAutoAckTime = + (expectedAutomaticTransferLength.isPresent()) + ? clock.nowUtc().plus(expectedAutomaticTransferLength.get()) + : clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength()); assertAboutDomains().that(domain) .hasTransferStatus(TransferStatus.PENDING).and() .hasTransferGainingClientId("NewRegistrar").and() .hasTransferLosingClientId("TheRegistrar").and() .hasTransferRequestClientTrid(getClientTrid()).and() .hasCurrentSponsorClientId("TheRegistrar").and() - .hasPendingTransferExpirationTime( - clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength())).and() + .hasPendingTransferExpirationTime(afterAutoAckTime).and() .hasStatusValue(StatusValue.PENDING_TRANSFER); } - private void assertTransferApproved(DomainResource domain) { - DateTime afterAutoAck = - clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength()); + private void assertTransferApproved( + DomainResource domain, Optional expectedAutomaticTransferLength) { + DateTime afterAutoAckTime = + (expectedAutomaticTransferLength.isPresent()) + ? clock.nowUtc().plus(expectedAutomaticTransferLength.get()) + : clock.nowUtc().plus(Registry.get(domain.getTld()).getAutomaticTransferLength()); assertAboutDomains().that(domain) .hasTransferStatus(TransferStatus.SERVER_APPROVED).and() .hasCurrentSponsorClientId("NewRegistrar").and() - .hasLastTransferTime(afterAutoAck).and() - .hasPendingTransferExpirationTime(afterAutoAck).and() + .hasLastTransferTime(afterAutoAckTime).and() + .hasPendingTransferExpirationTime(afterAutoAckTime).and() .doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER); } @@ -169,7 +183,7 @@ public class DomainTransferRequestFlowTest final HistoryEntry historyEntryTransferRequest = getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST); subordinateHost = reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc()); - assertTransferRequested(domain); + assertTransferRequested(domain, Optional.absent()); assertAboutDomains().that(domain) .hasPendingTransferExpirationTime(implicitTransferTime).and() .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_TRANSFER_REQUEST); @@ -179,7 +193,30 @@ public class DomainTransferRequestFlowTest .and() .hasOtherClientId("TheRegistrar"); assertAboutHosts().that(subordinateHost).hasNoHistoryEntries(); - assertThat(getPollMessages("TheRegistrar", clock.nowUtc())).hasSize(1); + + assertHistoryEntriesContainBillingEventsAndGracePeriods( + expectedExpirationTime, + implicitTransferTime, + transferCost, + originalGracePeriods, + extraExpectedBillingEvents); + + assertPollMessagesEmitted(expectedExpirationTime, implicitTransferTime, Optional.absent()); + + assertAboutDomainAfterAutomaticTransfer( + expectedExpirationTime, implicitTransferTime, Optional.absent()); + } + + private void assertHistoryEntriesContainBillingEventsAndGracePeriods( + DateTime expectedExpirationTime, + DateTime implicitTransferTime, + Optional transferCost, + ImmutableSet originalGracePeriods, + BillingEvent.Cancellation.Builder... extraExpectedBillingEvents) + throws Exception { + Registry registry = Registry.get(domain.getTld()); + final HistoryEntry historyEntryTransferRequest = + getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST); // A BillingEvent should be created AUTOMATIC_TRANSFER_DAYS in the future, for the case when the // transfer is implicitly acked, but there should be no grace period yet. There should also be @@ -235,7 +272,69 @@ public class DomainTransferRequestFlowTest "NewRegistrar", null), transferBillingEvent)); - assertTransferApproved(domainAfterAutomaticTransfer); + } + + public void assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods( + DateTime expectedExpirationTime, + DateTime implicitTransferTime, + ImmutableSet originalGracePeriods, + BillingEvent.Cancellation.Builder... extraExpectedBillingEvents) + throws Exception { + final HistoryEntry historyEntryTransferRequest = + getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST); + + // There should also be two autorenew billing events, one for the losing client that ends at the + // transfer time, and one for the gaining client that starts at that time. + // All of the other transfer flow tests happen on day 3 of the transfer, but the initial + // request by definition takes place on day 1, so we need to edit the times in the + // autorenew events from the base test case. + assertBillingEvents( + FluentIterable.from(extraExpectedBillingEvents) + .transform( + new Function() { + @Override + public BillingEvent apply(Builder builder) { + return builder.setParent(historyEntryTransferRequest).build(); + } + }) + .append( + getLosingClientAutorenewEvent() + .asBuilder() + .setRecurrenceEndTime(implicitTransferTime) + .build(), + getGainingClientAutorenewEvent() + .asBuilder() + .setEventTime(expectedExpirationTime) + .build()) + .toArray(BillingEvent.class)); + // The domain's autorenew billing event should still point to the losing client's event. + BillingEvent.Recurring domainAutorenewEvent = + ofy().load().key(domain.getAutorenewBillingEvent()).now(); + assertThat(domainAutorenewEvent.getClientId()).isEqualTo("TheRegistrar"); + assertThat(domainAutorenewEvent.getRecurrenceEndTime()).isEqualTo(implicitTransferTime); + // The original grace periods should remain untouched. + assertThat(domain.getGracePeriods()).containsExactlyElementsIn(originalGracePeriods); + // If we fast forward AUTOMATIC_TRANSFER_DAYS, we should see the grace period appear, the + // transfer should have happened, and all other grace periods should be gone. Also, both the + // gaining and losing registrars should have a new poll message. + DomainResource domainAfterAutomaticTransfer = domain.cloneProjectedAtTime(implicitTransferTime); + // There should be no grace period. + assertGracePeriods(domainAfterAutomaticTransfer.getGracePeriods(), ImmutableMap.of()); + } + + private void assertPollMessagesEmitted( + DateTime expectedExpirationTime, + DateTime implicitTransferTime, + Optional expectedAutomaticTransferLength) { + // Assert that there exists a poll message to notify the losing registrar that a transfer was + // requested. If the expected automatic transfer length is zero, then also expect a server + // approved poll message. + assertThat(getPollMessages("TheRegistrar", clock.nowUtc())) + .hasSize( + (expectedAutomaticTransferLength.isPresent() + && expectedAutomaticTransferLength.get().equals(Duration.ZERO)) + ? 2 + : 1); // Two poll messages on the gaining registrar's side at the expected expiration time: a // (OneTime) transfer approved message, and an Autorenew poll message. @@ -261,8 +360,15 @@ public class DomainTransferRequestFlowTest // Two poll messages on the losing registrar's side at the implicit transfer time: a // transfer pending message, and a transfer approved message (both OneTime messages). assertThat(getPollMessages("TheRegistrar", implicitTransferTime)).hasSize(2); - PollMessage losingTransferPendingPollMessage = - getOnlyPollMessage("TheRegistrar", clock.nowUtc()); + PollMessage losingTransferPendingPollMessage = Iterables.getOnlyElement( + FluentIterable.from(getPollMessages("TheRegistrar", clock.nowUtc())) + .filter( + new Predicate() { + @Override + public boolean apply(PollMessage pollMessage) { + return TransferStatus.PENDING.getMessage().equals(pollMessage.getMsg()); + } + })); PollMessage losingTransferApprovedPollMessage = Iterables.getOnlyElement(FluentIterable .from(getPollMessages("TheRegistrar", implicitTransferTime)) .filter(Predicates.not(Predicates.equalTo(losingTransferPendingPollMessage)))); @@ -280,7 +386,15 @@ public class DomainTransferRequestFlowTest .filter(TransferResponse.class)) .getTransferStatus()) .isEqualTo(TransferStatus.SERVER_APPROVED); + } + private void assertAboutDomainAfterAutomaticTransfer( + DateTime expectedExpirationTime, + DateTime implicitTransferTime, + Optional expectedAutomaticTransferLength) { + Registry registry = Registry.get(domain.getTld()); + DomainResource domainAfterAutomaticTransfer = domain.cloneProjectedAtTime(implicitTransferTime); + assertTransferApproved(domainAfterAutomaticTransfer, expectedAutomaticTransferLength); assertAboutDomains().that(domainAfterAutomaticTransfer) .hasRegistrationExpirationTime(expectedExpirationTime); assertThat(ofy().load().key(domainAfterAutomaticTransfer.getAutorenewBillingEvent()).now() @@ -327,6 +441,76 @@ public class DomainTransferRequestFlowTest commandFilename, expectedXmlFilename, domain.getRegistrationExpirationTime().plusYears(1)); } + private void doSuccessfulSuperuserExtensionTest( + String commandFilename, + String expectedXmlFilename, + DateTime expectedExpirationTime, + Map substitutions, + Optional transferCost, + Period expectedPeriod, + Duration expectedAutomaticTransferLength, + BillingEvent.Cancellation.Builder... extraExpectedBillingEvents) throws Exception { + setEppInput(commandFilename, substitutions); + ImmutableSet originalGracePeriods = domain.getGracePeriods(); + // Replace the ROID in the xml file with the one generated in our test. + eppLoader.replaceAll("JD1234-REP", contact.getRepoId()); + // For all of the other transfer flow tests, 'now' corresponds to day 3 of the transfer, but + // for the request test we want that same 'now' to be the initial request time, so we shift + // the transfer timeline 3 days later by adjusting the implicit transfer time here. + DateTime implicitTransferTime = clock.nowUtc().plus(expectedAutomaticTransferLength); + // Setup done; run the test. + assertTransactionalFlow(true); + runFlowAssertResponse( + CommitMode.LIVE, UserPrivileges.SUPERUSER, readFile(expectedXmlFilename, substitutions)); + + if (expectedAutomaticTransferLength.equals(Duration.ZERO)) { + // The transfer is going to happen immediately. To observe the domain in the pending transfer + // state, grab it directly from the database. + domain = Iterables.getOnlyElement(ofy().load().type(DomainResource.class).list()); + assertThat(domain.getFullyQualifiedDomainName()).isEqualTo("example.tld"); + } else { + // Transfer should have been requested. + domain = reloadResourceByForeignKey(); + } + // Verify correct fields were set. + subordinateHost = reloadResourceAndCloneAtTime(subordinateHost, clock.nowUtc()); + assertTransferRequested(domain, Optional.of(expectedAutomaticTransferLength)); + assertAboutDomains().that(domain) + .hasPendingTransferExpirationTime(implicitTransferTime).and() + .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_TRANSFER_REQUEST); + final HistoryEntry historyEntryTransferRequest = + getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST); + assertAboutHistoryEntries() + .that(historyEntryTransferRequest) + .hasPeriodYears(expectedPeriod.getValue()) + .and() + .hasOtherClientId("TheRegistrar"); + assertAboutHosts().that(subordinateHost).hasNoHistoryEntries(); + + if (expectedPeriod.getValue() == 0) { + assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods( + expectedExpirationTime, + implicitTransferTime, + originalGracePeriods, + extraExpectedBillingEvents); + } else { + assertHistoryEntriesContainBillingEventsAndGracePeriods( + expectedExpirationTime, + implicitTransferTime, + transferCost, + originalGracePeriods, + extraExpectedBillingEvents); + } + + assertPollMessagesEmitted( + expectedExpirationTime, + implicitTransferTime, + Optional.of(expectedAutomaticTransferLength)); + + assertAboutDomainAfterAutomaticTransfer( + expectedExpirationTime, implicitTransferTime, Optional.of(expectedAutomaticTransferLength)); + } + private void runTest( String commandFilename, UserPrivileges userPrivileges, @@ -517,6 +701,95 @@ public class DomainTransferRequestFlowTest doFailingTest("domain_transfer_request_2_years.xml"); } + @Test + public void testSuccess_superuserExtension_zeroPeriod_nonZeroAutomaticTransferLength() + throws Exception { + setupDomain("example", "tld"); + eppRequestSource = EppRequestSource.TOOL; + clock.advanceOneMilli(); + doSuccessfulSuperuserExtensionTest( + "domain_transfer_request_superuser_extension.xml", + "domain_transfer_request_response_su_ext_zero_period_nonzero_transfer_length.xml", + domain.getRegistrationExpirationTime().plusYears(0), + ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "5"), + Optional.absent(), + Period.create(0, Unit.YEARS), + Duration.standardDays(5)); + } + + @Test + public void testSuccess_superuserExtension_zeroPeriod_zeroAutomaticTransferLength() + throws Exception { + setupDomain("example", "tld"); + eppRequestSource = EppRequestSource.TOOL; + clock.advanceOneMilli(); + doSuccessfulSuperuserExtensionTest( + "domain_transfer_request_superuser_extension.xml", + "domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml", + domain.getRegistrationExpirationTime().plusYears(0), + ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0"), + Optional.absent(), + Period.create(0, Unit.YEARS), + Duration.standardDays(0)); + } + + @Test + public void testSuccess_superuserExtension_nonZeroPeriod_nonZeroAutomaticTransferLength() + throws Exception { + setupDomain("example", "tld"); + eppRequestSource = EppRequestSource.TOOL; + clock.advanceOneMilli(); + doSuccessfulSuperuserExtensionTest( + "domain_transfer_request_superuser_extension.xml", + "domain_transfer_request_response_su_ext_one_year_period_nonzero_transfer_length.xml", + domain.getRegistrationExpirationTime().plusYears(1), + ImmutableMap.of("PERIOD", "1", "AUTOMATIC_TRANSFER_LENGTH", "5"), + Optional.absent(), + Period.create(1, Unit.YEARS), + Duration.standardDays(5)); + } + + @Test + public void testFailure_superuserExtension_duringAutorenewGracePeriod() throws Exception { + setupDomain("example", "tld"); + eppRequestSource = EppRequestSource.TOOL; + DomainResource domain = reloadResourceByForeignKey(); + DateTime oldExpirationTime = clock.nowUtc().minusDays(1); + persistResource(domain.asBuilder() + .setRegistrationExpirationTime(oldExpirationTime) + .build()); + clock.advanceOneMilli(); + thrown.expect(SuperuserExtensionAndAutorenewGracePeriodException.class); + runTest( + "domain_transfer_request_superuser_extension.xml", + UserPrivileges.SUPERUSER, + ImmutableMap.of("PERIOD", "1", "AUTOMATIC_TRANSFER_LENGTH", "5")); + } + + @Test + public void testFailure_superuserExtension_twoYearPeriod() throws Exception { + setupDomain("example", "tld"); + eppRequestSource = EppRequestSource.TOOL; + clock.advanceOneMilli(); + thrown.expect(InvalidTransferPeriodValueException.class); + runTest( + "domain_transfer_request_superuser_extension.xml", + UserPrivileges.SUPERUSER, + ImmutableMap.of("PERIOD", "2", "AUTOMATIC_TRANSFER_LENGTH", "5")); + } + + @Test + public void testFailure_superuserExtension_zeroPeriod_feeTransferExtension() throws Exception { + setupDomain("example", "tld"); + eppRequestSource = EppRequestSource.TOOL; + clock.advanceOneMilli(); + thrown.expect(TransferPeriodZeroAndFeeTransferExtensionException.class); + runTest( + "domain_transfer_request_fee_and_superuser_extension.xml", + UserPrivileges.SUPERUSER, + ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "5")); + } + @Test public void testSuccess_cappedExpiration() throws Exception { setupDomain("example", "tld"); diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_approve_response_zero_period.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_approve_response_zero_period.xml new file mode 100644 index 000000000..464e018f3 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_approve_response_zero_period.xml @@ -0,0 +1,23 @@ + + + + Command completed successfully + + + + example.tld + clientApproved + NewRegistrar + 2000-06-06T22:00:00.0Z + TheRegistrar + 2000-06-09T22:00:00.0Z + 2001-09-08T22:00:00.0Z + + + + ABC-12345 + server-trid + + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_fee_and_superuser_extension.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_fee_and_superuser_extension.xml new file mode 100644 index 000000000..ad0a02cc7 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_fee_and_superuser_extension.xml @@ -0,0 +1,24 @@ + + + + + example.tld + + 2fooBAR + + + + + + USD + 11 + + + 0 + 0 + + + ABC-12345 + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_one_year_period_nonzero_transfer_length.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_one_year_period_nonzero_transfer_length.xml new file mode 100644 index 000000000..46c31519c --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_one_year_period_nonzero_transfer_length.xml @@ -0,0 +1,23 @@ + + + + Command completed successfully; action pending + + + + example.tld + pending + NewRegistrar + 2000-06-09T22:00:00.0Z + TheRegistrar + 2000-06-14T22:00:00.0Z + 2002-09-08T22:00:00.0Z + + + + ABC-12345 + server-trid + + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_zero_period_nonzero_transfer_length.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_zero_period_nonzero_transfer_length.xml new file mode 100644 index 000000000..026e0a339 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_zero_period_nonzero_transfer_length.xml @@ -0,0 +1,23 @@ + + + + Command completed successfully; action pending + + + + example.tld + pending + NewRegistrar + 2000-06-09T22:00:00.0Z + TheRegistrar + 2000-06-14T22:00:00.0Z + 2001-09-08T22:00:00.0Z + + + + ABC-12345 + server-trid + + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml new file mode 100644 index 000000000..31b671390 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml @@ -0,0 +1,23 @@ + + + + Command completed successfully; action pending + + + + example.tld + pending + NewRegistrar + 2000-06-09T22:00:00.0Z + TheRegistrar + 2000-06-09T22:00:00.0Z + 2001-09-08T22:00:00.0Z + + + + ABC-12345 + server-trid + + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_superuser_extension.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_superuser_extension.xml new file mode 100644 index 000000000..8bb4285a9 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_superuser_extension.xml @@ -0,0 +1,21 @@ + + + + + example.tld + + 2fooBAR + + + + + + %PERIOD% + %AUTOMATIC_TRANSFER_LENGTH% + + + + ABC-12345 + + diff --git a/javatests/google/registry/model/schema.txt b/javatests/google/registry/model/schema.txt index b699d54b4..fc0e9a479 100644 --- a/javatests/google/registry/model/schema.txt +++ b/javatests/google/registry/model/schema.txt @@ -920,6 +920,7 @@ class google.registry.model.transfer.TransferData { com.googlecode.objectify.Key serverApproveBillingEvent; com.googlecode.objectify.Key serverApproveAutorenewEvent; com.googlecode.objectify.Key serverApproveAutorenewPollMessage; + google.registry.model.domain.Period transferPeriod; google.registry.model.eppcommon.Trid transferRequestTrid; google.registry.model.transfer.TransferStatus transferStatus; java.lang.String gainingClientId;