Restrict domain transfer periods to 1 year

It turns out that this ICANN policy appears to prohibit transfers with
registration extensions other than 1 year (section A.8):
https://www.icann.org/resources/pages/policy-2012-03-07-en

This is backed up by the practical fact that we've never seen a registrar
request a transfer for any period other than one year.

And removing the support for multi-year transfers vastly simplifies
transfer logic and eliminates a bunch of annoying corner cases.  Users
still can achieve the same thing by doing a 1-year transfer plus a
manual renewal afterwards for the remainder of the desired extension.

This change leaves in place lots of infrastructure to support multi-year
transfers that is now obsolete (e.g. TransferData.extendedRegistrationYears).
This should all be cleaned up, but it's a lower priority than fixing the
gap itself and insulating ourselves against needing to handle any real
multi-year transfer case.  Once this CL goes in, we can start ignoring
extendedRegistrationYears entirely because it'll always be 1 year, which
makes the cleanup process easier.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=150212864
This commit is contained in:
nickfelt 2017-03-15 10:56:18 -07:00 committed by Ben McIlwain
parent 852f1afb6c
commit f28104ad03
13 changed files with 97 additions and 42 deletions

View file

@ -332,6 +332,7 @@ new ones with the correct approval time).
* The requested domain name is on the premium price list, and this * The requested domain name is on the premium price list, and this
registrar has blocked premium registrations. registrar has blocked premium registrations.
* 2306 * 2306
* Domain transfer period must be one year.
* Periods for domain registrations must be specified in years. * Periods for domain registrations must be specified in years.
* The requested fees cannot be provided in the requested currency. * The requested fees cannot be provided in the requested currency.

View file

