diff --git a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java index 0179f2dc6..2ee16dc08 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java @@ -26,6 +26,7 @@ import static google.registry.flows.domain.DomainFlowUtils.checkHasBillingAccoun import static google.registry.flows.domain.DomainFlowUtils.getReservationTypes; import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest; import static google.registry.flows.domain.DomainFlowUtils.isAnchorTenant; +import static google.registry.flows.domain.DomainFlowUtils.isRegisterBsaCreate; import static google.registry.flows.domain.DomainFlowUtils.isReserved; import static google.registry.flows.domain.DomainFlowUtils.isValidReservedCreate; import static google.registry.flows.domain.DomainFlowUtils.validateDomainName; @@ -269,13 +270,13 @@ public final class DomainCheckFlow implements TransactionalFlow { if (tokenResult.isPresent()) { return tokenResult; } - if (bsaBlockedDomains.contains(domainName)) { - // TODO(weiminyu): extract to a constant for here and CheckApiAction. - // Excerpt from BSA's custom message. Max len 32 chars by EPP XML schema. - return Optional.of("Blocked by a GlobalBlock service"); - } else { + if (isRegisterBsaCreate(domainName, allocationToken) + || !bsaBlockedDomains.contains(domainName)) { return Optional.empty(); } + // TODO(weiminyu): extract to a constant for here and CheckApiAction. + // Excerpt from BSA's custom message. Max len 32 chars by EPP XML schema. + return Optional.of("Blocked by a GlobalBlock service"); } /** Handle the fee check extension. */ diff --git a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java index 192499794..9c46627be 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java @@ -330,7 +330,7 @@ public final class DomainCreateFlow implements MutatingFlow { .verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now) .getId(); } - verifyNotBlockedByBsa(domainLabel, tld, now); + verifyNotBlockedByBsa(domainName, tld, now, allocationToken); flowCustomLogic.afterValidation( DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder() .setDomainName(domainName) @@ -421,8 +421,7 @@ public final class DomainCreateFlow implements MutatingFlow { createNameCollisionOneTimePollMessage(targetId, domainHistory, registrarId, now)); } entitiesToSave.add(domain, domainHistory); - if (allocationToken.isPresent() - && TokenType.SINGLE_USE.equals(allocationToken.get().getTokenType())) { + if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) { entitiesToSave.add( allocationTokenFlowUtils.redeemToken( allocationToken.get(), domainHistory.getHistoryEntryId())); diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java index 8de74a0e6..f4e39e3c8 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -27,6 +27,7 @@ import static com.google.common.collect.Sets.intersection; import static com.google.common.collect.Sets.union; import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked; import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS; +import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA; import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY; import static google.registry.model.tld.Tld.TldState.PREDELEGATION; import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD; @@ -265,9 +266,14 @@ public class DomainFlowUtils { * Verifies that the {@code domainLabel} is not blocked by any BSA block label for the given * {@code tld} at the specified time. */ - public static void verifyNotBlockedByBsa(String domainLabel, Tld tld, DateTime now) + public static void verifyNotBlockedByBsa( + InternetDomainName domainName, + Tld tld, + DateTime now, + Optional allocationToken) throws DomainLabelBlockedByBsaException { - if (isBlockedByBsa(domainLabel, tld, now)) { + if (!isRegisterBsaCreate(domainName, allocationToken) + && isBlockedByBsa(domainName.parts().get(0), tld, now)) { throw new DomainLabelBlockedByBsaException(); } } @@ -311,6 +317,15 @@ public class DomainFlowUtils { && token.get().getDomainName().get().equals(domainName.toString()); } + /** Returns whether a given domain create request may bypass the BSA block check. */ + public static boolean isRegisterBsaCreate( + InternetDomainName domainName, Optional token) { + return token.isPresent() + && token.get().getTokenType().equals(REGISTER_BSA) + && token.get().getDomainName().isPresent() + && token.get().getDomainName().get().equals(domainName.toString()); + } + /** Check if the registrar running the flow has access to the TLD in question. */ public static void checkAllowedAccessToTld(String registrarId, String tld) throws EppException { if (!Registrar.loadByRegistrarIdCached(registrarId).get().getAllowedTlds().contains(tld)) { diff --git a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java index 757141391..fb32a269e 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java @@ -73,7 +73,6 @@ 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.token.AllocationToken; -import google.registry.model.domain.token.AllocationToken.TokenType; import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; @@ -258,8 +257,7 @@ public final class DomainRenewFlow implements MutatingFlow { ImmutableSet.Builder entitiesToSave = new ImmutableSet.Builder<>(); entitiesToSave.add( newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage); - if (allocationToken.isPresent() - && TokenType.SINGLE_USE.equals(allocationToken.get().getTokenType())) { + if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) { entitiesToSave.add( allocationTokenFlowUtils.redeemToken( allocationToken.get(), domainHistory.getHistoryEntryId())); diff --git a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java index 1d6d67438..a187cdbb2 100644 --- a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java @@ -36,7 +36,6 @@ import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName import google.registry.model.domain.token.AllocationToken; import google.registry.model.domain.token.AllocationToken.TokenBehavior; import google.registry.model.domain.token.AllocationToken.TokenStatus; -import google.registry.model.domain.token.AllocationToken.TokenType; import google.registry.model.domain.token.AllocationTokenExtension; import google.registry.model.reporting.HistoryEntry.HistoryEntryId; import google.registry.model.tld.Tld; @@ -105,8 +104,7 @@ public class AllocationTokenFlowUtils { /** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */ public AllocationToken redeemToken(AllocationToken token, HistoryEntryId redemptionHistoryId) { checkArgument( - TokenType.SINGLE_USE.equals(token.getTokenType()), - "Only SINGLE_USE tokens can be marked as redeemed"); + token.getTokenType().isOneTimeUse(), "Only SINGLE_USE tokens can be marked as redeemed"); return token.asBuilder().setRedemptionHistoryId(redemptionHistoryId).build(); } diff --git a/core/src/main/java/google/registry/model/domain/token/AllocationToken.java b/core/src/main/java/google/registry/model/domain/token/AllocationToken.java index 978678e98..09ef5a661 100644 --- a/core/src/main/java/google/registry/model/domain/token/AllocationToken.java +++ b/core/src/main/java/google/registry/model/domain/token/AllocationToken.java @@ -22,6 +22,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.CAN import static google.registry.model.domain.token.AllocationToken.TokenStatus.ENDED; import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED; import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID; +import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.CollectionUtils.forceEmptyToNull; import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; @@ -120,18 +121,37 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda /** Type of the token that indicates how and where it should be used. */ public enum TokenType { /** Token used for bulk pricing */ - BULK_PRICING, + BULK_PRICING(/* isOneTimeUse= */ false), /** Token saved on a TLD to use if no other token is passed from the client */ - DEFAULT_PROMO, + DEFAULT_PROMO(/* isOneTimeUse= */ false), /** This is the old name for what is now BULK_PRICING. */ // TODO(sarahbot@): Remove this type once all tokens of this type have been scrubbed from the // database @Deprecated - PACKAGE, + PACKAGE(/* isOneTimeUse= */ false), /** Invalid after use */ - SINGLE_USE, + SINGLE_USE(/* isOneTimeUse= */ true), /** Do not expire after use */ - UNLIMITED_USE, + UNLIMITED_USE(/* isOneTimeUse= */ false), + /** + * Allows bypassing the BSA check during domain creation, otherwise has the same semantics as + * {@link #SINGLE_USE}. + * + *

This token applies to a single domain only. If the domain is not blocked by BSA at the + * redemption time this token is processed like {@code SINGLE_USE}, as mentioned above. + */ + REGISTER_BSA(/* isOneTimeUse= */ true); + + private final boolean isOneTimeUse; + + private TokenType(boolean isOneTimeUse) { + this.isOneTimeUse = isOneTimeUse; + } + + /** Returns true if token should be invalidated after use. */ + public boolean isOneTimeUse() { + return this.isOneTimeUse; + } } /** @@ -361,12 +381,11 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda || !getInstance().discountPremiums, "Bulk tokens cannot discount premium names"); checkArgument( - getInstance().domainName == null || TokenType.SINGLE_USE.equals(getInstance().tokenType), - "Domain name can only be specified for SINGLE_USE tokens"); + getInstance().domainName == null || getInstance().tokenType.isOneTimeUse(), + "Domain name can only be specified for SINGLE_USE or REGISTER_BSA tokens"); checkArgument( - getInstance().redemptionHistoryId == null - || TokenType.SINGLE_USE.equals(getInstance().tokenType), - "Redemption history entry can only be specified for SINGLE_USE tokens"); + getInstance().redemptionHistoryId == null || getInstance().tokenType.isOneTimeUse(), + "Redemption history entry can only be specified for SINGLE_USE or REGISTER_BSA tokens"); checkArgument( getInstance().tokenType != TokenType.BULK_PRICING || (getInstance().allowedClientIds != null @@ -378,6 +397,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda checkArgument( getInstance().discountFraction > 0 || getInstance().discountYears == 1, "Discount years can only be specified along with a discount fraction"); + if (getInstance().getTokenType().equals(REGISTER_BSA)) { + checkArgumentNotNull( + getInstance().domainName, "REGISTER_BSA tokens must be tied to a domain"); + } if (getInstance().registrationBehavior.equals(RegistrationBehavior.ANCHOR_TENANT)) { checkArgumentNotNull( getInstance().domainName, "ANCHOR_TENANT tokens must be tied to a domain"); diff --git a/core/src/main/java/google/registry/tools/DeleteAllocationTokensCommand.java b/core/src/main/java/google/registry/tools/DeleteAllocationTokensCommand.java index ab5520c79..4effbcf4b 100644 --- a/core/src/main/java/google/registry/tools/DeleteAllocationTokensCommand.java +++ b/core/src/main/java/google/registry/tools/DeleteAllocationTokensCommand.java @@ -18,7 +18,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.Iterables.partition; import static com.google.common.collect.Streams.stream; -import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import com.beust.jcommander.Parameter; @@ -78,7 +77,7 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens ImmutableSet> tokensToDelete = tm().loadByKeys(batch).values().stream() .filter(t -> withDomains || !t.getDomainName().isPresent()) - .filter(t -> SINGLE_USE.equals(t.getTokenType())) + .filter(t -> t.getTokenType().isOneTimeUse()) .filter(t -> !t.isRedeemed()) .map(AllocationToken::createVKey) .collect(toImmutableSet()); diff --git a/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java index bb87d89ea..e494f7f74 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java @@ -19,6 +19,7 @@ import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEF import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM; import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED; import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO; +import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA; import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE; import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE; import static google.registry.model.eppoutput.CheckData.DomainCheck.create; @@ -189,6 +190,40 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase token.build())) + .hasMessageThat() + .isEqualTo("REGISTER_BSA tokens must be tied to a domain"); + token.setDomainName("example.tld").build(); + } + @Test void testFail_bulkTokenNullEppActions() { AllocationToken.Builder builder = @@ -317,7 +330,7 @@ public class AllocationTokenTest extends EntityTestCase { IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build); assertThat(thrown) .hasMessageThat() - .isEqualTo("Domain name can only be specified for SINGLE_USE tokens"); + .isEqualTo("Domain name can only be specified for SINGLE_USE or REGISTER_BSA tokens"); } @Test @@ -347,7 +360,8 @@ public class AllocationTokenTest extends EntityTestCase { IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build); assertThat(thrown) .hasMessageThat() - .isEqualTo("Redemption history entry can only be specified for SINGLE_USE tokens"); + .isEqualTo( + "Redemption history entry can only be specified for SINGLE_USE or REGISTER_BSA tokens"); } @Test diff --git a/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java b/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java index 77ec65ab2..1918add41 100644 --- a/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java +++ b/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java @@ -380,7 +380,7 @@ class GenerateAllocationTokensCommandTest extends CommandTestCase