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 4257a2507..a77606880 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 @@ -86,6 +86,22 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { .putAll(VALID, ENDED, CANCELLED) .build(); + /** Any special behavior that should be used when registering domains using this token. */ + public enum RegistrationBehavior { + /** No special behavior */ + DEFAULT, + /** + * Bypasses the TLD state check, e.g. allowing registration during QUIET_PERIOD. + * + *

NB: while this means that, for instance, one can register non-trademarked domains in the + * sunrise period, any trademarked-domain registrations in the sunrise period must still include + * the proper signed marks. In other words, this only bypasses the TLD state check. + */ + BYPASS_TLD_STATE, + /** Bypasses most checks and creates the domain as an anchor tenant, with all that implies. */ + ANCHOR_TENANT + } + /** Single-use tokens are invalid after use. Infinite-use tokens, predictably, are not. */ public enum TokenType { SINGLE_USE, @@ -153,6 +169,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { @Column(name = "renewalPriceBehavior", nullable = false) RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + RegistrationBehavior registrationBehavior = RegistrationBehavior.DEFAULT; + // TODO: Remove onLoad once all allocation tokens are migrated to have a discountYears of 1. @OnLoad void onLoad() { @@ -225,6 +245,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { return renewalPriceBehavior; } + public RegistrationBehavior getRegistrationBehavior() { + return registrationBehavior; + } + @Override public VKey createVKey() { return VKey.create(AllocationToken.class, getToken(), Key.create(this)); @@ -261,6 +285,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { checkArgument( getInstance().discountFraction > 0 || getInstance().discountYears == 1, "Discount years can only be specified along with a discount fraction"); + if (getInstance().registrationBehavior.equals(RegistrationBehavior.ANCHOR_TENANT)) { + checkArgumentNotNull( + getInstance().domainName, "ANCHOR_TENANT tokens must be tied to a domain"); + } if (getInstance().domainName != null) { try { DomainFlowUtils.validateDomainName(getInstance().domainName); @@ -352,5 +380,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { getInstance().renewalPriceBehavior = renewalPriceBehavior; return this; } + + public Builder setRegistrationBehavior(RegistrationBehavior registrationBehavior) { + getInstance().registrationBehavior = registrationBehavior; + return this; + } } } diff --git a/core/src/main/java/google/registry/tools/GenerateAllocationTokensCommand.java b/core/src/main/java/google/registry/tools/GenerateAllocationTokensCommand.java index fee463c9e..2c2fe66e2 100644 --- a/core/src/main/java/google/registry/tools/GenerateAllocationTokensCommand.java +++ b/core/src/main/java/google/registry/tools/GenerateAllocationTokensCommand.java @@ -39,6 +39,7 @@ import com.google.common.collect.Streams; import com.google.common.io.Files; import google.registry.model.billing.BillingEvent.RenewalPriceBehavior; import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.RegistrationBehavior; import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.model.domain.token.AllocationToken.TokenType; import google.registry.persistence.VKey; @@ -152,6 +153,14 @@ class GenerateAllocationTokensCommand implements CommandWithRemoteApi { + " same as the domain's calculated create price.") private RenewalPriceBehavior renewalPriceBehavior = DEFAULT; + @Parameter( + names = {"--registration_behavior"}, + description = + "Any special registration behavior, including DEFAULT (no special behavior)," + + " BYPASS_TLD_STATE (allow registrations during e.g. QUIET_PERIOD), and" + + " ANCHOR_TENANT (used for anchor tenant registrations") + private RegistrationBehavior registrationBehavior = RegistrationBehavior.DEFAULT; + @Parameter( names = {"--dry_run"}, description = "Do not actually persist the tokens; defaults to false") @@ -196,6 +205,7 @@ class GenerateAllocationTokensCommand implements CommandWithRemoteApi { new AllocationToken.Builder() .setToken(t) .setRenewalPriceBehavior(renewalPriceBehavior) + .setRegistrationBehavior(registrationBehavior) .setTokenType(tokenType == null ? SINGLE_USE : tokenType) .setAllowedRegistrarIds( ImmutableSet.copyOf(nullToEmpty(allowedClientIds))) diff --git a/core/src/main/java/google/registry/tools/UpdateAllocationTokensCommand.java b/core/src/main/java/google/registry/tools/UpdateAllocationTokensCommand.java index 90519f435..cf2edd39f 100644 --- a/core/src/main/java/google/registry/tools/UpdateAllocationTokensCommand.java +++ b/core/src/main/java/google/registry/tools/UpdateAllocationTokensCommand.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import google.registry.model.billing.BillingEvent.RenewalPriceBehavior; import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.RegistrationBehavior; import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.tools.params.TransitionListParameter.TokenStatusTransitions; import java.util.List; @@ -100,6 +101,15 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens @Nullable private RenewalPriceBehavior renewalPriceBehavior; + @Parameter( + names = {"--registration_behavior"}, + description = + "Any special registration behavior, including DEFAULT (no special behavior)," + + " BYPASS_TLD_STATE (allow registrations during e.g. QUIET_PERIOD), and" + + " ANCHOR_TENANT (used for anchor tenant registrations") + @Nullable + private RegistrationBehavior registrationBehavior; + private static final int BATCH_SIZE = 20; private static final Joiner JOINER = Joiner.on(", "); @@ -156,8 +166,8 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens Optional.ofNullable(discountPremiums).ifPresent(builder::setDiscountPremiums); Optional.ofNullable(discountYears).ifPresent(builder::setDiscountYears); Optional.ofNullable(tokenStatusTransitions).ifPresent(builder::setTokenStatusTransitions); - Optional.ofNullable(renewalPriceBehavior) - .ifPresent(behavior -> builder.setRenewalPriceBehavior(renewalPriceBehavior)); + Optional.ofNullable(renewalPriceBehavior).ifPresent(builder::setRenewalPriceBehavior); + Optional.ofNullable(registrationBehavior).ifPresent(builder::setRegistrationBehavior); return builder.build(); } diff --git a/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java b/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java index a2655db32..e73c7583c 100644 --- a/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java +++ b/core/src/test/java/google/registry/model/domain/token/AllocationTokenTest.java @@ -36,6 +36,7 @@ import com.googlecode.objectify.Key; import google.registry.model.EntityTestCase; import google.registry.model.billing.BillingEvent.RenewalPriceBehavior; import google.registry.model.domain.DomainBase; +import google.registry.model.domain.token.AllocationToken.RegistrationBehavior; import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.model.domain.token.AllocationToken.TokenType; import google.registry.model.reporting.HistoryEntry; @@ -449,6 +450,34 @@ public class AllocationTokenTest extends EntityTestCase { .isEqualTo("Discount years can only be specified along with a discount fraction"); } + @Test + void testBuild_registrationBehaviors() { + createTld("tld"); + // BYPASS_TLD_STATE doesn't require a domain + AllocationToken token = + new AllocationToken.Builder() + .setToken("abc") + .setTokenType(SINGLE_USE) + .setRegistrationBehavior(RegistrationBehavior.BYPASS_TLD_STATE) + .build(); + // ANCHOR_TENANT does + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> + token + .asBuilder() + .setRegistrationBehavior(RegistrationBehavior.ANCHOR_TENANT) + .build())) + .hasMessageThat() + .isEqualTo("ANCHOR_TENANT tokens must be tied to a domain"); + token + .asBuilder() + .setRegistrationBehavior(RegistrationBehavior.ANCHOR_TENANT) + .setDomainName("example.tld") + .build(); + } + private void assertBadInitialTransition(TokenStatus status) { assertBadTransition( ImmutableSortedMap.naturalOrder() diff --git a/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java b/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java index eca0bbb6d..eaea3da08 100644 --- a/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java +++ b/core/src/test/java/google/registry/tools/GenerateAllocationTokensCommandTest.java @@ -14,6 +14,7 @@ package google.registry.tools; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM; import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED; @@ -260,6 +261,64 @@ class GenerateAllocationTokensCommandTest extends CommandTestCase runCommand("--tokens", "foobar", "--registration_behavior"))) + .hasMessageThat() + .contains("Expected a value after parameter --registration_behavior"); + assertThat( + assertThrows( + ParameterException.class, + () -> runCommand("--tokens", "foobar", "--registration_behavior", "bad"))) + .hasMessageThat() + .contains("Invalid value for --registration_behavior"); + assertThat( + assertThrows( + ParameterException.class, + () -> runCommand("--tokens", "foobar", "--registration_behavior", ""))) + .hasMessageThat() + .contains("Invalid value for --registration_behavior"); + } + @Test void testSuccess_specifyManyTokens() throws Exception { command.stringGenerator = diff --git a/core/src/test/java/google/registry/tools/UpdateAllocationTokensCommandTest.java b/core/src/test/java/google/registry/tools/UpdateAllocationTokensCommandTest.java index 0812e8988..57454395d 100644 --- a/core/src/test/java/google/registry/tools/UpdateAllocationTokensCommandTest.java +++ b/core/src/test/java/google/registry/tools/UpdateAllocationTokensCommandTest.java @@ -24,6 +24,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID; 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.testing.DatabaseHelper.loadByEntity; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static org.joda.time.DateTimeZone.UTC; @@ -33,6 +34,7 @@ import com.beust.jcommander.ParameterException; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import google.registry.model.domain.token.AllocationToken; +import google.registry.model.domain.token.AllocationToken.RegistrationBehavior; import google.registry.model.domain.token.AllocationToken.TokenStatus; import org.joda.time.DateTime; import org.junit.jupiter.api.Test; @@ -190,6 +192,67 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase + runCommandForced( + "--tokens", "token", "--registration_behavior", "ANCHOR_TENANT"))) + .hasMessageThat() + .isEqualTo("ANCHOR_TENANT tokens must be tied to a domain"); + } + + @Test + void testFailure_registrationBehavior_invalid() throws Exception { + assertThat( + assertThrows( + ParameterException.class, + () -> runCommand("--tokens", "foobar", "--registration_behavior"))) + .hasMessageThat() + .contains("Expected a value after parameter --registration_behavior"); + assertThat( + assertThrows( + ParameterException.class, + () -> runCommand("--tokens", "foobar", "--registration_behavior", "bad"))) + .hasMessageThat() + .contains("Invalid value for --registration_behavior"); + assertThat( + assertThrows( + ParameterException.class, + () -> runCommand("--tokens", "foobar", "--registration_behavior", ""))) + .hasMessageThat() + .contains("Invalid value for --registration_behavior"); + } + @Test void testUpdateStatusTransitions() throws Exception { DateTime now = DateTime.now(UTC); diff --git a/core/src/test/resources/google/registry/model/schema.txt b/core/src/test/resources/google/registry/model/schema.txt index cf95b593f..34d698a67 100644 --- a/core/src/test/resources/google/registry/model/schema.txt +++ b/core/src/test/resources/google/registry/model/schema.txt @@ -338,6 +338,7 @@ class google.registry.model.domain.token.AllocationToken { google.registry.model.UpdateAutoTimestamp updateTimestamp; google.registry.model.billing.BillingEvent$RenewalPriceBehavior renewalPriceBehavior; google.registry.model.common.TimedTransitionProperty tokenStatusTransitions; + google.registry.model.domain.token.AllocationToken$RegistrationBehavior registrationBehavior; google.registry.model.domain.token.AllocationToken$TokenType tokenType; google.registry.persistence.DomainHistoryVKey redemptionHistoryEntry; int discountYears; @@ -345,6 +346,11 @@ class google.registry.model.domain.token.AllocationToken { java.util.Set allowedClientIds; java.util.Set allowedTlds; } +enum google.registry.model.domain.token.AllocationToken$RegistrationBehavior { + ANCHOR_TENANT; + BYPASS_TLD_STATE; + DEFAULT; +} enum google.registry.model.domain.token.AllocationToken$TokenStatus { CANCELLED; ENDED; diff --git a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html index 2e483e4e7..15e681e15 100644 --- a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html @@ -261,7 +261,7 @@ td.section { generated on - 2022-07-15 16:41:11.765867 + 2022-07-15 18:56:30.776055 last flyway file @@ -284,7 +284,7 @@ td.section { generated on - 2022-07-15 16:41:11.765867 + 2022-07-15 18:56:30.776055 diff --git a/db/src/main/resources/sql/er_diagram/full_er_diagram.html b/db/src/main/resources/sql/er_diagram/full_er_diagram.html index 169372b50..5e3d7265b 100644 --- a/db/src/main/resources/sql/er_diagram/full_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/full_er_diagram.html @@ -261,7 +261,7 @@ td.section { generated on - 2022-07-15 16:41:09.696997 + 2022-07-15 18:56:28.677292 last flyway file @@ -284,7 +284,7 @@ td.section { generated on - 2022-07-15 16:41:09.696997 + 2022-07-15 18:56:28.677292 diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index 2e1f5d495..402365a75 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -24,6 +24,7 @@ domain_name text, redemption_domain_history_id int8, redemption_domain_repo_id text, + registration_behavior text not null, renewal_price_behavior text not null, token_status_transitions hstore, token_type text,