@ -44,6 +44,7 @@ import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow; import google.registry.flows.TransactionalFlow;
import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.model.domain.DomainCommand.Transfer; import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import google.registry.model.domain.Period; import google.registry.model.domain.Period;
@ -89,6 +90,7 @@ import org.joda.time.DateTime;
* @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException} * @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException} * @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.TransferPeriodMustBeOneYearException}
* @error {@link DomainFlowUtils.BadPeriodUnitException} * @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException} * @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException} * @error {@link DomainFlowUtils.CurrencyValueScaleException}
@ -216,12 +218,39 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
throw new ObjectAlreadySponsoredException(); throw new ObjectAlreadySponsoredException();
} }
checkAllowedAccessToTld(gainingClientId, existingDomain.getTld()); checkAllowedAccessToTld(gainingClientId, existingDomain.getTld());
verifyUnitIsYears(period); verifyTransferPeriodIsOneYear(period);
if (!isSuperuser) { if (!isSuperuser) {
verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId); verifyPremiumNameIsNotBlocked(targetId, now, gainingClientId);
} }
} }
/**
* Verify that the transfer period is one year.
*
* <p>Restricting transfers to one year is seemingly required by ICANN's <a
* href="https://www.icann.org/resources/pages/policy-2012-03-07-en">Policy on Transfer of
* Registrations between Registrars</a>, section A.8. It states that "the completion by Registry
* Operator of a holder-authorized transfer under this Part A shall result in a one-year extension
* of the existing registration, provided that in no event shall the total unexpired term of a
* registration exceed ten (10) years."
*
* <p>Even if not required, this policy is desirable because it dramatically simplifies the logic
* in transfer flows. Registrars appear to never request 2+ year transfers in practice, and they
* can always decompose an multi-year transfer into a 1-year transfer followed by a manual renewal
* afterwards. The <a href="https://tools.ietf.org/html/rfc5731#section-3.2.4">EPP Domain RFC,
* section 3.2.4</a> says about EPP transfer periods that "the number of units available MAY be
* subject to limits imposed by the server" so we're just limiting the units to one.
*
* <p>Note that clients can omit the period element from the transfer EPP entirely, but then it
* will simply default to one year.
*/
private static void verifyTransferPeriodIsOneYear(Period period) throws EppException {
verifyUnitIsYears(period);
if (period.getValue() != 1) {
throw new TransferPeriodMustBeOneYearException();
}
}
private HistoryEntry buildHistory(Period period, DomainResource existingResource, DateTime now) { private HistoryEntry buildHistory(Period period, DomainResource existingResource, DateTime now) {
return historyBuilder return historyBuilder
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST) .setType(HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST)

View file

@ -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.ParameterValuePolicyErrorException;
/** Domain transfer period must be one year. */
public class TransferPeriodMustBeOneYearException extends ParameterValuePolicyErrorException {
public TransferPeriodMustBeOneYearException() {
super("Domain transfer period must be one year");
}
}

View file

@ -373,7 +373,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
} }
@Test @Test
public void testIgnoredTransferDuringAutoRenewPeriod_succeeds() throws Exception { public void testTransfer_autoRenewGraceActive_onlyAtAutomaticTransferTime_getsSubsumed()
throws Exception {
// Register the domain as the first registrar. // Register the domain as the first registrar.
assertCommandAndResponse("login_valid.xml", "login_response.xml"); assertCommandAndResponse("login_valid.xml", "login_response.xml");
createFakesite(); createFakesite();
@ -382,8 +383,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Request a transfer of the domain to the second registrar. // Request a transfer of the domain to the second registrar.
assertCommandAndResponse("login2_valid.xml", "login_response.xml"); assertCommandAndResponse("login2_valid.xml", "login_response.xml");
assertCommandAndResponse( assertCommandAndResponse(
"domain_transfer_request_2_years.xml", "domain_transfer_request.xml",
"domain_transfer_response_2_years.xml", "domain_transfer_response.xml",
DateTime.parse("2002-05-30T00:00:00Z")); DateTime.parse("2002-05-30T00:00:00Z"));
assertCommandAndResponse("logout.xml", "logout_response.xml"); assertCommandAndResponse("logout.xml", "logout_response.xml");
@ -402,7 +403,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Log back in as the second registrar and verify transfer details. // Log back in as the second registrar and verify transfer details.
assertCommandAndResponse("login2_valid.xml", "login_response.xml"); assertCommandAndResponse("login2_valid.xml", "login_response.xml");
// Verify that domain is in the transfer period now with expiration date two years out. // Verify that domain is in the transfer period now with expiration date still one year out,
// since the transfer should subsume the autorenew that happened during the transfer window.
assertCommandAndResponse( assertCommandAndResponse(
"domain_info_fakesite.xml", "domain_info_fakesite.xml",
"domain_info_response_fakesite_transfer_period.xml", "domain_info_response_fakesite_transfer_period.xml",
@ -425,8 +427,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Request a transfer of the domain to the second registrar. // Request a transfer of the domain to the second registrar.
assertCommandAndResponse("login2_valid.xml", "login_response.xml"); assertCommandAndResponse("login2_valid.xml", "login_response.xml");
assertCommandAndResponse( assertCommandAndResponse(
"domain_transfer_request_2_years.xml", "domain_transfer_request.xml",
"domain_transfer_response_2_years.xml", "domain_transfer_response.xml",
DateTime.parse("2002-05-30T00:00:00Z")); DateTime.parse("2002-05-30T00:00:00Z"));
assertCommandAndResponse("logout.xml", "logout_response.xml"); assertCommandAndResponse("logout.xml", "logout_response.xml");
@ -464,8 +466,8 @@ public class EppLifecycleDomainTest extends EppTestCase {
// Request a transfer of the domain to the second registrar. // Request a transfer of the domain to the second registrar.
assertCommandAndResponse("login2_valid.xml", "login_response.xml"); assertCommandAndResponse("login2_valid.xml", "login_response.xml");
assertCommandAndResponse( assertCommandAndResponse(
"domain_transfer_request_2_years.xml", "domain_transfer_request.xml",
"domain_transfer_response_2_years.xml", "domain_transfer_response.xml",
DateTime.parse("2002-05-30T00:00:00Z")); DateTime.parse("2002-05-30T00:00:00Z"));
assertCommandAndResponse("logout.xml", "logout_response.xml"); assertCommandAndResponse("logout.xml", "logout_response.xml");

View file

@ -53,6 +53,7 @@ import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Cancellation.Builder; import google.registry.model.billing.BillingEvent.Cancellation.Builder;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
@ -500,22 +501,35 @@ public class DomainTransferRequestFlowTest
} }
@Test @Test
public void testSuccess_missingPeriod() throws Exception { public void testSuccess_missingPeriod_defaultsToOneYear() throws Exception {
setupDomain("example", "tld"); setupDomain("example", "tld");
doSuccessfulTest("domain_transfer_request_missing_period.xml", doSuccessfulTest(
"domain_transfer_request_missing_period.xml",
"domain_transfer_request_response.xml"); "domain_transfer_request_response.xml");
} }
@Test
public void testFailure_multiYearPeriod() throws Exception {
setupDomain("example", "tld");
clock.advanceOneMilli();
thrown.expect(TransferPeriodMustBeOneYearException.class);
doFailingTest("domain_transfer_request_2_years.xml");
}
@Test @Test
public void testSuccess_cappedExpiration() throws Exception { public void testSuccess_cappedExpiration() throws Exception {
setupDomain("example", "tld"); setupDomain("example", "tld");
// The current expiration is in 15 months, so requesting 10 years would give 11 years 3 months, // Set the domain to expire 10 years from now (as if it were just created with a 10-year term).
// were it not that we cap at 10 years. (MAX_REGISTRATION_YEARS == 10 and is unlikely to ever domain = persistResource(domain.asBuilder()
// change; we just use a constant for readability.) .setRegistrationExpirationTime(clock.nowUtc().plusYears(10))
.build());
// New expiration time should be capped at exactly 10 years from the transfer server-approve
// time, so the domain only ends up gaining the 5-day transfer window's worth of extra
// registration time.
clock.advanceOneMilli(); clock.advanceOneMilli();
doSuccessfulTest( doSuccessfulTest(
"domain_transfer_request_10_years.xml", "domain_transfer_request.xml",
"domain_transfer_request_response_10_years.xml", "domain_transfer_request_response_10_year_cap.xml",
clock.nowUtc().plus(Registry.get("tld").getAutomaticTransferLength()).plusYears(10)); clock.nowUtc().plus(Registry.get("tld").getAutomaticTransferLength()).plusYears(10));
} }
@ -534,16 +548,16 @@ public class DomainTransferRequestFlowTest
doSuccessfulTest( doSuccessfulTest(
"domain_transfer_request_fee.xml", "domain_transfer_request_fee.xml",
"domain_transfer_request_response_fees.xml", "domain_transfer_request_response_fees.xml",
domain.getRegistrationExpirationTime().plusYears(3), domain.getRegistrationExpirationTime().plusYears(1),
new ImmutableMap.Builder<String, String>() new ImmutableMap.Builder<String, String>()
.put("DOMAIN", "expensive-domain.foo") .put("DOMAIN", "expensive-domain.foo")
.put("YEARS", "3") .put("YEARS", "1")
.put("AMOUNT", "133.00") .put("AMOUNT", "111.00")
.put("EXDATE", "2004-09-08T22:00:00.0Z") .put("EXDATE", "2002-09-08T22:00:00.0Z")
.put("FEE_VERSION", "0.6") .put("FEE_VERSION", "0.6")
.put("FEE_NS", "fee") .put("FEE_NS", "fee")
.build(), .build(),
Optional.of(Money.of(USD, 133))); Optional.of(Money.of(USD, 111)));
} }
@Test @Test

