diff --git a/docs/flows.md b/docs/flows.md index 2756b0f89..6bf53d42e 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -529,9 +529,9 @@ comes in at the exact millisecond that the domain would have expired. * Fees must be explicitly acknowledged when performing any operations on a premium name. * 2004 + * New registration period exceeds maximum number of years. * The fees passed in the transform command do not match the fees that will be charged. - * New registration period exceeds maximum number of years. * The current expiration date is incorrect. * 2005 * The requested fee is expressed in a scale that is invalid for the given @@ -620,6 +620,7 @@ An EPP flow that creates a new domain resource. * 2004 * The acceptance time specified in the claim notice is more than 48 hours in the past. + * New registration period exceeds maximum number of years. * The expiration time specified in the claim notice has elapsed. * The fees passed in the transform command do not match the fees that will be charged. @@ -830,6 +831,7 @@ An EPP flow that creates a new application for a domain resource. * 2004 * The acceptance time specified in the claim notice is more than 48 hours in the past. + * New registration period exceeds maximum number of years. * The expiration time specified in the claim notice has elapsed. * The fees passed in the transform command do not match the fees that will be charged. @@ -907,6 +909,8 @@ An EPP flow that allocates a new domain resource from a domain application. ### Errors +* 2004 + * New registration period exceeds maximum number of years. * 2201 * Only a superuser can allocate domains. * 2302 diff --git a/java/google/registry/flows/domain/DomainAllocateFlow.java b/java/google/registry/flows/domain/DomainAllocateFlow.java index d775df769..f5d5060cf 100644 --- a/java/google/registry/flows/domain/DomainAllocateFlow.java +++ b/java/google/registry/flows/domain/DomainAllocateFlow.java @@ -26,6 +26,7 @@ import static google.registry.flows.domain.DomainFlowUtils.prepareMarkedLrpToken import static google.registry.flows.domain.DomainFlowUtils.validateCreateCommandContactsAndNameservers; import static google.registry.flows.domain.DomainFlowUtils.validateDomainName; import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables; +import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationPeriod; import static google.registry.flows.domain.DomainFlowUtils.validateSecDnsExtension; import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears; import static google.registry.model.EppResourceUtils.createDomainRepoId; @@ -94,6 +95,7 @@ import org.joda.time.DateTime; * @error {@link DomainAllocateFlow.HasFinalStatusException} * @error {@link DomainAllocateFlow.MissingApplicationException} * @error {@link DomainAllocateFlow.OnlySuperuserCanAllocateException} + * @error {@link DomainFlowUtils.ExceedsMaxRegistrationYearsException} */ public class DomainAllocateFlow implements TransactionalFlow { @@ -134,6 +136,7 @@ public class DomainAllocateFlow implements TransactionalFlow { Period period = command.getPeriod(); Integer years = period.getValue(); verifyUnitIsYears(period); + validateRegistrationPeriod(years); validateCreateCommandContactsAndNameservers(command, registry.getTldStr()); SecDnsCreateExtension secDnsCreate = validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class)); diff --git a/java/google/registry/flows/domain/DomainApplicationCreateFlow.java b/java/google/registry/flows/domain/DomainApplicationCreateFlow.java index e1468cd0b..88398ed6f 100644 --- a/java/google/registry/flows/domain/DomainApplicationCreateFlow.java +++ b/java/google/registry/flows/domain/DomainApplicationCreateFlow.java @@ -28,6 +28,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateDomainName; import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables; import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge; import static google.registry.flows.domain.DomainFlowUtils.validateLaunchCreateNotice; +import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationPeriod; import static google.registry.flows.domain.DomainFlowUtils.validateSecDnsExtension; import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsNoticeIfAndOnlyIfNeeded; import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsPeriodNotEnded; @@ -117,6 +118,7 @@ import org.joda.time.DateTime; * @error {@link DomainFlowUtils.DomainReservedException} * @error {@link DomainFlowUtils.DuplicateContactForRoleException} * @error {@link DomainFlowUtils.EmptyDomainNamePartException} + * @error {@link DomainFlowUtils.ExceedsMaxRegistrationYearsException} * @error {@link DomainFlowUtils.ExpiredClaimException} * @error {@link DomainFlowUtils.FeesMismatchException} * @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException} @@ -203,6 +205,7 @@ public final class DomainApplicationCreateFlow implements TransactionalFlow { // notice without specifying a claims key, and override blocks on registering premium domains. verifyUnitIsYears(command.getPeriod()); int years = command.getPeriod().getValue(); + validateRegistrationPeriod(years); validateCreateCommandContactsAndNameservers(command, tld); LaunchCreateExtension launchCreate = eppInput.getSingleExtension(LaunchCreateExtension.class); if (launchCreate != null) { diff --git a/java/google/registry/flows/domain/DomainCreateFlow.java b/java/google/registry/flows/domain/DomainCreateFlow.java index bc1e21175..8387e9c1e 100644 --- a/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/java/google/registry/flows/domain/DomainCreateFlow.java @@ -27,6 +27,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateDomainName; import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables; import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge; import static google.registry.flows.domain.DomainFlowUtils.validateLaunchCreateNotice; +import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationPeriod; import static google.registry.flows.domain.DomainFlowUtils.validateSecDnsExtension; import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsNoticeIfAndOnlyIfNeeded; import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsPeriodNotEnded; @@ -116,6 +117,7 @@ import org.joda.time.DateTime; * @error {@link DomainFlowUtils.DomainReservedException} * @error {@link DomainFlowUtils.DuplicateContactForRoleException} * @error {@link DomainFlowUtils.EmptyDomainNamePartException} + * @error {@link DomainFlowUtils.ExceedsMaxRegistrationYearsException} * @error {@link DomainFlowUtils.ExpiredClaimException} * @error {@link DomainFlowUtils.FeesMismatchException} * @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException} @@ -242,6 +244,7 @@ public class DomainCreateFlow implements TransactionalFlow { validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class)); String repoId = createDomainRepoId(ObjectifyService.allocateId(), registry.getTldStr()); DateTime registrationExpirationTime = leapSafeAddYears(now, years); + validateRegistrationPeriod(years); HistoryEntry historyEntry = buildHistory(repoId, period, now); // Bill for the create. BillingEvent.OneTime createBillingEvent = diff --git a/java/google/registry/flows/domain/DomainFlowUtils.java b/java/google/registry/flows/domain/DomainFlowUtils.java index 57777273e..a3bc1b92b 100644 --- a/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/java/google/registry/flows/domain/DomainFlowUtils.java @@ -22,6 +22,8 @@ import static com.google.common.collect.Sets.difference; import static com.google.common.collect.Sets.union; import static google.registry.flows.domain.DomainPricingLogic.getMatchingLrpToken; import static google.registry.model.EppResourceUtils.loadByForeignKey; +import static google.registry.model.domain.DomainResource.MAX_REGISTRATION_YEARS; +import static google.registry.model.domain.DomainResource.extendRegistrationWithCap; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.registry.Registries.findTldForName; import static google.registry.model.registry.label.ReservedList.getReservation; @@ -30,6 +32,7 @@ import static google.registry.tldconfig.idn.IdnLabelValidator.findValidIdnTableF import static google.registry.util.CollectionUtils.nullToEmpty; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.isAtOrAfter; +import static google.registry.util.DateTimeUtils.leapSafeAddYears; import static google.registry.util.DomainNameUtils.ACE_PREFIX; import com.google.common.base.CharMatcher; @@ -262,7 +265,6 @@ public class DomainFlowUtils { private static void verifyNotInPendingDelete( Key resourceKey) throws EppException { - EppResource resource = ofy().load().key(resourceKey).now(); if (resource.getStatusValues().contains(StatusValue.PENDING_DELETE)) { throw new LinkedResourceInPendingDeleteProhibitsOperationException(resource.getForeignKey()); @@ -621,6 +623,35 @@ public class DomainFlowUtils { } } + /** + * Check whether a new registration period (via a renew) does not extend beyond a maximum number + * of years (e.g. {@link DomainResource#MAX_REGISTRATION_YEARS}). + * + * @throws ExceedsMaxRegistrationYearsException if the new registration period is too long + */ + public static void validateRegistrationPeriod( + DateTime now, + DateTime oldExpirationTime, + int years) throws EppException { + DateTime newExpirationTime = leapSafeAddYears(oldExpirationTime, years); // uncapped + if (extendRegistrationWithCap(now, oldExpirationTime, years).isBefore(newExpirationTime)) { + throw new ExceedsMaxRegistrationYearsException(); + } + } + + /** + * Check whether a new registration period (via a create, allocate, or application create) does + * not extend beyond a maximum number of years (e.g. + * {@link DomainResource#MAX_REGISTRATION_YEARS}). + * + * @throws ExceedsMaxRegistrationYearsException if the new registration period is too long + */ + public static void validateRegistrationPeriod(int years) throws EppException { + if (years > MAX_REGISTRATION_YEARS) { + throw new ExceedsMaxRegistrationYearsException(); + } + } + /** * Adds a secDns extension to a list if the given set of dsData is non-empty. * @@ -830,7 +861,7 @@ public class DomainFlowUtils { } } - /** Create a response extension listign the fees on a domain or application create. */ + /** Create a response extension listing the fees on a domain or application create. */ static FeeTransformResponseExtension createFeeCreateResponse( FeeTransformCommandExtension feeCreate, FeesAndCredits feesAndCredits) { return feeCreate @@ -1260,4 +1291,13 @@ public class DomainFlowUtils { super("Only encoded signed marks are supported"); } } + + /** New registration period exceeds maximum number of years. */ + static class ExceedsMaxRegistrationYearsException extends ParameterValueRangeErrorException { + public ExceedsMaxRegistrationYearsException() { + super(String.format( + "New registration period exceeds maximum number of years (%d)", + MAX_REGISTRATION_YEARS)); + } + } } diff --git a/java/google/registry/flows/domain/DomainRenewFlow.java b/java/google/registry/flows/domain/DomainRenewFlow.java index b7dd7b2ef..dcc53ed8e 100644 --- a/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/java/google/registry/flows/domain/DomainRenewFlow.java @@ -25,9 +25,8 @@ import static google.registry.flows.domain.DomainFlowUtils.newAutorenewBillingEv import static google.registry.flows.domain.DomainFlowUtils.newAutorenewPollMessage; import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime; import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge; +import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationPeriod; import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears; -import static google.registry.model.domain.DomainResource.MAX_REGISTRATION_YEARS; -import static google.registry.model.domain.DomainResource.extendRegistrationWithCap; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.DateTimeUtils.leapSafeAddYears; @@ -95,12 +94,12 @@ import org.joda.time.DateTime; * @error {@link DomainFlowUtils.BadPeriodUnitException} * @error {@link DomainFlowUtils.CurrencyUnitMismatchException} * @error {@link DomainFlowUtils.CurrencyValueScaleException} + * @error {@link DomainFlowUtils.ExceedsMaxRegistrationYearsException} * @error {@link DomainFlowUtils.FeesMismatchException} * @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException} * @error {@link DomainFlowUtils.NotAuthorizedForTldException} * @error {@link DomainFlowUtils.UnsupportedFeeAttributeException} * @error {@link DomainRenewFlow.DomainHasPendingTransferException} - * @error {@link DomainRenewFlow.ExceedsMaxRegistrationYearsException} * @error {@link DomainRenewFlow.IncorrectCurrentExpirationDateException} */ public final class DomainRenewFlow implements TransactionalFlow { @@ -154,9 +153,7 @@ public final class DomainRenewFlow implements TransactionalFlow { .build(); DateTime oldExpirationTime = existingDomain.getRegistrationExpirationTime(); DateTime newExpirationTime = leapSafeAddYears(oldExpirationTime, years); // Uncapped - if (extendRegistrationWithCap(now, oldExpirationTime, years).isBefore(newExpirationTime)) { - throw new ExceedsMaxRegistrationYearsException(); - } + validateRegistrationPeriod(now, oldExpirationTime, years); String tld = existingDomain.getTld(); // Bill for this explicit renew itself. BillingEvent.OneTime explicitRenewEvent = @@ -271,13 +268,4 @@ public final class DomainRenewFlow implements TransactionalFlow { super("The current expiration date is incorrect"); } } - - /** New registration period exceeds maximum number of years. */ - static class ExceedsMaxRegistrationYearsException extends ParameterValueRangeErrorException { - public ExceedsMaxRegistrationYearsException() { - super(String.format( - "Registrations cannot extend for more than %d years into the future", - MAX_REGISTRATION_YEARS)); - } - } } diff --git a/java/google/registry/model/domain/DomainResource.java b/java/google/registry/model/domain/DomainResource.java index f32a897d7..185ae0f04 100644 --- a/java/google/registry/model/domain/DomainResource.java +++ b/java/google/registry/model/domain/DomainResource.java @@ -44,6 +44,7 @@ import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferStatus; import java.util.HashSet; import java.util.Set; +import javax.annotation.Nullable; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -334,11 +335,14 @@ public class DomainResource extends DomainBase /** Return what the expiration time would be if the given number of years were added to it. */ public static DateTime extendRegistrationWithCap( - DateTime now, DateTime currentExpirationTime, Integer extendedRegistrationYears) { + DateTime now, + DateTime currentExpirationTime, + @Nullable Integer extendedRegistrationYears) { // We must cap registration at the max years (aka 10), even if that truncates the last year. return earliestOf( leapSafeAddYears( - currentExpirationTime, Optional.fromNullable(extendedRegistrationYears).or(0)), + currentExpirationTime, + Optional.fromNullable(extendedRegistrationYears).or(0)), leapSafeAddYears(now, MAX_REGISTRATION_YEARS)); } diff --git a/javatests/google/registry/flows/domain/DomainAllocateFlowTest.java b/javatests/google/registry/flows/domain/DomainAllocateFlowTest.java index d68af746d..a4cb7721a 100644 --- a/javatests/google/registry/flows/domain/DomainAllocateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainAllocateFlowTest.java @@ -45,6 +45,7 @@ import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.domain.DomainAllocateFlow.HasFinalStatusException; import google.registry.flows.domain.DomainAllocateFlow.MissingApplicationException; import google.registry.flows.domain.DomainAllocateFlow.OnlySuperuserCanAllocateException; +import google.registry.flows.domain.DomainFlowUtils.ExceedsMaxRegistrationYearsException; import google.registry.flows.exceptions.ResourceAlreadyExistsException; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Flag; @@ -484,4 +485,12 @@ public class DomainAllocateFlowTest thrown.expect(OnlySuperuserCanAllocateException.class); runFlow(CommitMode.LIVE, UserPrivileges.NORMAL); } + + @Test + public void testFailure_max10Years() throws Exception { + setupDomainApplication("tld", TldState.QUIET_PERIOD); + setEppInput("domain_allocate_11_years.xml"); + thrown.expect(ExceedsMaxRegistrationYearsException.class); + runFlowAsSuperuser(); + } } diff --git a/javatests/google/registry/flows/domain/DomainApplicationCreateFlowTest.java b/javatests/google/registry/flows/domain/DomainApplicationCreateFlowTest.java index 074cb1653..788e372e4 100644 --- a/javatests/google/registry/flows/domain/DomainApplicationCreateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainApplicationCreateFlowTest.java @@ -74,6 +74,7 @@ import google.registry.flows.domain.DomainFlowUtils.DomainLabelTooLongException; import google.registry.flows.domain.DomainFlowUtils.DomainReservedException; import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException; import google.registry.flows.domain.DomainFlowUtils.EmptyDomainNamePartException; +import google.registry.flows.domain.DomainFlowUtils.ExceedsMaxRegistrationYearsException; import google.registry.flows.domain.DomainFlowUtils.ExpiredClaimException; import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException; import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForPremiumNameException; @@ -1575,6 +1576,16 @@ public class DomainApplicationCreateFlowTest runFlow(); } + @Test + public void testFailure_max10Years() throws Exception { + createTld("tld", TldState.LANDRUSH); + setEppInput("domain_create_landrush_11_years.xml"); + persistContactsAndHosts(); + clock.advanceOneMilli(); + thrown.expect(ExceedsMaxRegistrationYearsException.class); + runFlow(); + } + /** * There is special logic that disallows a failfast for domains in add grace period and sunrush * add grace period, so make sure that they fail anyways in the actual flow. diff --git a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java index 583f230d1..86dec061a 100644 --- a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java @@ -70,6 +70,7 @@ import google.registry.flows.domain.DomainFlowUtils.DomainLabelTooLongException; import google.registry.flows.domain.DomainFlowUtils.DomainReservedException; import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException; import google.registry.flows.domain.DomainFlowUtils.EmptyDomainNamePartException; +import google.registry.flows.domain.DomainFlowUtils.ExceedsMaxRegistrationYearsException; import google.registry.flows.domain.DomainFlowUtils.ExpiredClaimException; import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException; import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForPremiumNameException; @@ -1792,4 +1793,12 @@ public class DomainCreateFlowTest extends ResourceFlowTestCase + + + + example-one.tld + 11 + jd1234 + sh8013 + sh8013 + + 2fooBAR + + + + + + 2-TLD + 2010-08-16T10:00:00.0Z + + + ABC-12345 + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_create_11_years.xml b/javatests/google/registry/flows/domain/testdata/domain_create_11_years.xml new file mode 100644 index 000000000..c0ac5f7cb --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_create_11_years.xml @@ -0,0 +1,22 @@ + + + + + example.tld + 11 + + ns1.example.net + ns2.example.net + + jd1234 + sh8013 + sh8013 + + 2fooBAR + + + + ABC-12345 + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_create_landrush_11_years.xml b/javatests/google/registry/flows/domain/testdata/domain_create_landrush_11_years.xml new file mode 100644 index 000000000..9154a5ca1 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_create_landrush_11_years.xml @@ -0,0 +1,29 @@ + + + + + + test-validate.tld + 11 + + ns1.example.net + ns2.example.net + + jd1234 + sh8013 + sh8013 + + 2fooBAR + + + + + + landrush + + + ABC-12345 + +