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,