View file

@ -1,15 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<domain:transfer
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">10</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -18,7 +18,7 @@
<extension> <extension>
<fee:trnData xmlns:fee="urn:ietf:params:xml:ns:fee-%FEE_VERSION%"> <fee:trnData xmlns:fee="urn:ietf:params:xml:ns:fee-%FEE_VERSION%">
<fee:currency>USD</fee:currency> <fee:currency>USD</fee:currency>
<fee:fee description="renew">33.00</fee:fee> <fee:fee description="renew">11.00</fee:fee>
<fee:fee description="transfer">100.00</fee:fee> <fee:fee description="transfer">100.00</fee:fee>
</fee:trnData> </fee:trnData>
</extension> </extension>

View file

@ -23,7 +23,7 @@
<domain:crDate>2000-06-01T00:04:00Z</domain:crDate> <domain:crDate>2000-06-01T00:04:00Z</domain:crDate>
<domain:upID>NewRegistrar</domain:upID> <domain:upID>NewRegistrar</domain:upID>
<domain:upDate>2000-06-08T00:00:00Z</domain:upDate> <domain:upDate>2000-06-08T00:00:00Z</domain:upDate>
<domain:exDate>2004-06-01T00:04:00Z</domain:exDate> <domain:exDate>2003-06-01T00:04:00Z</domain:exDate>
<domain:trDate>2002-06-04T00:00:00Z</domain:trDate> <domain:trDate>2002-06-04T00:00:00Z</domain:trDate>
<domain:authInfo> <domain:authInfo>
<domain:pw>2fooBAR</domain:pw> <domain:pw>2fooBAR</domain:pw>

