diff --git a/docs/flows.md b/docs/flows.md index 5ed134696..2d0057d62 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -535,6 +535,7 @@ An EPP flow that creates a new domain resource. * The checksum in the specified TCNID does not validate. * Domain name is under tld which doesn't exist. * 2005 + * The allocation token is invalid. * Domain name must have exactly one part above the TLD. * Domain name must not equal an existing multi-part TLD. * The requested fee is expressed in a scale that is invalid for the given @@ -557,6 +558,7 @@ An EPP flow that creates a new domain resource. * 2303 * Resource linked to this domain does not exist. * 2304 + * There is an open application for this domain. * The claims period for this TLD has ended. * Requested domain does not have nameserver-restricted reservation for a TLD that requires such a reservation to create domains. @@ -572,7 +574,8 @@ An EPP flow that creates a new domain resource. registrar has blocked premium registrations. * Registrant is not whitelisted for this TLD. * Requested domain does not require a claims notice. - * There is an open application for this domain. +* 2305 + * The allocation token was already redeemed. * 2306 * Domain names can only contain a-z, 0-9, '.' and '-'. * Periods for domain registrations must be specified in years. diff --git a/java/google/registry/flows/domain/AllocationTokenFlowUtils.java b/java/google/registry/flows/domain/AllocationTokenFlowUtils.java new file mode 100644 index 000000000..bdd4a067a --- /dev/null +++ b/java/google/registry/flows/domain/AllocationTokenFlowUtils.java @@ -0,0 +1,70 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain; + +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.google.common.net.InternetDomainName; +import com.googlecode.objectify.Key; +import google.registry.flows.EppException; +import google.registry.flows.EppException.AssociationProhibitsOperationException; +import google.registry.flows.EppException.ParameterValueSyntaxErrorException; +import google.registry.model.domain.AllocationToken; +import google.registry.model.registry.Registry; +import google.registry.model.reporting.HistoryEntry; + +/** Static utility functions for dealing with {@link AllocationToken}s in domain flows. */ +public class AllocationTokenFlowUtils { + + /** + * Verifies that a given allocation token string is valid. + * + * @return the loaded {@link AllocationToken} for that string. + * @throws InvalidAllocationTokenException if the token doesn't exist. + */ + static AllocationToken verifyToken( + InternetDomainName domainName, String token, Registry registry, String clientId) + throws EppException { + AllocationToken tokenEntity = ofy().load().key(Key.create(AllocationToken.class, token)).now(); + if (tokenEntity == null) { + throw new InvalidAllocationTokenException(); + } + if (tokenEntity.isRedeemed()) { + throw new AlreadyRedeemedAllocationTokenException(); + } + return tokenEntity; + } + + /** Redeems an {@link AllocationToken}, returning the redeemed copy. */ + static AllocationToken redeemToken( + AllocationToken token, Key redemptionHistoryEntry) { + return token.asBuilder().setRedemptionHistoryEntry(redemptionHistoryEntry).build(); + } + + /** The allocation token was already redeemed. */ + static class AlreadyRedeemedAllocationTokenException + extends AssociationProhibitsOperationException { + public AlreadyRedeemedAllocationTokenException() { + super("The allocation token was already redeemed"); + } + } + + /** The allocation token is invalid. */ + static class InvalidAllocationTokenException extends ParameterValueSyntaxErrorException { + public InvalidAllocationTokenException() { + super("The allocation token is invalid"); + } + } +} diff --git a/java/google/registry/flows/domain/DomainCreateFlow.java b/java/google/registry/flows/domain/DomainCreateFlow.java index 35654f0d9..9c4a74c14 100644 --- a/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/java/google/registry/flows/domain/DomainCreateFlow.java @@ -17,6 +17,8 @@ package google.registry.flows.domain; import static google.registry.flows.FlowUtils.persistEntityChanges; import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist; +import static google.registry.flows.domain.AllocationTokenFlowUtils.redeemToken; +import static google.registry.flows.domain.AllocationTokenFlowUtils.verifyToken; import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.cloneAndLinkReferences; import static google.registry.flows.domain.DomainFlowUtils.createFeeCreateResponse; @@ -65,15 +67,13 @@ import google.registry.flows.custom.DomainCreateFlowCustomLogic; import google.registry.flows.custom.DomainCreateFlowCustomLogic.BeforeResponseParameters; import google.registry.flows.custom.DomainCreateFlowCustomLogic.BeforeResponseReturnData; import google.registry.flows.custom.EntityChanges; -import google.registry.flows.domain.DomainFlowUtils.DomainNotAllowedForTldWithCreateRestrictionException; -import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForNameserverRestrictedDomainException; -import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverWhitelistException; import google.registry.model.ImmutableObject; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.OneTime; import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Recurring; +import google.registry.model.domain.AllocationToken; import google.registry.model.domain.DomainApplication; import google.registry.model.domain.DomainCommand.Create; import google.registry.model.domain.DomainResource; @@ -116,7 +116,11 @@ import org.joda.time.Duration; * @error {@link google.registry.flows.exceptions.ResourceAlreadyExistsException} * @error {@link google.registry.flows.EppException.UnimplementedExtensionException} * @error {@link google.registry.flows.ExtensionManager.UndeclaredServiceExtensionException} - * @error {@link google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException} + * @error {@link AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException} + * @error {@link AllocationTokenFlowUtils.InvalidAllocationTokenException} + * @error {@link DomainCreateFlow.DomainHasOpenApplicationsException} + * @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException} + * @error {@link DomainFlowUtils.NotAuthorizedForTldException} * @error {@link DomainFlowUtils.AcceptedTooLongAgoException} * @error {@link DomainFlowUtils.BadDomainNameCharacterException} * @error {@link DomainFlowUtils.BadDomainNamePartsCountException} @@ -127,7 +131,7 @@ import org.joda.time.Duration; * @error {@link DomainFlowUtils.CurrencyValueScaleException} * @error {@link DomainFlowUtils.DashesInThirdAndFourthException} * @error {@link DomainFlowUtils.DomainLabelTooLongException} - * @error {@link DomainNotAllowedForTldWithCreateRestrictionException} + * @error {@link DomainFlowUtils.DomainNotAllowedForTldWithCreateRestrictionException} * @error {@link DomainFlowUtils.DomainReservedException} * @error {@link DomainFlowUtils.DuplicateContactForRoleException} * @error {@link DomainFlowUtils.EmptyDomainNamePartException} @@ -153,8 +157,8 @@ import org.joda.time.Duration; * @error {@link DomainFlowUtils.MissingTechnicalContactException} * @error {@link DomainFlowUtils.NameserversNotAllowedForDomainException} * @error {@link DomainFlowUtils.NameserversNotAllowedForTldException} - * @error {@link NameserversNotSpecifiedForNameserverRestrictedDomainException} - * @error {@link NameserversNotSpecifiedForTldWithNameserverWhitelistException} + * @error {@link DomainFlowUtils.NameserversNotSpecifiedForNameserverRestrictedDomainException} + * @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverWhitelistException} * @error {@link DomainFlowUtils.PremiumNameBlockedException} * @error {@link DomainFlowUtils.RegistrantNotAllowedException} * @error {@link DomainFlowUtils.RegistrarMustBeActiveToCreateDomainsException} @@ -165,8 +169,6 @@ import org.joda.time.Duration; * @error {@link DomainFlowUtils.UnexpectedClaimsNoticeException} * @error {@link DomainFlowUtils.UnsupportedFeeAttributeException} * @error {@link DomainFlowUtils.UnsupportedMarkTypeException} - * @error {@link DomainCreateFlow.DomainHasOpenApplicationsException} - * @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException} */ @ReportingSpec(ActivityReportField.DOMAIN_CREATE) public class DomainCreateFlow implements TransactionalFlow { @@ -247,12 +249,15 @@ public class DomainCreateFlow implements TransactionalFlow { verifyPremiumNameIsNotBlocked(targetId, now, clientId); verifyNoOpenApplications(now); verifyIsGaOrIsSpecialCase(tldState, isAnchorTenant); - signedMarkId = hasSignedMarks + if (hasSignedMarks) { // If a signed mark was provided, then it must match the desired domain label. Get the mark // at this point so that we can verify it before the "after validation" extension point. - ? tmchUtils.verifySignedMarks(launchCreate.getSignedMarks(), domainLabel, now).getId() - : null; + signedMarkId = + tmchUtils.verifySignedMarks(launchCreate.getSignedMarks(), domainLabel, now).getId(); + } } + Optional allocationToken = + verifyAllocationTokenIfPresent(domainName, registry, clientId); customLogic.afterValidation( DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder() .setDomainName(domainName) @@ -316,6 +321,7 @@ public class DomainCreateFlow implements TransactionalFlow { ForeignKeyIndex.create(newDomain, newDomain.getDeletionTime()), EppResourceIndex.create(Key.create(newDomain))); + allocationToken.ifPresent(t -> entitiesToSave.add(redeemToken(t, Key.create(historyEntry)))); // Anchor tenant registrations override LRP, and landrush applications can skip it. // If a token is passed in outside of an LRP phase, it is simply ignored (i.e. never redeemed). if (isLrpCreate(registry, isAnchorTenant, now)) { @@ -362,7 +368,7 @@ public class DomainCreateFlow implements TransactionalFlow { } } - /** Prohibit registrations for non-qlp and non-superuser outside of GA. **/ + /** Prohibit registrations for non-QLP and non-superuser outside of General Availability. **/ private void verifyIsGaOrIsSpecialCase(TldState tldState, boolean isAnchorTenant) throws NoGeneralRegistrationsInCurrentPhaseException { if (!isAnchorTenant && tldState != TldState.GENERAL_AVAILABILITY) { @@ -370,6 +376,17 @@ public class DomainCreateFlow implements TransactionalFlow { } } + /** Verifies and returns the allocation token if one is specified, otherwise does nothing. */ + private Optional verifyAllocationTokenIfPresent( + InternetDomainName domainName, Registry registry, String clientId) + throws EppException { + AllocationTokenExtension ext = eppInput.getSingleExtension(AllocationTokenExtension.class); + return Optional.ofNullable( + (ext == null) + ? null + : verifyToken(domainName, ext.getAllocationToken(), registry, clientId)); + } + private HistoryEntry buildHistoryEntry( String repoId, Registry registry, DateTime now, Period period, Duration addGracePeriod) { // We ignore prober transactions diff --git a/java/google/registry/model/domain/AllocationToken.java b/java/google/registry/model/domain/AllocationToken.java index 9aca4e3b0..af9016f44 100644 --- a/java/google/registry/model/domain/AllocationToken.java +++ b/java/google/registry/model/domain/AllocationToken.java @@ -73,6 +73,7 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { } public Builder setToken(String token) { + checkState(getInstance().token == null, "token can only be set once"); checkArgumentNotNull(token, "token must not be null"); checkArgument(!token.isEmpty(), "token must not be blank"); getInstance().token = token; diff --git a/java/google/registry/model/domain/token/AllocationTokenExtension.java b/java/google/registry/model/domain/token/AllocationTokenExtension.java index d6fa72949..496053fdb 100644 --- a/java/google/registry/model/domain/token/AllocationTokenExtension.java +++ b/java/google/registry/model/domain/token/AllocationTokenExtension.java @@ -16,8 +16,10 @@ package google.registry.model.domain.token; import google.registry.model.ImmutableObject; import google.registry.model.eppinput.EppInput.CommandExtension; +import google.registry.xml.TrimWhitespaceAdapter; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlValue; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; /** * An allocation token extension that may be present on EPP domain commands. @@ -30,6 +32,7 @@ public class AllocationTokenExtension extends ImmutableObject implements Command /** The allocation token for the command. */ @XmlValue + @XmlJavaTypeAdapter(TrimWhitespaceAdapter.class) String allocationToken; public String getAllocationToken() { diff --git a/java/google/registry/xml/TrimWhitespaceAdapter.java b/java/google/registry/xml/TrimWhitespaceAdapter.java new file mode 100644 index 000000000..f0b978998 --- /dev/null +++ b/java/google/registry/xml/TrimWhitespaceAdapter.java @@ -0,0 +1,48 @@ +// 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.xml; + +import com.google.common.base.CharMatcher; +import javax.annotation.Nullable; +import javax.xml.bind.annotation.adapters.XmlAdapter; + +/** + * {@link XmlAdapter} which trims all whitespace surrounding a String. + * + *

This is primarily useful for @XmlValue-annotated fields in JAXB objects, as XML + * values can commonly be formatted like so: + * + *

{@code
+ *   <ns:tag>
+ *     XML value here.
+ *   </ns:tag>
+ * }
+ */ +public class TrimWhitespaceAdapter extends XmlAdapter { + + private static final CharMatcher WHITESPACE = CharMatcher.anyOf(" \t\r\n"); + + @Override + @Nullable + public String unmarshal(@Nullable String value) { + return (value == null) ? null : WHITESPACE.trimFrom(value); + } + + @Override + @Nullable + public String marshal(@Nullable String str) { + return str; + } +} diff --git a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java index c0182def5..e3e436c8c 100644 --- a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java @@ -59,11 +59,14 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; +import com.googlecode.objectify.Key; import google.registry.flows.EppException; import google.registry.flows.EppException.UnimplementedExtensionException; import google.registry.flows.EppRequestSource; import google.registry.flows.ExtensionManager.UndeclaredServiceExtensionException; import google.registry.flows.ResourceFlowTestCase; +import google.registry.flows.domain.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException; +import google.registry.flows.domain.AllocationTokenFlowUtils.InvalidAllocationTokenException; import google.registry.flows.domain.DomainCreateFlow.DomainHasOpenApplicationsException; import google.registry.flows.domain.DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException; import google.registry.flows.domain.DomainFlowUtils.AcceptedTooLongAgoException; @@ -120,6 +123,7 @@ import google.registry.flows.exceptions.ResourceAlreadyExistsException; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Reason; +import google.registry.model.domain.AllocationToken; import google.registry.model.domain.DomainResource; import google.registry.model.domain.GracePeriod; import google.registry.model.domain.LrpTokenEntity; @@ -381,11 +385,40 @@ public class DomainCreateFlowTest extends ResourceFlowTestCase builder.setCreationTime(DateTime.parse("2010-11-13T05:00:00Z"))); assertThat(thrown).hasMessageThat().isEqualTo("creationTime can only be set once"); } + + @Test + public void testSetToken_cantCallMoreThanOnce() throws Exception { + AllocationToken.Builder builder = new AllocationToken.Builder().setToken("foobar"); + IllegalStateException thrown = + expectThrows(IllegalStateException.class, () -> builder.setToken("barfoo")); + assertThat(thrown).hasMessageThat().isEqualTo("token can only be set once"); + } } diff --git a/javatests/google/registry/xml/DateAdapterTest.java b/javatests/google/registry/xml/DateAdapterTest.java index 11f03ffe7..580e4ecb9 100644 --- a/javatests/google/registry/xml/DateAdapterTest.java +++ b/javatests/google/registry/xml/DateAdapterTest.java @@ -25,6 +25,7 @@ import org.junit.runners.JUnit4; /** Unit tests for {@link DateAdapter}. */ @RunWith(JUnit4.class) public class DateAdapterTest { + @Test public void testMarshal() { assertThat(new DateAdapter().marshal( diff --git a/javatests/google/registry/xml/TrimWhitespaceAdapterTest.java b/javatests/google/registry/xml/TrimWhitespaceAdapterTest.java new file mode 100644 index 000000000..f38631edf --- /dev/null +++ b/javatests/google/registry/xml/TrimWhitespaceAdapterTest.java @@ -0,0 +1,45 @@ +// 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.xml; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link TrimWhitespaceAdapter}. */ +@RunWith(JUnit4.class) +public class TrimWhitespaceAdapterTest { + + @Test + public void testUnmarshal() { + TrimWhitespaceAdapter adapter = new TrimWhitespaceAdapter(); + assertThat(adapter.unmarshal("blah")).isEqualTo("blah"); + assertThat(adapter.unmarshal("")).isEmpty(); + assertThat(adapter.unmarshal(null)).isNull(); + assertThat(adapter.unmarshal("\n test foo bar \n \r")).isEqualTo("test foo bar"); + } + + @Test + public void testMarshal() { + TrimWhitespaceAdapter adapter = new TrimWhitespaceAdapter(); + assertThat(adapter.marshal("blah")).isEqualTo("blah"); + assertThat(adapter.marshal("")).isEmpty(); + assertThat(adapter.marshal(null)).isNull(); + assertThat(adapter.marshal("\n test foo bar \n \r")) + .isEqualTo("\n test foo bar \n \r"); + } +} diff --git a/javatests/google/registry/xml/UtcDateTimeAdapterTest.java b/javatests/google/registry/xml/UtcDateTimeAdapterTest.java index 5f7d38d1e..02695e199 100644 --- a/javatests/google/registry/xml/UtcDateTimeAdapterTest.java +++ b/javatests/google/registry/xml/UtcDateTimeAdapterTest.java @@ -27,6 +27,7 @@ import org.junit.runners.JUnit4; /** Unit tests for {@link UtcDateTimeAdapter}. */ @RunWith(JUnit4.class) public class UtcDateTimeAdapterTest { + @Test public void testMarshal() { assertThat(new UtcDateTimeAdapter().marshal(new DateTime(2010, 10, 17, 4, 20, 0, UTC)))