View file

@ -19,7 +19,7 @@
<domain:clID>TheRegistrar</domain:clID> <domain:clID>TheRegistrar</domain:clID>
<domain:crID>NewRegistrar</domain:crID> <domain:crID>NewRegistrar</domain:crID>
<domain:crDate>2000-06-01T00:04:00Z</domain:crDate> <domain:crDate>2000-06-01T00:04:00Z</domain:crDate>
<domain:exDate>2004-06-01T00:04:00Z</domain:exDate> <domain:exDate>2003-06-01T00:04:00Z</domain:exDate>
<domain:trDate>2002-06-04T00:00:00Z</domain:trDate> <domain:trDate>2002-06-04T00:00:00Z</domain:trDate>
<domain:authInfo> <domain:authInfo>
<domain:pw>2fooBAR</domain:pw> <domain:pw>2fooBAR</domain:pw>

View file

@ -19,7 +19,7 @@
<domain:clID>TheRegistrar</domain:clID> <domain:clID>TheRegistrar</domain:clID>
<domain:crID>NewRegistrar</domain:crID> <domain:crID>NewRegistrar</domain:crID>
<domain:crDate>2000-06-01T00:04:00Z</domain:crDate> <domain:crDate>2000-06-01T00:04:00Z</domain:crDate>
<domain:exDate>2004-06-01T00:04:00Z</domain:exDate> <domain:exDate>2003-06-01T00:04:00Z</domain:exDate>
<domain:trDate>2002-06-04T00:00:00Z</domain:trDate> <domain:trDate>2002-06-04T00:00:00Z</domain:trDate>
<domain:authInfo> <domain:authInfo>
<domain:pw>2fooBAR</domain:pw> <domain:pw>2fooBAR</domain:pw>

View file

@ -4,7 +4,7 @@
<domain:transfer <domain:transfer
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0"> xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>fakesite.example</domain:name> <domain:name>fakesite.example</domain:name>
<domain:period unit="y">2</domain:period> <domain:period unit="y">1</domain:period>
<domain:authInfo> <domain:authInfo>
<domain:pw>2fooBAR</domain:pw> <domain:pw>2fooBAR</domain:pw>
</domain:authInfo> </domain:authInfo>

View file

@ -11,7 +11,7 @@
<domain:reDate>2002-05-30T00:00:00Z</domain:reDate> <domain:reDate>2002-05-30T00:00:00Z</domain:reDate>
<domain:acID>NewRegistrar</domain:acID> <domain:acID>NewRegistrar</domain:acID>
<domain:acDate>2002-06-04T00:00:00Z</domain:acDate> <domain:acDate>2002-06-04T00:00:00Z</domain:acDate>
<domain:exDate>2004-06-01T00:04:00Z</domain:exDate> <domain:exDate>2003-06-01T00:04:00Z</domain:exDate>
</domain:trnData> </domain:trnData>
</resData> </resData>
<trID> <trID>