mirror of
https://github.com/google/nomulus
synced 2026-07-18 14:02:35 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0489e5f9 | |||
| d29a98bdd3 | |||
| d6dd95b052 | |||
| 6b74924067 | |||
| 71e9bc95a7 | |||
| bf54a0d0c4 | |||
| 2a04b9be9b | |||
| 675354ae65 | |||
| 21421726e0 | |||
| 192a7e6c4e | |||
| 542084eb70 | |||
| 54e605fa27 |
@@ -72,10 +72,19 @@ This document outlines foundational mandates, architectural patterns, and projec
|
||||
- **No CodeSearch:** This project is hosted on GitHub, not Google3. Do NOT use `mcp_Coding_search_for_files_codesearch` or other internal Google3 search tools.
|
||||
- **Local Grep:** Use local shell commands like `git grep` or `grep` via `run_shell_command` to search the codebase.
|
||||
|
||||
### 8. Git Operation Boundaries & Remote Push Prohibition
|
||||
- **Zero-Bundling Rule for Remote Transfers:** NEVER bundle remote transfer commands (`git push`, `repo upload`, `g4 upload`, `g4 mail`, `hg push`, `jj git push`) inside compound bash pipelines (`&&`, `||`, `;`) with local staging or commit commands (e.g., `git commit --amend && git push ...`). Every remote transfer command MUST be executed as a standalone, single-command `run_command` invocation.
|
||||
- **Mandatory Two-Step Handover Protocol:** When working on pull requests, changelists, or branches, strictly halt right at the local repository boundary (`git commit` / `g4 change`). Report the local commit SHA, `git status`, and `git diff` verification to the user, stop calling tools, and explicitly prompt: *"Local commit `[SHA]` is verified. Do you authorize running `git push origin [branch]` (or `--force-with-lease` when force-pushing an amended branch) to update the remote repository?"*
|
||||
- **Absolute Prohibition on Unsolicited Pushes:** NEVER propose or run any remote synchronization or push command (`git push`, `repo upload`, `g4 upload`) on your own volition without explicit, unambiguous textual authorization in the immediate turn from the user.
|
||||
- **Mandatory GitHub PR Description & Reviewable Synchronization:** Whenever pushing an amended commit to a branch associated with an active pull request, the agent MUST immediately synchronize the PR description on GitHub (`gh pr edit`). To do this safely:
|
||||
1. Check `gh pr view --json body` first to inspect existing content.
|
||||
2. Explicitly preserve any existing Reviewable bot links (`This change is https://reviewable.io/reviews/...`) when updating the body rather than blindly overwriting the entire description.
|
||||
|
||||
## Performance and Efficiency
|
||||
- **Turn Minimization:** Aim for "perfect" code in the first iteration. Iterative fixes for checkstyle or compilation errors consume significant context and time.
|
||||
- **Context Management:** Use sub-agents for batch refactoring or high-volume output tasks to keep the main session history lean and efficient.
|
||||
- **Code Formatting:** Do not write custom Python scripts or manual regex replacements to fix code formatting issues (e.g., unused imports, import ordering, line length). Instead, use the project's built-in formatting tools: run `./gradlew spotlessApply` to fix unused/unordered imports and `./gradlew javaIncrementalFormatApply` (or `google-java-format --replace <files>`) to automatically fix Java formatting and indentation errors.
|
||||
- **Programmatic Measurement for Markdown:** When writing Markdown files (`.md`)—and especially when formatting or line-wrapping them—you **MUST** explicitly use programmatic functions (such as `awk '{print length($0)}'` or Python `len(line)`) to measure the exact string length of every line from column 1, rather than making assumptions based on what appears in your context window.
|
||||
|
||||
## General Code Review Lessons & Avoidable Mistakes
|
||||
Based on historical PR reviews, avoid the following common mistakes:
|
||||
|
||||
@@ -288,8 +288,9 @@ export class BackendService {
|
||||
verifyRegistryLockRequest(
|
||||
lockVerificationCode: string
|
||||
): Observable<RegistryLockVerificationResponse> {
|
||||
return this.http.get<RegistryLockVerificationResponse>(
|
||||
`/console-api/registry-lock-verify?lockVerificationCode=${lockVerificationCode}`
|
||||
return this.http.post<RegistryLockVerificationResponse>(
|
||||
`/console-api/registry-lock-verify?lockVerificationCode=${lockVerificationCode}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.config;
|
||||
|
||||
import static com.google.common.base.Suppliers.memoize;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
|
||||
import static google.registry.config.ConfigUtils.makeUrl;
|
||||
import static google.registry.util.DateTimeUtils.START_INSTANT;
|
||||
@@ -49,6 +50,7 @@ import jakarta.mail.internet.AddressException;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.DayOfWeek;
|
||||
@@ -59,6 +61,7 @@ import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
|
||||
/**
|
||||
* Central clearing-house for all configuration.
|
||||
@@ -1110,6 +1113,40 @@ public final class RegistryConfig {
|
||||
return config.registryPolicy.registryAdminClientId;
|
||||
}
|
||||
|
||||
/** Returns the total length of the Expiry Access Period. */
|
||||
@Provides
|
||||
@Config("domainExpiryAccessPeriodTotalLength")
|
||||
public static Duration provideDomainExpiryAccessPeriodTotalLength(
|
||||
RegistryConfigSettings config) {
|
||||
return Duration.ofSeconds(config.registryPolicy.domainExpiryAccessPeriod.totalLengthSeconds);
|
||||
}
|
||||
|
||||
/** Returns the length of each tier of the Expiry Access Period. */
|
||||
@Provides
|
||||
@Config("domainExpiryAccessPeriodTierLength")
|
||||
public static Duration provideDomainExpiryAccessPeriodTierLength(
|
||||
RegistryConfigSettings config) {
|
||||
return Duration.ofSeconds(config.registryPolicy.domainExpiryAccessPeriod.tierLengthSeconds);
|
||||
}
|
||||
|
||||
/** Returns a map of the fee for the first tier of the Expiry Access Period, by currency. */
|
||||
@Provides
|
||||
@Config("domainExpiryAccessPeriodInitialFee")
|
||||
public static ImmutableMap<CurrencyUnit, BigDecimal> provideDomainExpiryAccessPeriodInitialFee(
|
||||
RegistryConfigSettings config) {
|
||||
return config.registryPolicy.domainExpiryAccessPeriod.initialFee.entrySet().stream()
|
||||
.collect(toImmutableMap(entry -> CurrencyUnit.of(entry.getKey()), Entry::getValue));
|
||||
}
|
||||
|
||||
/** Returns a map of the fee for the last tier of the Expiry Access Period, by currency. */
|
||||
@Provides
|
||||
@Config("domainExpiryAccessPeriodFinalFee")
|
||||
public static ImmutableMap<CurrencyUnit, BigDecimal> provideDomainExpiryAccessPeriodFinalFee(
|
||||
RegistryConfigSettings config) {
|
||||
return config.registryPolicy.domainExpiryAccessPeriod.finalFee.entrySet().stream()
|
||||
.collect(toImmutableMap(entry -> CurrencyUnit.of(entry.getKey()), Entry::getValue));
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static RegistryConfigSettings provideRegistryConfigSettings() {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package google.registry.config;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -94,6 +95,7 @@ public class RegistryConfigSettings {
|
||||
public String tmchCrlUrl;
|
||||
public String tmchMarksDbUrl;
|
||||
public String registryAdminClientId;
|
||||
public DomainExpiryAccessPeriod domainExpiryAccessPeriod;
|
||||
public String premiumTermsExportDisclaimer;
|
||||
public String reservedTermsExportDisclaimer;
|
||||
public String rdapTos;
|
||||
@@ -106,6 +108,13 @@ public class RegistryConfigSettings {
|
||||
public Set<String> noPollMessageOnDeletionRegistrarIds;
|
||||
}
|
||||
|
||||
public static class DomainExpiryAccessPeriod {
|
||||
public long totalLengthSeconds;
|
||||
public long tierLengthSeconds;
|
||||
public Map<String, BigDecimal> initialFee;
|
||||
public Map<String, BigDecimal> finalFee;
|
||||
}
|
||||
|
||||
/** Configuration for Hibernate. */
|
||||
public static class Hibernate {
|
||||
public boolean allowNestedTransactions;
|
||||
|
||||
@@ -88,6 +88,28 @@ registryPolicy:
|
||||
# registrar
|
||||
registryAdminClientId: TheRegistrar
|
||||
|
||||
# Configuration for the Expiry Access Period (XAP) that occurs immediately
|
||||
# following completion of a domain's deletion. This needs to be enabled on a
|
||||
# per-TLD basis as well.
|
||||
domainExpiryAccessPeriod:
|
||||
# The length of the total expiry access period; defaults to 10 days.
|
||||
totalLengthSeconds: 864000
|
||||
# The length of each tier during the expiry access period; defaults to 1
|
||||
# hour. The XAP fee remains constant for the duration of each tier. The
|
||||
# total length must be evenly divisible by the tier length, i.e. there
|
||||
# should be a whole number of tiers all the same length.
|
||||
tierLengthSeconds: 3600
|
||||
# The initial fee for the first tier when the expiry access period starts,
|
||||
# specified for each currency this registry platform uses.
|
||||
initialFee:
|
||||
USD: 100000
|
||||
JPY: 10000000
|
||||
# The final fee for the last tier when the expiry access period ends,
|
||||
# specified for each currency this registry platform uses.
|
||||
finalFee:
|
||||
USD: 10
|
||||
JPY: 1000
|
||||
|
||||
# Disclaimer at the top of the exported premium terms list.
|
||||
premiumTermsExportDisclaimer: |
|
||||
This list contains domains for the TLD offered at a premium price. This
|
||||
|
||||
+10
@@ -323,4 +323,14 @@
|
||||
<schedule>*/5 * * * *</schedule>
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/syncRemoteCache]]></url>
|
||||
<name>syncRemoteCache</name>
|
||||
<description>
|
||||
Syncs remote (Valkey/Redis) EPP resource caches with changes made recently.
|
||||
</description>
|
||||
<!-- Runs every 5 minutes. RDAP needs to be updated within 60 minutes of changes. -->
|
||||
<schedule>*/5 * * * *</schedule>
|
||||
</task>
|
||||
|
||||
</entries>
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.toLogSafeLabel;
|
||||
import static java.util.Collections.EMPTY_LIST;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -90,9 +90,7 @@ public class FlowReporter {
|
||||
*/
|
||||
private static Optional<String> extractTld(String domainName) {
|
||||
int index = domainName.indexOf('.');
|
||||
return index == -1
|
||||
? Optional.empty()
|
||||
: Optional.of(Ascii.toLowerCase(domainName.substring(index + 1)));
|
||||
return index == -1 ? Optional.empty() : toLogSafeLabel(domainName.substring(index + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,7 @@ import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -100,8 +101,30 @@ public final class ResourceFlowUtils {
|
||||
|
||||
public static <R extends EppResource> void verifyResourceDoesNotExist(
|
||||
Class<R> clazz, String targetId, Instant now, String registrarId) throws EppException {
|
||||
Optional<R> resource = ForeignKeyUtils.loadResource(clazz, targetId, now);
|
||||
verifyResourceDoesNotExist(clazz, targetId, now, Duration.ZERO, registrarId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a resource with the specified type and id did not exist at the specified time.
|
||||
*
|
||||
* <p>This will throw an exception if the resource exists with a deletionTime greater than the
|
||||
* specified time (i.e. a registrar is attempting to create a duplicate domain, host, or contact).
|
||||
* If the resource had existed within the specified lookback window, but is not active now, i.e.
|
||||
* it is recently deleted, then don't throw an exception, but do return the recently deleted
|
||||
* resource for further processing (this is used by the Expiry Access Period).
|
||||
*
|
||||
* <p>If there is no need to specially handle the situation of recently deleted resources, then
|
||||
* simply pass a lookback duration of {@link Duration#ZERO}.
|
||||
*/
|
||||
public static <R extends EppResource> Optional<R> verifyResourceDoesNotExist(
|
||||
Class<R> clazz, String targetId, Instant now, Duration lookback, String registrarId)
|
||||
throws EppException {
|
||||
Instant startOfWindow = now.minus(lookback);
|
||||
Optional<R> resource = ForeignKeyUtils.loadResource(clazz, targetId, startOfWindow);
|
||||
if (resource.isPresent()) {
|
||||
if (resource.get().getDeletionTime().isBefore(now)) {
|
||||
return resource;
|
||||
}
|
||||
// These are similar exceptions, but we can track them internally as log-based metrics.
|
||||
if (Objects.equals(registrarId, resource.get().getPersistedCurrentSponsorRegistrarId())) {
|
||||
throw new ResourceAlreadyExistsForThisClientException(targetId);
|
||||
@@ -109,6 +132,7 @@ public final class ResourceFlowUtils {
|
||||
throw new ResourceCreateContentionException(targetId);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** Check that the given AuthInfo is present for a resource being transferred. */
|
||||
|
||||
@@ -25,6 +25,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.isDomainEligibleForXap;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isRegisterBsaCreate;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isReserved;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isValidReservedCreate;
|
||||
@@ -75,6 +76,7 @@ import google.registry.model.eppoutput.CheckData.DomainCheck;
|
||||
import google.registry.model.eppoutput.CheckData.DomainCheckData;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldState;
|
||||
@@ -82,6 +84,7 @@ import google.registry.model.tld.label.ReservationType;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
@@ -138,6 +141,10 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
@Config("maxChecks")
|
||||
int maxChecks;
|
||||
|
||||
@Inject
|
||||
@Config("domainExpiryAccessPeriodTotalLength")
|
||||
Duration domainExpiryAccessPeriodTotalLength;
|
||||
|
||||
@Inject @Superuser boolean isSuperuser;
|
||||
@Inject Clock clock;
|
||||
@Inject EppResponse.Builder responseBuilder;
|
||||
@@ -249,7 +256,14 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
return Optional.of(e.getMessage());
|
||||
}
|
||||
return getMessageForCheckWithToken(
|
||||
idn, existingDomains, bsaBlockedDomainNames, tldStates, token);
|
||||
idn, existingDomains, bsaBlockedDomainNames, tldStates, token, now);
|
||||
}
|
||||
|
||||
private static Optional<Domain> loadDomainIfInXap(
|
||||
String domainName, Instant now, Duration domainExpiryAccessPeriodTotalLength) {
|
||||
return ForeignKeyUtils.loadResource(
|
||||
Domain.class, domainName, now.minus(domainExpiryAccessPeriodTotalLength))
|
||||
.filter(domain -> isDomainEligibleForXap(domain, Tld.get(domain.getTld()), now));
|
||||
}
|
||||
|
||||
private Optional<String> getMessageForCheckWithToken(
|
||||
@@ -257,10 +271,18 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
ImmutableMap<String, VKey<Domain>> existingDomains,
|
||||
ImmutableSet<InternetDomainName> bsaBlockedDomains,
|
||||
ImmutableMap<String, TldState> tldStates,
|
||||
Optional<AllocationToken> allocationToken) {
|
||||
Optional<AllocationToken> allocationToken,
|
||||
Instant now) {
|
||||
if (existingDomains.containsKey(domainName.toString())) {
|
||||
return Optional.of("In use");
|
||||
}
|
||||
Tld tld = Tld.get(domainName.parent().toString());
|
||||
if (tld.getExpiryAccessPeriodModeAt(now) == Tld.ExpiryAccessPeriodMode.ENABLED
|
||||
&& !Registrar.loadByRegistrarIdCached(registrarId).get().getExpiryAccessPeriodEnabled()
|
||||
&& loadDomainIfInXap(domainName.toString(), now, domainExpiryAccessPeriodTotalLength)
|
||||
.isPresent()) {
|
||||
return Optional.of("Reserved");
|
||||
}
|
||||
TldState tldState = tldStates.get(domainName.parent().toString());
|
||||
if (isReserved(domainName, START_DATE_SUNRISE.equals(tldState))) {
|
||||
if (!isValidReservedCreate(domainName, allocationToken)
|
||||
@@ -339,16 +361,27 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
.build());
|
||||
continue;
|
||||
}
|
||||
Optional<Domain> domainForFee = domain;
|
||||
boolean isAvailable = availableDomains.contains(domainName);
|
||||
if (isAvailable
|
||||
&& feeCheckItem.getCommandName().equals(FeeQueryCommandExtensionItem.CommandName.CREATE)
|
||||
&& tld.getExpiryAccessPeriodModeAt(now) == Tld.ExpiryAccessPeriodMode.ENABLED) {
|
||||
Optional<Domain> recentlyDeletedDomain =
|
||||
loadDomainIfInXap(domainName, now, domainExpiryAccessPeriodTotalLength);
|
||||
if (recentlyDeletedDomain.isPresent()) {
|
||||
domainForFee = recentlyDeletedDomain;
|
||||
}
|
||||
}
|
||||
handleFeeRequest(
|
||||
feeCheckItem,
|
||||
builder,
|
||||
domainNames.get(domainName),
|
||||
domain,
|
||||
domainForFee,
|
||||
feeCheck.getCurrency(),
|
||||
now,
|
||||
pricingLogic,
|
||||
token,
|
||||
availableDomains.contains(domainName),
|
||||
isAvailable,
|
||||
recurrences.getOrDefault(domainName, null));
|
||||
// In the case of a registrar that is running a tiered pricing promotion, we issue two
|
||||
// responses for the CREATE fee check command: one (the default response) with the
|
||||
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
|
||||
import static google.registry.flows.FlowUtils.persistEntityChanges;
|
||||
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
|
||||
import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.COLLISION_MESSAGE;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.checkHasBillingAccount;
|
||||
@@ -25,6 +26,7 @@ import static google.registry.flows.domain.DomainFlowUtils.cloneAndLinkReference
|
||||
import static google.registry.flows.domain.DomainFlowUtils.createFeeCreateResponse;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.getReservationTypes;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isAnchorTenant;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isDomainEligibleForXap;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isReserved;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isValidReservedCreate;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateCreateCommandContactsAndNameservers;
|
||||
@@ -58,6 +60,7 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.CommandUseErrorException;
|
||||
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
|
||||
@@ -71,6 +74,7 @@ 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.DomainReservedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
|
||||
import google.registry.flows.exceptions.ContactsProhibitedException;
|
||||
@@ -107,6 +111,7 @@ import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.poll.PollMessage.Autorenew;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.reporting.DomainTransactionRecord;
|
||||
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
@@ -123,6 +128,7 @@ import jakarta.inject.Inject;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.joda.money.Money;
|
||||
|
||||
/**
|
||||
* An EPP flow that creates a new domain resource.
|
||||
@@ -219,6 +225,10 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
@Inject DomainPricingLogic pricingLogic;
|
||||
@Inject DomainDeletionTimeCache domainDeletionTimeCache;
|
||||
|
||||
@Inject
|
||||
@Config("domainExpiryAccessPeriodTotalLength")
|
||||
Duration domainExpiryAccessPeriodTotalLength;
|
||||
|
||||
@Inject
|
||||
DomainCreateFlow() {}
|
||||
|
||||
@@ -245,6 +255,13 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
InternetDomainName domainName = validateDomainName(command.getDomainName());
|
||||
String domainLabel = domainName.parts().getFirst();
|
||||
Tld tld = Tld.get(domainName.parent().toString());
|
||||
Duration lookback =
|
||||
tld.getExpiryAccessPeriodModeAt(now) == Tld.ExpiryAccessPeriodMode.ENABLED
|
||||
? domainExpiryAccessPeriodTotalLength
|
||||
: Duration.ZERO;
|
||||
Optional<Domain> recentlyDeletedDomain =
|
||||
verifyResourceDoesNotExist(Domain.class, targetId, now, lookback, registrarId)
|
||||
.filter(domain -> isDomainEligibleForXap(domain, tld, now));
|
||||
validateCreateCommandContactsAndNameservers(command, tld, domainName);
|
||||
TldState tldState = tld.getTldState(now);
|
||||
Optional<LaunchCreateExtension> launchCreate =
|
||||
@@ -280,6 +297,10 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
if (!isSuperuser) {
|
||||
checkAllowedAccessToTld(registrarId, tld.getTldStr());
|
||||
checkHasBillingAccount(registrarId, tld.getTldStr());
|
||||
if (recentlyDeletedDomain.isPresent()
|
||||
&& !Registrar.loadByRegistrarIdCached(registrarId).get().getExpiryAccessPeriodEnabled()) {
|
||||
throw new DomainReservedException(domainName.toString());
|
||||
}
|
||||
boolean isValidReservedCreate = isValidReservedCreate(domainName, allocationToken);
|
||||
ClaimsList claimsList = ClaimsListDao.get();
|
||||
verifyIsGaOrSpecialCase(
|
||||
@@ -327,7 +348,14 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
eppInput.getSingleExtension(FeeCreateCommandExtension.class);
|
||||
FeesAndCredits feesAndCredits =
|
||||
pricingLogic.getCreatePrice(
|
||||
tld, targetId, now, years, isAnchorTenant, isSunriseCreate, allocationToken);
|
||||
tld,
|
||||
targetId,
|
||||
now,
|
||||
recentlyDeletedDomain,
|
||||
years,
|
||||
isAnchorTenant,
|
||||
isSunriseCreate,
|
||||
allocationToken);
|
||||
validateFeeChallenge(feeCreate, feesAndCredits, defaultTokenUsed);
|
||||
Optional<SecDnsCreateExtension> secDnsCreate =
|
||||
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));
|
||||
@@ -358,7 +386,15 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
entitiesToInsert.add(createBillingEvent, autorenewBillingEvent, autorenewPollMessage);
|
||||
// Bill for EAP cost, if any.
|
||||
if (!feesAndCredits.getEapCost().isZero()) {
|
||||
entitiesToInsert.add(createEapBillingEvent(feesAndCredits, createBillingEvent));
|
||||
entitiesToInsert.add(
|
||||
createAdditionalBillingEvent(
|
||||
Reason.FEE_EARLY_ACCESS, feesAndCredits.getEapCost(), createBillingEvent));
|
||||
}
|
||||
// Bill for XAP cost, if any.
|
||||
if (!feesAndCredits.getXapCost().isZero()) {
|
||||
entitiesToInsert.add(
|
||||
createAdditionalBillingEvent(
|
||||
Reason.FEE_EXPIRY_ACCESS, feesAndCredits.getXapCost(), createBillingEvent));
|
||||
}
|
||||
|
||||
ImmutableSet<ReservationType> reservationTypes = getReservationTypes(domainName);
|
||||
@@ -434,7 +470,14 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
FeesAndCredits responseFeesAndCredits =
|
||||
shouldShowDefaultPrice
|
||||
? pricingLogic.getCreatePrice(
|
||||
tld, targetId, now, years, isAnchorTenant, isSunriseCreate, Optional.empty())
|
||||
tld,
|
||||
targetId,
|
||||
now,
|
||||
recentlyDeletedDomain,
|
||||
years,
|
||||
isAnchorTenant,
|
||||
isSunriseCreate,
|
||||
Optional.empty())
|
||||
: feesAndCredits;
|
||||
|
||||
BeforeResponseReturnData responseData =
|
||||
@@ -656,14 +699,14 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
}
|
||||
}
|
||||
|
||||
private static BillingEvent createEapBillingEvent(
|
||||
FeesAndCredits feesAndCredits, BillingEvent createBillingEvent) {
|
||||
private static BillingEvent createAdditionalBillingEvent(
|
||||
Reason reason, Money cost, BillingEvent createBillingEvent) {
|
||||
return new BillingEvent.Builder()
|
||||
.setReason(Reason.FEE_EARLY_ACCESS)
|
||||
.setReason(reason)
|
||||
.setTargetId(createBillingEvent.getTargetId())
|
||||
.setRegistrarId(createBillingEvent.getRegistrarId())
|
||||
.setPeriodYears(1)
|
||||
.setCost(feesAndCredits.getEapCost())
|
||||
.setCost(cost)
|
||||
.setEventTime(createBillingEvent.getEventTime())
|
||||
.setBillingTime(createBillingEvent.getBillingTime())
|
||||
.setFlags(createBillingEvent.getFlags())
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package google.registry.flows.domain;
|
||||
|
||||
import static com.google.common.base.Ascii.toLowerCase;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Strings.emptyToNull;
|
||||
@@ -175,6 +176,27 @@ public class DomainFlowUtils {
|
||||
/** Maximum number of characters in a domain label, from RFC 2181. */
|
||||
private static final int MAX_LABEL_SIZE = 63;
|
||||
|
||||
/// Given a tld name or domain label, returns a label that can be safely logged, or
|
||||
/// `Optional.empty()` if `label` is null or blank.
|
||||
///
|
||||
/// If `label` contains disallowed characters, they are replaced with '-'. If the resulting
|
||||
/// string has a valid length, it is returned as is; if not, the first {@link #MAX_LABEL_SIZE}
|
||||
/// chars plus a suffix of `...` is returned.
|
||||
///
|
||||
/// This method is mainly for use by `FlowReporter#recordToLogs()` to ensure that the logs
|
||||
/// can be safely and correctly queried by SQL queries. See b/534931586 for more information.
|
||||
public static Optional<String> toLogSafeLabel(String label) {
|
||||
if (label == null || label.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
var newLabel = ALLOWED_CHARS.negate().replaceFrom(toLowerCase(label), '-');
|
||||
if (newLabel.length() <= MAX_LABEL_SIZE) {
|
||||
return Optional.of(newLabel);
|
||||
} else {
|
||||
return Optional.of(newLabel.substring(0, MAX_LABEL_SIZE) + "...");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns parsed version of {@code name} if domain name label follows our naming rules and is
|
||||
* under one of the given allowed TLDs.
|
||||
@@ -642,6 +664,7 @@ public class DomainFlowUtils {
|
||||
tld,
|
||||
domainNameString,
|
||||
now,
|
||||
domain,
|
||||
years,
|
||||
isAnchorTenant(domainName, allocationToken, Optional.empty()),
|
||||
isSunrise,
|
||||
@@ -765,6 +788,9 @@ public class DomainFlowUtils {
|
||||
if (!feesAndCredits.getEapCost().isZero()) {
|
||||
throw new FeesRequiredDuringEarlyAccessProgramException(feesAndCredits.getEapCost());
|
||||
}
|
||||
if (!feesAndCredits.getXapCost().isZero()) {
|
||||
throw new FeesRequiredDuringExpiryAccessPeriodException(feesAndCredits.getXapCost());
|
||||
}
|
||||
if (feesAndCredits.getTotalCost().isZero() || !feesAndCredits.isFeeExtensionRequired()) {
|
||||
return;
|
||||
}
|
||||
@@ -1168,6 +1194,32 @@ public class DomainFlowUtils {
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the domain was deleted during its Add Grace Period (AGP).
|
||||
*
|
||||
* <p>Per policy, domains deleted during AGP are not subject to Expiry Access Period (XAP) fees or
|
||||
* reservation restrictions upon subsequent re-registration.
|
||||
*/
|
||||
public static boolean wasDeletedDuringAddGracePeriod(Domain domain, Tld tld) {
|
||||
if (domain.getCreationTime() == null || domain.getDeletionTime() == null) {
|
||||
return false;
|
||||
}
|
||||
return !domain
|
||||
.getDeletionTime()
|
||||
.isAfter(domain.getCreationTime().plus(tld.getAddGracePeriodLength()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the domain was deleted before {@code now} and is eligible for Expiry Access
|
||||
* Period (XAP) evaluation.
|
||||
*/
|
||||
public static boolean isDomainEligibleForXap(Domain domain, Tld tld, Instant now) {
|
||||
if (domain.getDeletionTime() == null || !domain.getDeletionTime().isBefore(now)) {
|
||||
return false;
|
||||
}
|
||||
return !wasDeletedDuringAddGracePeriod(domain, tld);
|
||||
}
|
||||
|
||||
/** Resource linked to this domain does not exist. */
|
||||
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
|
||||
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {
|
||||
@@ -1355,6 +1407,18 @@ public class DomainFlowUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Fees must be explicitly acknowledged when creating domains during the Expiry Access Period. */
|
||||
static class FeesRequiredDuringExpiryAccessPeriodException
|
||||
extends RequiredParameterMissingException {
|
||||
|
||||
public FeesRequiredDuringExpiryAccessPeriodException(Money expectedFee) {
|
||||
super(
|
||||
"Fees must be explicitly acknowledged when creating domains "
|
||||
+ "during the Expiry Access Period. The XAP fee is: "
|
||||
+ expectedFee);
|
||||
}
|
||||
}
|
||||
|
||||
/** The 'grace-period', 'applied' and 'refundable' fields are disallowed by server policy. */
|
||||
static class UnsupportedFeeAttributeException extends UnimplementedOptionException {
|
||||
UnsupportedFeeAttributeException() {
|
||||
|
||||
@@ -15,13 +15,18 @@
|
||||
package google.registry.flows.domain;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isDomainEligibleForXap;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
|
||||
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.discountTokenInvalidForPremiumName;
|
||||
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.custom.DomainPricingCustomLogic;
|
||||
import google.registry.flows.custom.DomainPricingCustomLogic.CreatePriceParameters;
|
||||
@@ -31,6 +36,7 @@ import google.registry.flows.custom.DomainPricingCustomLogic.TransferPriceParame
|
||||
import google.registry.flows.custom.DomainPricingCustomLogic.UpdatePriceParameters;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.fee.BaseFee;
|
||||
import google.registry.model.domain.fee.BaseFee.FeeType;
|
||||
import google.registry.model.domain.fee.Fee;
|
||||
@@ -40,7 +46,9 @@ import google.registry.model.domain.token.AllocationToken.TokenBehavior;
|
||||
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
|
||||
import google.registry.model.tld.Tld;
|
||||
import jakarta.inject.Inject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -54,11 +62,28 @@ import org.joda.money.Money;
|
||||
*/
|
||||
public final class DomainPricingLogic {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final DomainPricingCustomLogic customLogic;
|
||||
private final Duration expiryAccessPeriodTotalLength;
|
||||
private final Duration expiryAccessPeriodTierLength;
|
||||
private final ImmutableMap<CurrencyUnit, BigDecimal> expiryAccessPeriodInitialFee;
|
||||
private final ImmutableMap<CurrencyUnit, BigDecimal> expiryAccessPeriodFinalFee;
|
||||
|
||||
@Inject
|
||||
public DomainPricingLogic(DomainPricingCustomLogic customLogic) {
|
||||
public DomainPricingLogic(
|
||||
DomainPricingCustomLogic customLogic,
|
||||
@Config("domainExpiryAccessPeriodTotalLength") Duration expiryAccessPeriodTotalLength,
|
||||
@Config("domainExpiryAccessPeriodTierLength") Duration expiryAccessPeriodTierLength,
|
||||
@Config("domainExpiryAccessPeriodInitialFee")
|
||||
ImmutableMap<CurrencyUnit, BigDecimal> expiryAccessPeriodInitialFee,
|
||||
@Config("domainExpiryAccessPeriodFinalFee")
|
||||
ImmutableMap<CurrencyUnit, BigDecimal> expiryAccessPeriodFinalFee) {
|
||||
this.customLogic = customLogic;
|
||||
this.expiryAccessPeriodTotalLength = expiryAccessPeriodTotalLength;
|
||||
this.expiryAccessPeriodTierLength = expiryAccessPeriodTierLength;
|
||||
this.expiryAccessPeriodInitialFee = expiryAccessPeriodInitialFee;
|
||||
this.expiryAccessPeriodFinalFee = expiryAccessPeriodFinalFee;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,35 +96,23 @@ public final class DomainPricingLogic {
|
||||
Tld tld,
|
||||
String domainName,
|
||||
Instant dateTime,
|
||||
Optional<Domain> recentlyDeletedDomain,
|
||||
int years,
|
||||
boolean isAnchorTenant,
|
||||
boolean isSunriseCreate,
|
||||
Optional<AllocationToken> allocationToken)
|
||||
throws EppException {
|
||||
CurrencyUnit currency = tld.getCurrency();
|
||||
|
||||
BaseFee createFee;
|
||||
// Domain create cost is always zero for anchor tenants
|
||||
if (isAnchorTenant) {
|
||||
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
|
||||
} else {
|
||||
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
|
||||
if (allocationToken.isPresent()) {
|
||||
// Handle any special NONPREMIUM / SPECIFIED cases configured in the token
|
||||
domainPrices =
|
||||
applyTokenToDomainPrices(domainPrices, tld, dateTime, years, allocationToken.get());
|
||||
}
|
||||
Money domainCreateCost =
|
||||
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken, tld);
|
||||
// Apply a sunrise discount if configured and applicable
|
||||
if (isSunriseCreate) {
|
||||
domainCreateCost =
|
||||
domainCreateCost.multipliedBy(
|
||||
1.0d - RegistryConfig.getSunriseDomainCreateDiscount(), RoundingMode.HALF_EVEN);
|
||||
}
|
||||
createFee =
|
||||
Fee.create(domainCreateCost.getAmount(), FeeType.CREATE, domainPrices.isPremium());
|
||||
}
|
||||
Fee createFee =
|
||||
computeCreateFee(
|
||||
tld,
|
||||
domainName,
|
||||
dateTime,
|
||||
years,
|
||||
isAnchorTenant,
|
||||
isSunriseCreate,
|
||||
allocationToken,
|
||||
currency);
|
||||
|
||||
// Create fees for the cost and the EAP fee, if any.
|
||||
Fee eapFee = tld.getEapFeeFor(dateTime);
|
||||
@@ -109,6 +122,7 @@ public final class DomainPricingLogic {
|
||||
if (!isAnchorTenant && !eapFee.hasZeroCost()) {
|
||||
feesBuilder.addFeeOrCredit(eapFee);
|
||||
}
|
||||
maybeAddXapFee(tld, dateTime, recentlyDeletedDomain, currency, feesBuilder);
|
||||
|
||||
// Apply custom logic to the create fee, if any.
|
||||
return customLogic.customizeCreatePrice(
|
||||
@@ -121,6 +135,113 @@ public final class DomainPricingLogic {
|
||||
.build());
|
||||
}
|
||||
|
||||
private Fee computeCreateFee(
|
||||
Tld tld,
|
||||
String domainName,
|
||||
Instant dateTime,
|
||||
int years,
|
||||
boolean isAnchorTenant,
|
||||
boolean isSunriseCreate,
|
||||
Optional<AllocationToken> allocationToken,
|
||||
CurrencyUnit currency)
|
||||
throws EppException {
|
||||
// Domain create cost is always zero for anchor tenants
|
||||
if (isAnchorTenant) {
|
||||
return Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
|
||||
}
|
||||
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
|
||||
if (allocationToken.isPresent()) {
|
||||
// Handle any special NONPREMIUM / SPECIFIED cases configured in the token
|
||||
domainPrices =
|
||||
applyTokenToDomainPrices(domainPrices, tld, dateTime, years, allocationToken.get());
|
||||
}
|
||||
Money domainCreateCost =
|
||||
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken, tld);
|
||||
// Apply a sunrise discount if configured and applicable
|
||||
if (isSunriseCreate) {
|
||||
domainCreateCost =
|
||||
domainCreateCost.multipliedBy(
|
||||
1.0d - RegistryConfig.getSunriseDomainCreateDiscount(), RoundingMode.HALF_EVEN);
|
||||
}
|
||||
return Fee.create(domainCreateCost.getAmount(), FeeType.CREATE, domainPrices.isPremium());
|
||||
}
|
||||
|
||||
private void maybeAddXapFee(
|
||||
Tld tld,
|
||||
Instant dateTime,
|
||||
Optional<Domain> recentlyDeletedDomain,
|
||||
CurrencyUnit currency,
|
||||
FeesAndCredits.Builder feesBuilder) {
|
||||
if (tld.getExpiryAccessPeriodModeAt(dateTime) == Tld.ExpiryAccessPeriodMode.ENABLED
|
||||
&& recentlyDeletedDomain.isPresent()
|
||||
&& isDomainEligibleForXap(recentlyDeletedDomain.get(), tld, dateTime)) {
|
||||
Optional<Fee> xapFee =
|
||||
getXapFeeFor(dateTime, recentlyDeletedDomain.get().getDeletionTime(), currency);
|
||||
xapFee.ifPresent(
|
||||
fee -> {
|
||||
feesBuilder.addFeeOrCredit(fee);
|
||||
if (!fee.hasZeroCost()) {
|
||||
feesBuilder.setFeeExtensionRequired(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and returns the Expiry Access Fee for a recently deleted domain at the given time.
|
||||
*/
|
||||
Optional<Fee> getXapFeeFor(Instant dateTime, Instant deletionTime, CurrencyUnit currency) {
|
||||
Duration elapsedTimeInXap = Duration.between(deletionTime, dateTime);
|
||||
if (!expiryAccessPeriodInitialFee.containsKey(currency)
|
||||
|| !expiryAccessPeriodFinalFee.containsKey(currency)) {
|
||||
// If the XAP schedule hasn't been configured in YAML for the currency this TLD uses, log the
|
||||
// error and then short-circuit return (to allow the EPP flow to continue normally).
|
||||
logger.atSevere().log(
|
||||
"Expiry Access Period configuration is lacking initial or final fees for currency %s.",
|
||||
currency);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Determine which tier the current time falls into (0-indexed).
|
||||
long tier = elapsedTimeInXap.toMillis() / expiryAccessPeriodTierLength.toMillis();
|
||||
long numTiers =
|
||||
expiryAccessPeriodTotalLength.toMillis() / expiryAccessPeriodTierLength.toMillis();
|
||||
if (tier < 0 || tier >= numTiers) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Calculate the parameters for the geometric sequence: XAP fee = initialFee * ratio ^ tier.
|
||||
BigDecimal xapFee;
|
||||
if (expiryAccessPeriodInitialFee.get(currency).signum() == 0 || numTiers <= 1 || tier == 0) {
|
||||
xapFee =
|
||||
expiryAccessPeriodInitialFee
|
||||
.get(currency)
|
||||
.setScale(currency.getDecimalPlaces(), RoundingMode.HALF_EVEN);
|
||||
} else {
|
||||
double base =
|
||||
expiryAccessPeriodFinalFee.get(currency).doubleValue()
|
||||
/ expiryAccessPeriodInitialFee.get(currency).doubleValue();
|
||||
double exponent = 1.0 / (numTiers - 1.0);
|
||||
BigDecimal ratio = BigDecimal.valueOf(Math.pow(base, exponent));
|
||||
BigDecimal fee = expiryAccessPeriodInitialFee.get(currency).multiply(ratio.pow((int) tier));
|
||||
xapFee = fee.setScale(currency.getDecimalPlaces(), RoundingMode.HALF_EVEN);
|
||||
}
|
||||
|
||||
Range<Instant> validPeriod =
|
||||
Range.closedOpen(
|
||||
deletionTime.plus(expiryAccessPeriodTierLength.multipliedBy(tier)),
|
||||
deletionTime.plus(expiryAccessPeriodTierLength.multipliedBy(tier + 1)));
|
||||
return Optional.of(
|
||||
Fee.create(
|
||||
xapFee,
|
||||
FeeType.XAP,
|
||||
// An XAP fee does not count as premium -- it's a separate one-time fee, independent of
|
||||
// which the domain is separately considered standard vs premium.
|
||||
false,
|
||||
validPeriod,
|
||||
validPeriod.upperEndpoint()));
|
||||
}
|
||||
|
||||
/** Returns a new renewal cost for the pricer. */
|
||||
public FeesAndCredits getRenewPrice(
|
||||
Tld tld,
|
||||
|
||||
@@ -77,6 +77,11 @@ public class FeesAndCredits extends ImmutableObject implements Buildable {
|
||||
return getTotalCostForType(FeeType.EAP);
|
||||
}
|
||||
|
||||
/** Returns the XAP cost for the event. */
|
||||
public Money getXapCost() {
|
||||
return getTotalCostForType(FeeType.XAP);
|
||||
}
|
||||
|
||||
/** Returns the renew cost for the event. */
|
||||
public Money getRenewCost() {
|
||||
return getTotalCostForType(FeeType.RENEW);
|
||||
|
||||
+15
-5
@@ -242,7 +242,14 @@ public class AllocationTokenFlowUtils {
|
||||
case CREATE ->
|
||||
pricingLogic
|
||||
.getCreatePrice(
|
||||
tld, domainName, now, yearsForAction, false, false, Optional.of(token))
|
||||
tld,
|
||||
domainName,
|
||||
now,
|
||||
Optional.empty(),
|
||||
yearsForAction,
|
||||
false,
|
||||
false,
|
||||
Optional.of(token))
|
||||
.getTotalCost()
|
||||
.getAmount();
|
||||
case RENEW ->
|
||||
@@ -270,11 +277,14 @@ public class AllocationTokenFlowUtils {
|
||||
return maybeTokenEntity.get();
|
||||
}
|
||||
|
||||
maybeTokenEntity = AllocationToken.get(VKey.create(AllocationToken.class, token));
|
||||
if (maybeTokenEntity.isEmpty()) {
|
||||
throw new NonexistentAllocationTokenException();
|
||||
VKey<AllocationToken> tokenKey = VKey.create(AllocationToken.class, token);
|
||||
AllocationToken tokenEntity =
|
||||
AllocationToken.get(tokenKey).orElseThrow(NonexistentAllocationTokenException::new);
|
||||
if (tokenEntity.getTokenType().equals(AllocationToken.TokenType.SINGLE_USE)) {
|
||||
// Reload the token to avoid possible cache race conditions where the token may have already
|
||||
// been redeemed
|
||||
tokenEntity = tm().loadByKey(tokenKey);
|
||||
}
|
||||
AllocationToken tokenEntity = maybeTokenEntity.get();
|
||||
validateTokenEntity(tokenEntity, registrarId, domainName, now);
|
||||
return tokenEntity;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import static google.registry.util.DateTimeUtils.parseInstant;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.KeyDeserializer;
|
||||
@@ -38,6 +39,7 @@ import com.google.common.collect.ImmutableSortedSet;
|
||||
import google.registry.model.common.FeatureFlag.FeatureStatus;
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.tld.Tld.ExpiryAccessPeriodMode;
|
||||
import google.registry.model.tld.Tld.TldState;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.IOException;
|
||||
@@ -336,7 +338,8 @@ public class EntityYamlUtils {
|
||||
@Override
|
||||
public TimedTransitionProperty<TldState> deserialize(
|
||||
JsonParser jp, DeserializationContext context) throws IOException {
|
||||
SortedMap<String, String> valueMap = jp.readValueAs(SortedMap.class);
|
||||
SortedMap<String, String> valueMap =
|
||||
jp.readValueAs(new TypeReference<SortedMap<String, String>>() {});
|
||||
return TimedTransitionProperty.fromValueMap(
|
||||
valueMap.keySet().stream()
|
||||
.collect(
|
||||
@@ -347,6 +350,37 @@ public class EntityYamlUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom JSON deserializer for a {@link TimedTransitionProperty} of {@link
|
||||
* ExpiryAccessPeriodMode}.
|
||||
*/
|
||||
public static class TimedTransitionPropertyExpiryAccessPeriodModeDeserializer
|
||||
extends StdDeserializer<TimedTransitionProperty<ExpiryAccessPeriodMode>> {
|
||||
|
||||
public TimedTransitionPropertyExpiryAccessPeriodModeDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public TimedTransitionPropertyExpiryAccessPeriodModeDeserializer(
|
||||
Class<TimedTransitionProperty<ExpiryAccessPeriodMode>> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimedTransitionProperty<ExpiryAccessPeriodMode> deserialize(
|
||||
JsonParser jp, DeserializationContext context) throws IOException {
|
||||
SortedMap<String, String> valueMap =
|
||||
jp.readValueAs(new TypeReference<SortedMap<String, String>>() {});
|
||||
return TimedTransitionProperty.fromValueMap(
|
||||
valueMap.keySet().stream()
|
||||
.collect(
|
||||
toImmutableSortedMap(
|
||||
natural(),
|
||||
key -> parseInstant(key),
|
||||
key -> ExpiryAccessPeriodMode.valueOf(valueMap.get(key)))));
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON deserializer for a {@link TimedTransitionProperty} of {@link Money}. */
|
||||
public static class TimedTransitionPropertyMoneyDeserializer
|
||||
extends StdDeserializer<TimedTransitionProperty<Money>> {
|
||||
@@ -362,7 +396,8 @@ public class EntityYamlUtils {
|
||||
@Override
|
||||
public TimedTransitionProperty<Money> deserialize(JsonParser jp, DeserializationContext context)
|
||||
throws IOException {
|
||||
SortedMap<String, LinkedHashMap<String, Object>> valueMap = jp.readValueAs(SortedMap.class);
|
||||
SortedMap<String, LinkedHashMap<String, Object>> valueMap =
|
||||
jp.readValueAs(new TypeReference<SortedMap<String, LinkedHashMap<String, Object>>>() {});
|
||||
return TimedTransitionProperty.fromValueMap(
|
||||
valueMap.keySet().stream()
|
||||
.collect(
|
||||
@@ -392,7 +427,8 @@ public class EntityYamlUtils {
|
||||
@Override
|
||||
public TimedTransitionProperty<FeatureStatus> deserialize(
|
||||
JsonParser jp, DeserializationContext context) throws IOException {
|
||||
SortedMap<String, String> valueMap = jp.readValueAs(SortedMap.class);
|
||||
SortedMap<String, String> valueMap =
|
||||
jp.readValueAs(new TypeReference<SortedMap<String, String>>() {});
|
||||
return TimedTransitionProperty.fromValueMap(
|
||||
valueMap.keySet().stream()
|
||||
.collect(
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.model;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
|
||||
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
@@ -400,4 +401,24 @@ public final class ForeignKeyUtils {
|
||||
.filter(e -> now.isBefore(e.getDeletionTime()))
|
||||
.map(e -> e.cloneProjectedAtTime(now));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the last created version of multiple {@link EppResource}s from the replica database by
|
||||
* foreign keys, using a cache.
|
||||
*
|
||||
* <p>This method ignores the config setting for caching, and is reserved for use cases that can
|
||||
* tolerate slightly stale data.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <E extends EppResource> ImmutableMap<String, E> loadResourcesByCache(
|
||||
Class<E> clazz, Collection<String> foreignKeys, Instant now) {
|
||||
ImmutableSet<VKey<? extends EppResource>> vkeys =
|
||||
foreignKeys.stream().map(fk -> VKey.create(clazz, fk)).collect(toImmutableSet());
|
||||
return foreignKeyToResourceCache.getAll(vkeys).entrySet().stream()
|
||||
.filter(e -> e.getValue().isPresent() && now.isBefore(e.getValue().get().getDeletionTime()))
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
e -> (String) e.getKey().getKey(),
|
||||
e -> (E) e.getValue().get().cloneProjectedAtTime(now)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public abstract class BillingBase extends ImmutableObject
|
||||
@Deprecated // DO NOT USE THIS REASON. IT REMAINS BECAUSE OF HISTORICAL DATA. SEE b/31676071.
|
||||
ERROR(false),
|
||||
FEE_EARLY_ACCESS(true),
|
||||
FEE_EXPIRY_ACCESS(true),
|
||||
RENEW(true),
|
||||
RESTORE(true),
|
||||
SERVER_STATUS(false),
|
||||
|
||||
@@ -200,7 +200,7 @@ public class BillingEvent extends BillingBase {
|
||||
checkState(
|
||||
instance.reason.hasPeriodYears() == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is "
|
||||
+ "CREATE, FEE_EARLY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
+ "CREATE, FEE_EARLY_ACCESS, FEE_EXPIRY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
}
|
||||
checkState(
|
||||
instance.getFlags().contains(Flag.SYNTHETIC) == (instance.syntheticCreationTime != null),
|
||||
|
||||
@@ -51,6 +51,10 @@ public abstract class BaseFee extends ImmutableObject {
|
||||
public enum FeeType {
|
||||
CREATE("create"),
|
||||
EAP("Early Access Period, fee expires: %s", "Early Access Period"),
|
||||
/**
|
||||
* A one-time fee for registering a recently dropped domain, during the Expiry Access Period.
|
||||
*/
|
||||
XAP("Expiry Access Period, fee expires: %s", "Expiry Access Period"),
|
||||
RENEW("renew"),
|
||||
RESTORE("restore"),
|
||||
/**
|
||||
|
||||
@@ -52,6 +52,7 @@ import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithVKey;
|
||||
import google.registry.persistence.converter.AllocationTokenStatusTransitionUserType;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import jakarta.persistence.AttributeOverride;
|
||||
import jakarta.persistence.AttributeOverrides;
|
||||
import jakarta.persistence.Column;
|
||||
@@ -61,6 +62,7 @@ import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -374,28 +376,39 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
|
||||
return ALLOCATION_TOKENS_CACHE.getAll(keys);
|
||||
}
|
||||
|
||||
/** A cache that loads the {@link AllocationToken} object for a given AllocationToken VKey. */
|
||||
private static final LoadingCache<VKey<AllocationToken>, Optional<AllocationToken>>
|
||||
ALLOCATION_TOKENS_CACHE =
|
||||
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
|
||||
.build(
|
||||
new CacheLoader<>() {
|
||||
@Override
|
||||
public Optional<AllocationToken> load(VKey<AllocationToken> key) {
|
||||
return tm().reTransact(() -> tm().loadByKeyIfPresent(key));
|
||||
}
|
||||
@VisibleForTesting
|
||||
public static void setCacheForTest(Optional<Duration> expiry) {
|
||||
Duration effectiveExpiry = expiry.orElse(getSingletonCacheRefreshDuration());
|
||||
ALLOCATION_TOKENS_CACHE = createAllocationTokensCache(effectiveExpiry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<? extends VKey<AllocationToken>, ? extends Optional<AllocationToken>>
|
||||
loadAll(Set<? extends VKey<AllocationToken>> keys) {
|
||||
return tm().reTransact(
|
||||
() ->
|
||||
keys.stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
key -> key, key -> tm().loadByKeyIfPresent(key))));
|
||||
}
|
||||
});
|
||||
/** A cache that loads the {@link AllocationToken} object for a given AllocationToken VKey. */
|
||||
@NonFinalForTesting
|
||||
private static LoadingCache<VKey<AllocationToken>, Optional<AllocationToken>>
|
||||
ALLOCATION_TOKENS_CACHE = createAllocationTokensCache(getSingletonCacheRefreshDuration());
|
||||
|
||||
private static LoadingCache<VKey<AllocationToken>, Optional<AllocationToken>>
|
||||
createAllocationTokensCache(Duration expiry) {
|
||||
return CacheUtils.newCacheBuilder(expiry)
|
||||
.build(
|
||||
new CacheLoader<>() {
|
||||
@Override
|
||||
public Optional<AllocationToken> load(VKey<AllocationToken> key) {
|
||||
return tm().reTransact(() -> tm().loadByKeyIfPresent(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<? extends VKey<AllocationToken>, ? extends Optional<AllocationToken>>
|
||||
loadAll(Set<? extends VKey<AllocationToken>> keys) {
|
||||
return tm().reTransact(
|
||||
() ->
|
||||
keys.stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
key -> key, key -> tm().loadByKeyIfPresent(key))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<AllocationToken> createVKey() {
|
||||
|
||||
@@ -265,6 +265,18 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
@Column(nullable = false)
|
||||
boolean blockPremiumNames;
|
||||
|
||||
/**
|
||||
* Whether this registrar has opted into participating in the Expiry Access Period (XAP).
|
||||
*
|
||||
* <p>If this is false, any domain currently in XAP will appear as reserved/unavailable on checks
|
||||
* and creates.
|
||||
*
|
||||
* <p>TODO(mcilwain): Drop the temporary database default for expiry_access_period_enabled in a
|
||||
* subsequent schema migration once this code is deployed.
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
boolean expiryAccessPeriodEnabled;
|
||||
|
||||
// Authentication.
|
||||
|
||||
/** X.509 PEM client certificate(s) used to authenticate registrar to EPP service. */
|
||||
@@ -560,6 +572,10 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
return blockPremiumNames;
|
||||
}
|
||||
|
||||
public boolean getExpiryAccessPeriodEnabled() {
|
||||
return expiryAccessPeriodEnabled;
|
||||
}
|
||||
|
||||
public boolean getContactsRequireSyncing() {
|
||||
return contactsRequireSyncing;
|
||||
}
|
||||
@@ -654,6 +670,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
.put("whoisServer", getWhoisServer())
|
||||
.putListOfStrings("rdapBaseUrls", getRdapBaseUrls())
|
||||
.put("blockPremiumNames", blockPremiumNames)
|
||||
.put("expiryAccessPeriodEnabled", expiryAccessPeriodEnabled)
|
||||
.put("url", url)
|
||||
.put("icannReferralEmail", getIcannReferralEmail())
|
||||
.put("driveFolderId", driveFolderId)
|
||||
@@ -918,6 +935,11 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setExpiryAccessPeriodEnabled(boolean expiryAccessPeriodEnabled) {
|
||||
getInstance().expiryAccessPeriodEnabled = expiryAccessPeriodEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUrl(String url) {
|
||||
getInstance().url = url;
|
||||
return this;
|
||||
|
||||
@@ -56,6 +56,7 @@ import google.registry.model.EntityYamlUtils.OptionalDurationSerializer;
|
||||
import google.registry.model.EntityYamlUtils.OptionalStringSerializer;
|
||||
import google.registry.model.EntityYamlUtils.SortedEnumSetSerializer;
|
||||
import google.registry.model.EntityYamlUtils.SortedSetSerializer;
|
||||
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyExpiryAccessPeriodModeDeserializer;
|
||||
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyMoneyDeserializer;
|
||||
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyTldStateDeserializer;
|
||||
import google.registry.model.EntityYamlUtils.TokenVKeyListDeserializer;
|
||||
@@ -76,6 +77,7 @@ import google.registry.persistence.EntityCallbacksListener.RecursivePostUpdate;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.converter.AllocationTokenVkeyListUserType;
|
||||
import google.registry.persistence.converter.BillingCostTransitionUserType;
|
||||
import google.registry.persistence.converter.ExpiryAccessPeriodModeTransitionUserType;
|
||||
import google.registry.persistence.converter.TldStateTransitionUserType;
|
||||
import google.registry.tldconfig.idn.IdnTableEnum;
|
||||
import google.registry.util.Idn;
|
||||
@@ -120,6 +122,8 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
|
||||
public static final boolean DEFAULT_ESCROW_ENABLED = false;
|
||||
public static final boolean DEFAULT_DNS_PAUSED = false;
|
||||
public static final ExpiryAccessPeriodMode DEFAULT_EXPIRY_ACCESS_PERIOD_MODE =
|
||||
ExpiryAccessPeriodMode.DISABLED;
|
||||
public static final Duration DEFAULT_ADD_GRACE_PERIOD = Duration.ofDays(5);
|
||||
public static final Duration DEFAULT_AUTO_RENEW_GRACE_PERIOD = Duration.ofDays(45);
|
||||
public static final Duration DEFAULT_REDEMPTION_GRACE_PERIOD = Duration.ofDays(30);
|
||||
@@ -197,6 +201,12 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
PDT
|
||||
}
|
||||
|
||||
/** The modes an Expiry Access Period (XAP) can be in at any given point in time. */
|
||||
public enum ExpiryAccessPeriodMode {
|
||||
DISABLED,
|
||||
ENABLED
|
||||
}
|
||||
|
||||
/** Returns the TLD for a given TLD, throwing if none exists. */
|
||||
public static Tld get(String tld) {
|
||||
return CACHE.get(tld).orElseThrow(() -> new TldNotFoundException(tld));
|
||||
@@ -426,6 +436,19 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
@Column(nullable = false)
|
||||
boolean dnsPaused = DEFAULT_DNS_PAUSED;
|
||||
|
||||
/**
|
||||
* Whether the Expiry Access Period following domain deletes is enabled for this TLD.
|
||||
*
|
||||
* <p>TODO(mcilwain): Once this Java code has been fully deployed to production, drop the
|
||||
* temporary database-level DEFAULT constraint on the "expiry_access_period_transitions" column in
|
||||
* the "Tld" table in a subsequent schema release.
|
||||
*/
|
||||
@Column(nullable = false, columnDefinition = "hstore")
|
||||
@Type(ExpiryAccessPeriodModeTransitionUserType.class)
|
||||
@JsonDeserialize(using = TimedTransitionPropertyExpiryAccessPeriodModeDeserializer.class)
|
||||
TimedTransitionProperty<ExpiryAccessPeriodMode> expiryAccessPeriodTransitions =
|
||||
TimedTransitionProperty.withInitialValue(DEFAULT_EXPIRY_ACCESS_PERIOD_MODE);
|
||||
|
||||
/**
|
||||
* The length of the add grace period for this TLD.
|
||||
*
|
||||
@@ -634,6 +657,14 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return dnsPaused;
|
||||
}
|
||||
|
||||
public ExpiryAccessPeriodMode getExpiryAccessPeriodModeAt(Instant now) {
|
||||
return expiryAccessPeriodTransitions.getValueAtTime(now);
|
||||
}
|
||||
|
||||
public ImmutableSortedMap<Instant, ExpiryAccessPeriodMode> getExpiryAccessPeriodTransitions() {
|
||||
return expiryAccessPeriodTransitions.toValueMap();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getDriveFolderId() {
|
||||
return driveFolderId;
|
||||
@@ -830,6 +861,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
createBillingCostTransitions.checkValidity();
|
||||
renewBillingCostTransitions.checkValidity();
|
||||
eapFeeSchedule.checkValidity();
|
||||
expiryAccessPeriodTransitions.checkValidity();
|
||||
// All costs must be in the expected currency.
|
||||
checkArgumentNotNull(getCurrency(), "Currency must be set");
|
||||
Predicate<Money> currencyCheck =
|
||||
@@ -913,6 +945,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap<Instant, ExpiryAccessPeriodMode> transitionsMap) {
|
||||
getInstance().expiryAccessPeriodTransitions =
|
||||
TimedTransitionProperty.fromValueMap(transitionsMap);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDriveFolderId(String driveFolderId) {
|
||||
getInstance().driveFolderId = driveFolderId;
|
||||
return this;
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 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.persistence.converter;
|
||||
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.model.tld.Tld.ExpiryAccessPeriodMode;
|
||||
|
||||
/** Hibernate custom type for {@link TimedTransitionProperty} of {@link ExpiryAccessPeriodMode}. */
|
||||
public class ExpiryAccessPeriodModeTransitionUserType
|
||||
extends TimedTransitionBaseUserType<ExpiryAccessPeriodMode> {
|
||||
|
||||
@Override
|
||||
String valueToString(ExpiryAccessPeriodMode value) {
|
||||
return value.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
ExpiryAccessPeriodMode stringToValue(String string) {
|
||||
return ExpiryAccessPeriodMode.valueOf(string);
|
||||
}
|
||||
}
|
||||
@@ -454,9 +454,8 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
||||
// We must break the query up into chunks, because the in operator is limited to 30 subqueries.
|
||||
// Since it is possible for the same domain to show up more than once in our result list (if
|
||||
// we do a wildcard nameserver search that returns multiple nameservers used by the same
|
||||
// domain), we must create a set of resulting {@link Domain} objects. Use a sorted set,
|
||||
// and fetch all domains, to make sure that we can return the first domains in alphabetical
|
||||
// order.
|
||||
// domain), we must create a set of resulting {@link Domain}s. Use a sorted set, fetch all
|
||||
// domains, to make sure that we can return the first domains in alphabetical order.
|
||||
ImmutableSortedSet.Builder<Domain> domainSetBuilder =
|
||||
ImmutableSortedSet.orderedBy(Comparator.comparing(Domain::getDomainName));
|
||||
int numHostKeysSearched = 0;
|
||||
@@ -465,7 +464,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
||||
replicaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
for (VKey<Host> hostKey : hostKeys) {
|
||||
for (VKey<Host> hostKey : chunk) {
|
||||
CriteriaQueryBuilder<Domain> queryBuilder =
|
||||
CriteriaQueryBuilder.create(replicaTm(), Domain.class)
|
||||
.whereFieldContains("nsHosts", hostKey)
|
||||
|
||||
@@ -185,7 +185,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
||||
throw new UnprocessableEntityException(
|
||||
"A suffix after a wildcard in a nameserver lookup must be an in-bailiwick domain");
|
||||
}
|
||||
List<Host> hostList = new ArrayList<>();
|
||||
List<String> matchingFqhns = new ArrayList<>();
|
||||
for (String fqhn : ImmutableSortedSet.copyOf(domain.get().getSubordinateHosts())) {
|
||||
if (cursorString.isPresent() && (fqhn.compareTo(cursorString.get()) <= 0)) {
|
||||
continue;
|
||||
@@ -193,10 +193,22 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
||||
// We can't just check that the host name starts with the initial query string, because
|
||||
// then the query ns.exam*.example.com would match against nameserver ns.example.com.
|
||||
if (partialStringQuery.matches(fqhn)) {
|
||||
Optional<Host> host =
|
||||
ForeignKeyUtils.loadResourceByCache(Host.class, fqhn, getRequestTime());
|
||||
if (shouldBeVisible(host)) {
|
||||
hostList.add(host.get());
|
||||
matchingFqhns.add(fqhn);
|
||||
}
|
||||
}
|
||||
List<Host> hostList = new ArrayList<>();
|
||||
int chunkSize = getStandardQuerySizeLimit();
|
||||
// Batch load from cache in chunks to avoid sequential N+1 database queries on cache misses.
|
||||
for (List<String> fqhnChunk : Iterables.partition(matchingFqhns, chunkSize)) {
|
||||
if (hostList.size() > rdapResultSetMaxSize) {
|
||||
break;
|
||||
}
|
||||
ImmutableMap<String, Host> cachedHosts =
|
||||
ForeignKeyUtils.loadResourcesByCache(Host.class, fqhnChunk, getRequestTime());
|
||||
for (String fqhn : fqhnChunk) {
|
||||
Host host = cachedHosts.get(fqhn);
|
||||
if (host != null && shouldBeVisible(host)) {
|
||||
hostList.add(host);
|
||||
if (hostList.size() > rdapResultSetMaxSize) {
|
||||
break;
|
||||
}
|
||||
@@ -204,10 +216,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
||||
}
|
||||
}
|
||||
return makeSearchResults(
|
||||
hostList,
|
||||
IncompletenessWarningType.COMPLETE,
|
||||
domain.get().getSubordinateHosts().size(),
|
||||
CursorType.NAME);
|
||||
hostList, IncompletenessWarningType.COMPLETE, hostList.size(), CursorType.NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,6 +119,18 @@ public abstract class HttpException extends RuntimeException {
|
||||
}
|
||||
}
|
||||
|
||||
/** Exception that causes a 413 response. */
|
||||
public static final class PayloadTooLargeException extends HttpException {
|
||||
public PayloadTooLargeException(String message) {
|
||||
super(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, message, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResponseCodeString() {
|
||||
return "Payload Too Large";
|
||||
}
|
||||
}
|
||||
|
||||
/** Exception that causes a 404 response. */
|
||||
public static final class NotFoundException extends HttpException {
|
||||
public NotFoundException() {
|
||||
|
||||
@@ -32,7 +32,6 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
@@ -42,6 +41,7 @@ import com.google.protobuf.ByteString;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.PayloadTooLargeException;
|
||||
import google.registry.request.HttpException.UnsupportedMediaTypeException;
|
||||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.request.lock.LockHandler;
|
||||
@@ -50,6 +50,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -57,6 +58,8 @@ import java.util.Optional;
|
||||
@Module
|
||||
public final class RequestModule {
|
||||
|
||||
public static final int MAX_PAYLOAD_BYTES = 50 * 1024 * 1024;
|
||||
|
||||
private final HttpServletRequest req;
|
||||
private final HttpServletResponse rsp;
|
||||
private final AuthResult authResult;
|
||||
@@ -167,9 +170,37 @@ public final class RequestModule {
|
||||
|
||||
@Provides
|
||||
@Payload
|
||||
static String providePayloadAsString(HttpServletRequest req) {
|
||||
public static String providePayloadAsString(
|
||||
@Payload byte[] payloadBytes, HttpServletRequest req) {
|
||||
String charsetName = req.getCharacterEncoding();
|
||||
Charset charset;
|
||||
try {
|
||||
return CharStreams.toString(req.getReader());
|
||||
charset = (charsetName != null) ? Charset.forName(charsetName) : UTF_8;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new UnsupportedMediaTypeException("Unsupported charset: " + charsetName, e);
|
||||
}
|
||||
return new String(payloadBytes, charset);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Payload
|
||||
public static byte[] providePayloadAsBytes(HttpServletRequest req) {
|
||||
try {
|
||||
if (req.getContentLengthLong() > MAX_PAYLOAD_BYTES) {
|
||||
throw new PayloadTooLargeException(
|
||||
String.format(
|
||||
"Payload size %d exceeds limit of %d bytes",
|
||||
req.getContentLengthLong(), MAX_PAYLOAD_BYTES));
|
||||
}
|
||||
if (req.getInputStream() == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
byte[] bytes =
|
||||
ByteStreams.toByteArray(ByteStreams.limit(req.getInputStream(), MAX_PAYLOAD_BYTES + 1));
|
||||
if (bytes.length > MAX_PAYLOAD_BYTES) {
|
||||
throw new PayloadTooLargeException("Payload exceeds maximum allowed size");
|
||||
}
|
||||
return bytes;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -177,22 +208,8 @@ public final class RequestModule {
|
||||
|
||||
@Provides
|
||||
@Payload
|
||||
static byte[] providePayloadAsBytes(HttpServletRequest req) {
|
||||
try {
|
||||
return ByteStreams.toByteArray(req.getInputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Payload
|
||||
static ByteString providePayloadAsByteString(HttpServletRequest req) {
|
||||
try {
|
||||
return ByteString.copyFrom(ByteStreams.toByteArray(req.getInputStream()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
public static ByteString providePayloadAsByteString(@Payload byte[] payloadBytes) {
|
||||
return ByteString.copyFrom(payloadBytes);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -251,12 +268,10 @@ public final class RequestModule {
|
||||
|
||||
@Provides
|
||||
@OptionalJsonPayload
|
||||
public static Optional<JsonElement> provideJsonBody(HttpServletRequest req, Gson gson) {
|
||||
try {
|
||||
// GET requests return a null reader and thus a null JsonObject, which is fine
|
||||
return Optional.ofNullable(gson.fromJson(req.getReader(), JsonElement.class));
|
||||
} catch (IOException e) {
|
||||
public static Optional<JsonElement> provideJsonBody(@Payload String payloadString, Gson gson) {
|
||||
if (payloadString.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(gson.fromJson(payloadString, JsonElement.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
|
||||
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -37,6 +38,11 @@ import org.apache.commons.compress.utils.IOUtils;
|
||||
/** Utilities for common functionality relating to {@link URLConnection}s. */
|
||||
public final class UrlConnectionUtils {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// 100 MB is arbitrary, not specifically selected for any reason
|
||||
public static final int MAX_RESPONSE_PAYLOAD_BYTES = 100 * 1024 * 1024;
|
||||
|
||||
private UrlConnectionUtils() {}
|
||||
|
||||
/**
|
||||
@@ -48,10 +54,26 @@ public final class UrlConnectionUtils {
|
||||
* @see HttpURLConnection#getErrorStream()
|
||||
*/
|
||||
public static byte[] getResponseBytes(HttpURLConnection connection) throws IOException {
|
||||
int responseCode = connection.getResponseCode();
|
||||
try (InputStream is =
|
||||
responseCode < 400 ? connection.getInputStream() : connection.getErrorStream()) {
|
||||
return ByteStreams.toByteArray(is);
|
||||
try {
|
||||
if (connection.getContentLengthLong() > MAX_RESPONSE_PAYLOAD_BYTES) {
|
||||
throw new IOException(
|
||||
String.format(
|
||||
"Response size %d exceeds limit of %d bytes",
|
||||
connection.getContentLengthLong(), MAX_RESPONSE_PAYLOAD_BYTES));
|
||||
} else if ((double) connection.getContentLengthLong() / MAX_RESPONSE_PAYLOAD_BYTES >= 0.9) {
|
||||
logger.atSevere().log(
|
||||
"Response from %s was within 90%% of the maximum response size", connection.getURL());
|
||||
}
|
||||
int responseCode = connection.getResponseCode();
|
||||
try (InputStream is =
|
||||
responseCode < 400 ? connection.getInputStream() : connection.getErrorStream()) {
|
||||
byte[] bytes =
|
||||
ByteStreams.toByteArray(ByteStreams.limit(is, MAX_RESPONSE_PAYLOAD_BYTES + 1));
|
||||
if (bytes.length > MAX_RESPONSE_PAYLOAD_BYTES) {
|
||||
throw new IOException("Response exceeds maximum allowed size");
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.console.ConsolePermission;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.console.UserRoles;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.RegistryLock;
|
||||
@@ -100,16 +103,16 @@ public final class DomainLockUtils {
|
||||
*
|
||||
* <p>This assumes that the lock object / domain in question has a pending lock or unlock.
|
||||
*/
|
||||
public RegistryLock verifyVerificationCode(String verificationCode, boolean isAdmin) {
|
||||
public RegistryLock verifyVerificationCode(String verificationCode, User user) {
|
||||
RegistryLock result =
|
||||
tm().transact(
|
||||
() -> {
|
||||
RegistryLock lock = getByVerificationCode(verificationCode);
|
||||
if (lock.getLockCompletionTime().isEmpty()) {
|
||||
return verifyAndApplyLock(lock, isAdmin);
|
||||
return verifyAndApplyLock(lock, user);
|
||||
} else if (lock.getUnlockRequestTime().isPresent()
|
||||
&& lock.getUnlockCompletionTime().isEmpty()) {
|
||||
return verifyAndApplyUnlock(lock, isAdmin);
|
||||
return verifyAndApplyUnlock(lock, user);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
@@ -203,11 +206,18 @@ public final class DomainLockUtils {
|
||||
countdown));
|
||||
}
|
||||
|
||||
private RegistryLock verifyAndApplyLock(RegistryLock lock, boolean isAdmin) {
|
||||
private RegistryLock verifyAndApplyLock(RegistryLock lock, User user) {
|
||||
Instant now = tm().getTxTime();
|
||||
checkArgument(
|
||||
!lock.isLockRequestExpired(now), "The pending lock has expired; please try again");
|
||||
|
||||
UserRoles userRoles = user.getUserRoles();
|
||||
boolean isAdmin = userRoles.isAdmin();
|
||||
checkArgument(
|
||||
userRoles.hasPermission(lock.getRegistrarId(), ConsolePermission.REGISTRY_LOCK),
|
||||
"User %s does not have registry lock permission on registrar %s",
|
||||
user.getEmailAddress(),
|
||||
lock.getRegistrarId());
|
||||
checkArgument(!lock.isSuperuser() || isAdmin, "Non-admin user cannot complete admin lock");
|
||||
|
||||
RegistryLock newLock =
|
||||
@@ -217,13 +227,29 @@ public final class DomainLockUtils {
|
||||
return newLock;
|
||||
}
|
||||
|
||||
private RegistryLock verifyAndApplyUnlock(RegistryLock lock, boolean isAdmin) {
|
||||
private RegistryLock verifyAndApplyUnlock(RegistryLock lock, User user) {
|
||||
Instant now = tm().getTxTime();
|
||||
checkArgument(
|
||||
!lock.isUnlockRequestExpired(now), "The pending unlock has expired; please try again");
|
||||
|
||||
checkArgument(isAdmin || !lock.isSuperuser(), "Non-admin user cannot complete admin unlock");
|
||||
UserRoles userRoles = user.getUserRoles();
|
||||
boolean isAdmin = userRoles.isAdmin();
|
||||
checkArgument(
|
||||
userRoles.hasPermission(lock.getRegistrarId(), ConsolePermission.REGISTRY_LOCK),
|
||||
"User %s does not have registry lock permission on registrar %s",
|
||||
user.getEmailAddress(),
|
||||
lock.getRegistrarId());
|
||||
checkArgument(!lock.isSuperuser() || isAdmin, "Non-admin user cannot complete admin unlock");
|
||||
|
||||
// The pending unlock must still be the most recent RegistryLock for this domain. If a newer
|
||||
// lock exists (e.g. an admin/superuser lock applied after this unlock was requested or a
|
||||
// different unlock+relock), the verification code being redeemed is for a superseded row and
|
||||
// must not be allowed to strip the lock statuses that the newer row applied.
|
||||
Optional<RegistryLock> mostRecent = RegistryLockDao.getMostRecentByRepoId(lock.getRepoId());
|
||||
checkArgument(
|
||||
mostRecent.isPresent() && mostRecent.get().getRevisionId().equals(lock.getRevisionId()),
|
||||
"The pending unlock on %s has been superseded; please request a new unlock",
|
||||
lock.getDomainName());
|
||||
RegistryLock newLock =
|
||||
RegistryLockDao.save(lock.asBuilder().setUnlockCompletionTime(now).build());
|
||||
removeLockStatuses(newLock, isAdmin, now);
|
||||
|
||||
@@ -91,10 +91,11 @@ public class ShellCommand implements Command {
|
||||
* flags aren't available in the constructor, so we have to do it in the {@link #run} function.
|
||||
*/
|
||||
private final CommandRunner originalRunner;
|
||||
|
||||
private final LineReader lineReader;
|
||||
private final Clock clock;
|
||||
|
||||
private LineReader lineReader;
|
||||
private Terminal terminal;
|
||||
private JCommander jcommanderForCompletions;
|
||||
private String prompt = null;
|
||||
|
||||
@Parameter(
|
||||
@@ -118,21 +119,22 @@ public class ShellCommand implements Command {
|
||||
""")
|
||||
boolean encapsulateOutput = false;
|
||||
|
||||
ShellCommand(CommandRunner runner) throws IOException {
|
||||
this(TerminalBuilder.terminal(), new SystemClock(), runner);
|
||||
prompt = "nom > ";
|
||||
lineReader.variable(LineReader.HISTORY_FILE, Path.of(USER_HOME.value(), HISTORY_FILE));
|
||||
ShellCommand(CommandRunner runner) {
|
||||
this.originalRunner = runner;
|
||||
this.clock = new SystemClock();
|
||||
this.prompt = "nom > ";
|
||||
}
|
||||
|
||||
ShellCommand(Terminal terminal, Clock clock, CommandRunner runner) {
|
||||
this.originalRunner = runner;
|
||||
this.terminal = terminal;
|
||||
this.lineReader = LineReaderBuilder.builder().terminal(terminal).build();
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
private void setPrompt(RegistryToolEnvironment environment, boolean alert) {
|
||||
// Do not set the prompt in tests.
|
||||
if (lineReader.getTerminal() instanceof DumbTerminal) {
|
||||
if (lineReader == null || lineReader.getTerminal() instanceof DumbTerminal) {
|
||||
return;
|
||||
}
|
||||
prompt =
|
||||
@@ -143,7 +145,10 @@ public class ShellCommand implements Command {
|
||||
}
|
||||
|
||||
public ShellCommand buildCompletions(JCommander jcommander) {
|
||||
((LineReaderImpl) lineReader).setCompleter(new JCommanderCompleter(jcommander));
|
||||
this.jcommanderForCompletions = jcommander;
|
||||
if (lineReader != null) {
|
||||
((LineReaderImpl) lineReader).setCompleter(new JCommanderCompleter(jcommander));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -209,6 +214,19 @@ public class ShellCommand implements Command {
|
||||
/** Run the shell until the user presses "Ctrl-D". */
|
||||
@Override
|
||||
public void run() {
|
||||
if (lineReader == null) {
|
||||
try {
|
||||
this.terminal = TerminalBuilder.terminal();
|
||||
this.lineReader = LineReaderBuilder.builder().terminal(terminal).build();
|
||||
lineReader.variable(LineReader.HISTORY_FILE, Path.of(USER_HOME.value(), HISTORY_FILE));
|
||||
if (jcommanderForCompletions != null) {
|
||||
((LineReaderImpl) lineReader)
|
||||
.setCompleter(new JCommanderCompleter(jcommanderForCompletions));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to initialize terminal", e);
|
||||
}
|
||||
}
|
||||
// Wrap standard output and error if requested. We have to do so here in run because the flags
|
||||
// haven't been processed in the constructor.
|
||||
CommandRunner runner =
|
||||
|
||||
+17
-19
@@ -15,7 +15,7 @@
|
||||
package google.registry.ui.server.console;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.gson.annotations.Expose;
|
||||
@@ -34,7 +34,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
@Action(
|
||||
service = Service.CONSOLE,
|
||||
path = ConsoleRegistryLockVerifyAction.PATH,
|
||||
method = {GET},
|
||||
method = {POST},
|
||||
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
|
||||
public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
|
||||
|
||||
@@ -54,9 +54,8 @@ public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getHandler(User user) {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.verifyVerificationCode(lockVerificationCode, user.getUserRoles().isAdmin());
|
||||
protected void postHandler(User user) {
|
||||
RegistryLock lock = domainLockUtils.verifyVerificationCode(lockVerificationCode, user);
|
||||
RegistryLockAction action =
|
||||
lock.getUnlockCompletionTime().isPresent()
|
||||
? RegistryLockAction.UNLOCKED
|
||||
@@ -65,20 +64,19 @@ public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
|
||||
new RegistryLockVerificationResponse(
|
||||
Ascii.toLowerCase(action.toString()), lock.getDomainName(), lock.getRegistrarId());
|
||||
tm().transact(
|
||||
() -> {
|
||||
finishAndPersistConsoleUpdateHistory(
|
||||
new ConsoleUpdateHistory.Builder()
|
||||
.setType(
|
||||
action == RegistryLockAction.LOCKED
|
||||
? ConsoleUpdateHistory.Type.REGISTRY_LOCK
|
||||
: ConsoleUpdateHistory.Type.REGISTRY_UNLOCK)
|
||||
.setDescription(
|
||||
String.format(
|
||||
"%s%s%s",
|
||||
lock.getRegistrarId(),
|
||||
ConsoleUpdateHistory.DESCRIPTION_SEPARATOR,
|
||||
lockResponse)));
|
||||
});
|
||||
() ->
|
||||
finishAndPersistConsoleUpdateHistory(
|
||||
new ConsoleUpdateHistory.Builder()
|
||||
.setType(
|
||||
action == RegistryLockAction.LOCKED
|
||||
? ConsoleUpdateHistory.Type.REGISTRY_LOCK
|
||||
: ConsoleUpdateHistory.Type.REGISTRY_UNLOCK)
|
||||
.setDescription(
|
||||
String.format(
|
||||
"%s%s%s",
|
||||
lock.getRegistrarId(),
|
||||
ConsoleUpdateHistory.DESCRIPTION_SEPARATOR,
|
||||
lockResponse))));
|
||||
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(lockResponse));
|
||||
consoleApiParams.response().setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.google.api.services.directory.model.UserName;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.console.ConsolePermission;
|
||||
@@ -58,6 +59,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@@ -69,6 +71,7 @@ import javax.annotation.Nullable;
|
||||
public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
static final String PATH = "/console-api/users";
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final int PASSWORD_LENGTH = 16;
|
||||
|
||||
private final String registrarId;
|
||||
@@ -184,11 +187,20 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
|
||||
// User has no registrars assigned
|
||||
if (updatedUser.getUserRoles().getRegistrarRoles().isEmpty()) {
|
||||
try {
|
||||
directory.users().delete(email).execute();
|
||||
} catch (IOException e) {
|
||||
setFailedResponse("Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR);
|
||||
throw e;
|
||||
// Only delete the Workspace account if runCreate() provably minted it. Addresses that
|
||||
// reached this row via CLI create_user, OteAccountBuilder, or other means must survive
|
||||
if (isConsoleMintedAddress(email)) {
|
||||
try {
|
||||
directory.users().delete(email).execute();
|
||||
} catch (IOException e) {
|
||||
setFailedResponse(
|
||||
"Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
logger.atInfo().log(
|
||||
"Skipping Workspace deletion for %s because the console did not mint the account",
|
||||
email);
|
||||
}
|
||||
|
||||
VKey<User> key = VKey.create(User.class, email);
|
||||
@@ -368,6 +380,22 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* True iff {@code email} matches the exact shape produced by {@link #runCreate()} for the current
|
||||
* {@code registrarId}: {@code <3 alnum>.<registrarId>@<gSuiteDomainName>}.
|
||||
*
|
||||
* <p>Addresses of any other shape were provisioned outside this action and should not be passed
|
||||
* to {@code directory.users().delete()}.
|
||||
*/
|
||||
private boolean isConsoleMintedAddress(String email) {
|
||||
return email.endsWith("@" + gSuiteDomainName)
|
||||
&& Pattern.matches(
|
||||
String.format(
|
||||
"^[a-zA-Z0-9]{3}\\.%s@%s$",
|
||||
Pattern.quote(registrarId), Pattern.quote(gSuiteDomainName)),
|
||||
email);
|
||||
}
|
||||
|
||||
public record UserData(
|
||||
@Expose String emailAddress,
|
||||
@Expose String registryLockEmailAddress,
|
||||
|
||||
@@ -21,15 +21,12 @@
|
||||
-- "tld":"","tlds":["a.how"],"icannActivityReportField":"srs-dom-check"}
|
||||
|
||||
SELECT
|
||||
-- Remove quotation marks from tld fields.
|
||||
REGEXP_EXTRACT(tld, '^"(.*)"$') AS tld,
|
||||
tld,
|
||||
activityReportField AS metricName,
|
||||
COUNT(*) AS count
|
||||
FROM (
|
||||
SELECT
|
||||
-- TODO(b/32486667): Replace with JSON.parse() UDF when available for views
|
||||
SPLIT(
|
||||
REGEXP_EXTRACT(JSON_EXTRACT(json, '$.tlds'), r'^\[(.*)\]$')) AS tlds,
|
||||
JSON_EXTRACT_STRING_ARRAY(json, '$.tlds') AS tlds,
|
||||
JSON_EXTRACT_SCALAR(json,
|
||||
'$.resourceType') AS resourceType,
|
||||
JSON_EXTRACT_SCALAR(json,
|
||||
@@ -43,10 +40,10 @@ FROM (
|
||||
WHERE
|
||||
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
|
||||
AND _TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
|
||||
) AS regexes
|
||||
) AS json_parsed
|
||||
JOIN
|
||||
-- Unnest the JSON-parsed tlds.
|
||||
UNNEST(regexes.tlds) AS tld
|
||||
UNNEST(json_parsed.tlds) AS tld
|
||||
-- Exclude cases that can't be tabulated correctly, where activityReportField
|
||||
-- is null/empty, or TLD is null/empty despite being a domain flow.
|
||||
WHERE
|
||||
|
||||
@@ -184,16 +184,22 @@ class FlowReporterTest {
|
||||
|
||||
@Test
|
||||
void testRecordToLogs_metadata_invalidDomainName_stillGuessesTld() throws Exception {
|
||||
var maxLenTld = "a".repeat(63);
|
||||
var domainMaxLenTld = "a." + maxLenTld;
|
||||
var domainTldTooLong = "a." + maxLenTld + "b";
|
||||
when(flowReporter.eppInput.isDomainType()).thenReturn(true);
|
||||
when(flowReporter.eppInput.getSingleTargetId()).thenReturn(Optional.of("<foo@bar.com>"));
|
||||
when(flowReporter.eppInput.getTargetIds()).thenReturn(ImmutableList.of("<foo@bar.com>"));
|
||||
when(flowReporter.eppInput.getTargetIds())
|
||||
.thenReturn(ImmutableList.of("<foo@bar.com>", domainMaxLenTld, domainTldTooLong));
|
||||
flowReporter.recordToLogs();
|
||||
Map<String, Object> json =
|
||||
parseJsonMap(findFirstLogMessageByPrefix(handler, "FLOW-LOG-SIGNATURE-METADATA: "));
|
||||
assertThat(json).containsEntry("targetId", "<foo@bar.com>");
|
||||
assertThat(json).containsEntry("targetIds", ImmutableList.of("<foo@bar.com>"));
|
||||
assertThat(json).containsEntry("tld", "com>");
|
||||
assertThat(json).containsEntry("tlds", ImmutableList.of("com>"));
|
||||
assertThat(json)
|
||||
.containsEntry(
|
||||
"targetIds", ImmutableList.of("<foo@bar.com>", domainMaxLenTld, domainTldTooLong));
|
||||
assertThat(json).containsEntry("tld", "com-");
|
||||
assertThat(json).containsEntry("tlds", ImmutableList.of("com-", maxLenTld, maxLenTld + "..."));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -49,6 +49,8 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Ordering;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.config.RegistryConfigSettings;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException;
|
||||
import google.registry.flows.FlowUtils.NotLoggedInException;
|
||||
@@ -95,6 +97,7 @@ import google.registry.model.tld.Tld.TldState;
|
||||
import google.registry.model.tld.label.ReservedList;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.joda.money.Money;
|
||||
@@ -2447,6 +2450,283 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
|
||||
runFlowAssertResponse(outputXml);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFeeExtension_xapLabel_std_v1() throws Exception {
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(true)
|
||||
.build());
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
|
||||
RegistryConfigSettings settings = RegistryConfig.CONFIG_SETTINGS.get();
|
||||
BigDecimal originalInitialFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee.get("USD");
|
||||
BigDecimal originalFinalFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee.get("USD");
|
||||
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("110.00"));
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("110.00"));
|
||||
|
||||
try {
|
||||
clock.setTo(Instant.parse("2009-01-06T00:00:00Z"));
|
||||
Instant deletionTime = Instant.parse("2009-01-01T00:00:00Z");
|
||||
persistDeletedDomain("example1.tld", deletionTime);
|
||||
|
||||
setEppInput("domain_check_fee_stdv1.xml");
|
||||
runFlowAssertResponse(loadFile("domain_check_fee_xap_response.xml"));
|
||||
} finally {
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", originalInitialFee);
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", originalFinalFee);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_xapLabel_notOptedIn_returnsReserved() throws Exception {
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
Instant deletionTime = clock.now().minus(Duration.ofDays(5));
|
||||
persistDeletedDomain("example1.tld", deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(false, "example1.tld", "Reserved"),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_recentlyDeletedDomain_afterXapEnds_notOptedIn_returnsAvailable() throws Exception {
|
||||
// Once the 10-day XAP window expires after deletion (e.g. day 11), checking the domain should
|
||||
// return available (avail="1") without XAP fees or reservation blocks for non-opted-in
|
||||
// registrars.
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
Instant deletionTime = clock.now().minus(Duration.ofDays(11));
|
||||
persistDeletedDomain("example1.tld", deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(true, "example1.tld", null),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_recentlyDeletedDomain_afterXapEnds_optedIn_returnsAvailable() throws Exception {
|
||||
// Once the 10-day XAP window expires after deletion (e.g. day 11), checking the domain should
|
||||
// return available (avail="1") without XAP fees for opted-in registrars.
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(true)
|
||||
.build());
|
||||
Instant deletionTime = clock.now().minus(Duration.ofDays(11));
|
||||
persistDeletedDomain("example1.tld", deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(true, "example1.tld", null),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_recentlyDeletedDomain_beforeXapStarts_notOptedIn_returnsAvailable()
|
||||
throws Exception {
|
||||
// If a domain was recently deleted but XAP mode is currently disabled on the TLD, checking the
|
||||
// domain should return available (avail="1") without XAP fees or reservation blocks for
|
||||
// non-opted-in registrars.
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.DISABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
Instant deletionTime = clock.now().minus(Duration.ofDays(5));
|
||||
persistDeletedDomain("example1.tld", deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(true, "example1.tld", null),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_recentlyDeletedDomain_beforeXapStarts_optedIn_returnsAvailable() throws Exception {
|
||||
// If a domain was recently deleted but XAP mode is currently disabled on the TLD, checking the
|
||||
// domain should return available (avail="1") without XAP fees for opted-in registrars.
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.DISABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(true)
|
||||
.build());
|
||||
Instant deletionTime = clock.now().minus(Duration.ofDays(5));
|
||||
persistDeletedDomain("example1.tld", deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(true, "example1.tld", null),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_agpDeletedRegularDomain_duringXap_returnsAvailable() throws Exception {
|
||||
// A regular domain (registered without an XAP fee) deleted during its Add Grace Period (AGP) is
|
||||
// exempt from XAP, returning available (avail="1") without reservation blocks or XAP fees when
|
||||
// checked during active XAP on the TLD.
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(
|
||||
START_INSTANT,
|
||||
Tld.ExpiryAccessPeriodMode.DISABLED,
|
||||
clock.now().minus(Duration.ofHours(1)),
|
||||
Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
Instant creationTime = clock.now().minus(Duration.ofDays(2));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
DatabaseHelper.persistDomainAsDeleted(
|
||||
DatabaseHelper.newDomain("example1.tld")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(creationTime)
|
||||
.build(),
|
||||
deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(true, "example1.tld", null),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_agpDeletedXapDomain_duringXap_returnsAvailable() throws Exception {
|
||||
// A domain originally registered with an XAP fee deleted during its Add Grace Period (AGP) is
|
||||
// exempt from XAP, returning available (avail="1") without reservation blocks or XAP fees when
|
||||
// checked during active XAP on the TLD.
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
Instant creationTime = clock.now().minus(Duration.ofHours(2));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
DatabaseHelper.persistDomainAsDeleted(
|
||||
DatabaseHelper.newDomain("example1.tld")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(creationTime)
|
||||
.build(),
|
||||
deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(true, "example1.tld", null),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheck_anchorTenantDeletedAfterStandardAgp_duringXap_returnsReserved() throws Exception {
|
||||
// An anchor tenant domain deleted after standard AGP (5 days), e.g. on day 10 of its 30-day
|
||||
// anchor tenant AGP, is subject to XAP fees and reservation blocks (not exempt as an AGP
|
||||
// delete).
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setAddGracePeriodLength(Duration.ofDays(5))
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
Instant creationTime = clock.now().minus(Duration.ofDays(10));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
DatabaseHelper.persistDomainAsDeleted(
|
||||
DatabaseHelper.newDomain("example1.tld")
|
||||
.asBuilder()
|
||||
.setCreationTimeForTest(creationTime)
|
||||
.build(),
|
||||
deletionTime);
|
||||
setEppInput("domain_check_one_tld.xml");
|
||||
doCheckTest(
|
||||
create(false, "example1.tld", "Reserved"),
|
||||
create(true, "example2.tld", null),
|
||||
create(true, "example3.tld", null));
|
||||
}
|
||||
|
||||
static AllocationToken setUpDefaultToken() {
|
||||
return setUpDefaultToken("TheRegistrar");
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Ordering;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.config.RegistryConfigSettings;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.UnimplementedExtensionException;
|
||||
import google.registry.flows.EppRequestSource;
|
||||
@@ -113,6 +114,7 @@ import google.registry.flows.domain.DomainFlowUtils.FeeDescriptionMultipleMatche
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeeDescriptionParseException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredDuringEarlyAccessProgramException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredDuringExpiryAccessPeriodException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForPremiumNameException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.InvalidDsRecordException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.InvalidIdnDomainLabelException;
|
||||
@@ -2688,6 +2690,427 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
+ "during the Early Access Program. The EAP fee is: USD 100.00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_domainInXap_withoutFeeAck_throwsException() throws Exception {
|
||||
// Creating a domain during an active XAP tier without acknowledging the XAP fee in the EPP fee
|
||||
// extension should fail with FeesRequiredDuringExpiryAccessPeriodException.
|
||||
RegistryConfigSettings settings = RegistryConfig.CONFIG_SETTINGS.get();
|
||||
BigDecimal originalInitialFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee.get("USD");
|
||||
BigDecimal originalFinalFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee.get("USD");
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("100.00"));
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("100.00"));
|
||||
try {
|
||||
persistHosts();
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
setXapForTld("tld", deletionTime);
|
||||
Exception e =
|
||||
assertThrows(FeesRequiredDuringExpiryAccessPeriodException.class, this::runFlow);
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Fees must be explicitly acknowledged when creating domains "
|
||||
+ "during the Expiry Access Period. The XAP fee is: USD 100.00");
|
||||
} finally {
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", originalInitialFee);
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", originalFinalFee);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_domainInXap_registrarNotOptedIn_throwsDomainReservedException()
|
||||
throws Exception {
|
||||
// When XAP is enabled on the TLD and the domain is in its XAP window, a registrar that has not
|
||||
// opted into XAP cannot register the domain and should receive a DomainReservedException.
|
||||
persistHosts();
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationTime(clock.now().minus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
Exception e = assertThrows(DomainReservedException.class, this::runFlow);
|
||||
assertThat(e).hasMessageThat().isEqualTo("example.tld is a reserved domain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_domainInXap_optedIn_withFeeAck_chargesXapFee() throws Exception {
|
||||
// When an opted-in registrar acknowledges the XAP fee during an active XAP tier, domain
|
||||
// creation should succeed and persist a one-time XAP fee (Reason.FEE_EXPIRY_ACCESS) in
|
||||
// addition to the standard domain create fee (Reason.CREATE) and autorenew recurrence
|
||||
// (Reason.RENEW).
|
||||
RegistryConfigSettings settings = RegistryConfig.CONFIG_SETTINGS.get();
|
||||
BigDecimal originalInitialFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee.get("USD");
|
||||
BigDecimal originalFinalFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee.get("USD");
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("100.00"));
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("100.00"));
|
||||
try {
|
||||
persistHosts();
|
||||
Instant deletionTime = Instant.parse("1999-04-03T21:00:00.0Z");
|
||||
setXapForTld("tld", deletionTime);
|
||||
setEppInput(
|
||||
"domain_create_eap_fee.xml",
|
||||
new ImmutableMap.Builder<String, String>()
|
||||
.putAll(FEE_STD_1_0_MAP)
|
||||
.put("DESCRIPTION_1", "create")
|
||||
.put("DESCRIPTION_2", "Expiry Access Period")
|
||||
.build());
|
||||
runFlowAssertResponse(loadFile("domain_create_response_xap_fee.xml"));
|
||||
|
||||
Domain domain = reloadResourceByForeignKey();
|
||||
DomainHistory historyEntry = getHistoryEntries(domain, DomainHistory.class).get(0);
|
||||
assertBillingEvents(
|
||||
new BillingEvent.Builder()
|
||||
.setReason(Reason.CREATE)
|
||||
.setTargetId("example.tld")
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setPeriodYears(2)
|
||||
.setCost(Money.of(USD, 24))
|
||||
.setEventTime(clock.now())
|
||||
.setBillingTime(clock.now().plus(Tld.get("tld").getAddGracePeriodLength()))
|
||||
.setDomainHistory(historyEntry)
|
||||
.build(),
|
||||
new BillingEvent.Builder()
|
||||
.setReason(Reason.FEE_EXPIRY_ACCESS)
|
||||
.setTargetId("example.tld")
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setPeriodYears(1)
|
||||
.setCost(Money.of(USD, 100))
|
||||
.setEventTime(clock.now())
|
||||
.setBillingTime(clock.now().plus(Tld.get("tld").getAddGracePeriodLength()))
|
||||
.setDomainHistory(historyEntry)
|
||||
.build(),
|
||||
new BillingRecurrence.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setTargetId("example.tld")
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setEventTime(domain.getRegistrationExpirationTime())
|
||||
.setRecurrenceEndTime(END_INSTANT)
|
||||
.setDomainHistory(historyEntry)
|
||||
.build());
|
||||
} finally {
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", originalInitialFee);
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", originalFinalFee);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_premiumDomainInXap_optedIn_withFeeAck_chargesPremiumAndXapFees()
|
||||
throws Exception {
|
||||
// Creating a premium domain during active XAP should stack both fees, charging the premium
|
||||
// domain create fee (Reason.CREATE) plus the one-time XAP tier fee (Reason.FEE_EXPIRY_ACCESS).
|
||||
createTld("example");
|
||||
RegistryConfigSettings settings = RegistryConfig.CONFIG_SETTINGS.get();
|
||||
BigDecimal originalInitialFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee.get("USD");
|
||||
BigDecimal originalFinalFee =
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee.get("USD");
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("100.00"));
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", new BigDecimal("100.00"));
|
||||
try {
|
||||
persistHosts();
|
||||
setEppInput("domain_create_premium_xap.xml");
|
||||
Instant deletionTime = Instant.parse("1999-04-03T21:00:00.0Z");
|
||||
setXapForTld("example", deletionTime);
|
||||
runFlowAssertResponse(loadFile("domain_create_response_premium_xap.xml"));
|
||||
|
||||
Domain domain = reloadResourceByForeignKey();
|
||||
DomainHistory historyEntry = getHistoryEntries(domain, DomainHistory.class).get(0);
|
||||
assertBillingEvents(
|
||||
new BillingEvent.Builder()
|
||||
.setReason(Reason.CREATE)
|
||||
.setTargetId("rich.example")
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setPeriodYears(2)
|
||||
.setCost(Money.of(USD, 200))
|
||||
.setEventTime(clock.now())
|
||||
.setBillingTime(clock.now().plus(Tld.get("example").getAddGracePeriodLength()))
|
||||
.setDomainHistory(historyEntry)
|
||||
.build(),
|
||||
new BillingEvent.Builder()
|
||||
.setReason(Reason.FEE_EXPIRY_ACCESS)
|
||||
.setTargetId("rich.example")
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setPeriodYears(1)
|
||||
.setCost(Money.of(USD, 100))
|
||||
.setEventTime(clock.now())
|
||||
.setBillingTime(clock.now().plus(Tld.get("example").getAddGracePeriodLength()))
|
||||
.setDomainHistory(historyEntry)
|
||||
.build(),
|
||||
new BillingRecurrence.Builder()
|
||||
.setReason(Reason.RENEW)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setTargetId("rich.example")
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setEventTime(domain.getRegistrationExpirationTime())
|
||||
.setRecurrenceEndTime(END_INSTANT)
|
||||
.setDomainHistory(historyEntry)
|
||||
.build());
|
||||
} finally {
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.initialFee =
|
||||
ImmutableMap.of("USD", originalInitialFee);
|
||||
settings.registryPolicy.domainExpiryAccessPeriod.finalFee =
|
||||
ImmutableMap.of("USD", originalFinalFee);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_recentlyDeletedDomain_afterXapEnds_notOptedIn_succeedsWithoutXapFee()
|
||||
throws Exception {
|
||||
// Once the 10-day XAP window expires after deletion (e.g. day 11), the domain returns to
|
||||
// standard pricing and can be registered without XAP fees or reservation blocks by
|
||||
// non-opted-in registrars.
|
||||
persistHosts();
|
||||
Instant deletionTime = clock.now().minus(Duration.ofDays(11));
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationTime(clock.now().minus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_recentlyDeletedDomain_afterXapEnds_optedIn_succeedsWithoutXapFee()
|
||||
throws Exception {
|
||||
// Once the 10-day XAP window expires after deletion (e.g. day 11), the domain returns to
|
||||
// standard pricing and can be registered without XAP fees by opted-in registrars.
|
||||
persistHosts();
|
||||
Instant deletionTime = clock.now().minus(Duration.ofDays(11));
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(true)
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationTime(clock.now().minus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_recentlyDeletedDomain_beforeXapStarts_notOptedIn_succeedsWithoutXapFee()
|
||||
throws Exception {
|
||||
// If a domain was recently deleted but XAP mode is currently disabled on the TLD, the domain
|
||||
// can be registered at standard pricing without XAP fees or reservation blocks by
|
||||
// non-opted-in registrars.
|
||||
persistHosts();
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.DISABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationTime(clock.now().minus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_recentlyDeletedDomain_beforeXapStarts_optedIn_succeedsWithoutXapFee()
|
||||
throws Exception {
|
||||
// If a domain was recently deleted but XAP mode is currently disabled on the TLD, the domain
|
||||
// can be registered at standard pricing without XAP fees by opted-in registrars.
|
||||
persistHosts();
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.DISABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(true)
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationTime(clock.now().minus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_agpDeletedRegularDomain_duringXap_succeedsWithoutXapFee() throws Exception {
|
||||
// A regular domain (registered without an XAP fee) that was deleted during its Add Grace
|
||||
// Period (AGP) is exempt from XAP, succeeding at standard pricing without reservation blocks
|
||||
// or XAP fees when re-registered during active XAP on the TLD.
|
||||
persistHosts();
|
||||
Instant creationTime = clock.now().minus(Duration.ofDays(2));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(
|
||||
START_INSTANT,
|
||||
Tld.ExpiryAccessPeriodMode.DISABLED,
|
||||
clock.now().minus(Duration.ofHours(1)),
|
||||
Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setCreationTime(creationTime)
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_agpDeletedXapDomain_duringXap_succeedsWithoutXapFee() throws Exception {
|
||||
// A domain originally registered with an XAP fee that was deleted during its Add Grace
|
||||
// Period (AGP) is exempt from XAP upon subsequent re-registration, succeeding at standard
|
||||
// pricing without reservation blocks or XAP fees when re-registered during active XAP on the
|
||||
// TLD.
|
||||
persistHosts();
|
||||
Instant creationTime = clock.now().minus(Duration.ofHours(2));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(false)
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setCreationTime(creationTime)
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
private void setXapForTld(String tldStr, Instant deletionTime) throws Exception {
|
||||
persistResource(
|
||||
Registrar.loadByRegistrarId("TheRegistrar")
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodEnabled(true)
|
||||
.build());
|
||||
persistResource(
|
||||
Tld.get(tldStr)
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
persistResource(
|
||||
new Domain.Builder()
|
||||
.setDomainName(getUniqueIdFromCommand())
|
||||
.setDeletionTime(deletionTime)
|
||||
.setRegistrationExpirationTime(clock.now().plus(Duration.ofDays(365)))
|
||||
.setCreationTime(clock.now().minus(Duration.ofDays(365)))
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("9999-EXAMPLE")
|
||||
.build());
|
||||
}
|
||||
|
||||
private void setEapForTld(String tld) {
|
||||
persistResource(
|
||||
Tld.get(tld)
|
||||
|
||||
@@ -402,6 +402,36 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
|
||||
GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_STD_1_0_MAP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_addGracePeriodDelete_doesNotRefundXapFee() throws Exception {
|
||||
setUpSuccessfulTest();
|
||||
BillingEvent createBillingEvent =
|
||||
persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 123)));
|
||||
BillingEvent xapBillingEvent =
|
||||
persistResource(createBillingEvent(Reason.FEE_EXPIRY_ACCESS, Money.of(USD, 100)));
|
||||
setUpGracePeriods(
|
||||
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, domain.getRepoId(), createBillingEvent));
|
||||
assertPollMessages(createAutorenewPollMessage("TheRegistrar").build());
|
||||
clock.advanceOneMilli();
|
||||
runFlowAssertResponse(loadFile("domain_delete_response_fee.xml", FEE_STD_1_0_MAP));
|
||||
assertThat(reloadResourceByForeignKey()).isNull();
|
||||
DomainHistory historyEntryDomainDelete =
|
||||
getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE, DomainHistory.class);
|
||||
assertBillingEvents(
|
||||
createAutorenewBillingEvent("TheRegistrar").setRecurrenceEndTime(clock.now()).build(),
|
||||
createBillingEvent,
|
||||
xapBillingEvent,
|
||||
new BillingCancellation.Builder()
|
||||
.setReason(Reason.CREATE)
|
||||
.setTargetId("example.tld")
|
||||
.setRegistrarId("TheRegistrar")
|
||||
.setEventTime(clock.now())
|
||||
.setBillingTime(plusDays(TIME_BEFORE_FLOW, 1))
|
||||
.setBillingEvent(createBillingEvent.createVKey())
|
||||
.setDomainHistory(historyEntryDomainDelete)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void doSuccessfulTest_noAddGracePeriod(String responseFilename) throws Exception {
|
||||
doSuccessfulTest_noAddGracePeriod(responseFilename, ImmutableMap.of());
|
||||
}
|
||||
|
||||
@@ -30,12 +30,15 @@ import static google.registry.util.DateTimeUtils.END_INSTANT;
|
||||
import static google.registry.util.DateTimeUtils.START_INSTANT;
|
||||
import static google.registry.util.DateTimeUtils.minusHours;
|
||||
import static google.registry.util.DateTimeUtils.plusHours;
|
||||
import static org.joda.money.CurrencyUnit.JPY;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Range;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.HttpSessionMetadata;
|
||||
import google.registry.flows.SessionMetadata;
|
||||
@@ -58,8 +61,10 @@ import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeHttpSession;
|
||||
import google.registry.util.Clock;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -85,7 +90,12 @@ public class DomainPricingLogicTest {
|
||||
createTld("example");
|
||||
sessionMetadata = new HttpSessionMetadata(new FakeHttpSession());
|
||||
domainPricingLogic =
|
||||
new DomainPricingLogic(new DomainPricingCustomLogic(eppInput, sessionMetadata, null));
|
||||
new DomainPricingLogic(
|
||||
new DomainPricingCustomLogic(eppInput, sessionMetadata, null),
|
||||
Duration.ofDays(10),
|
||||
Duration.ofHours(1),
|
||||
ImmutableMap.of(USD, new BigDecimal("100000.00"), JPY, new BigDecimal("10000000")),
|
||||
ImmutableMap.of(USD, new BigDecimal("10.00"), JPY, new BigDecimal("1000")));
|
||||
tld =
|
||||
persistResource(
|
||||
Tld.get("example")
|
||||
@@ -146,7 +156,14 @@ public class DomainPricingLogicTest {
|
||||
persistResource(Tld.get("sunrise").asBuilder().setTldStateTransitions(transitions).build());
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
sunriseTld, "domain.sunrise", clock.now(), 2, false, true, Optional.empty()))
|
||||
sunriseTld,
|
||||
"domain.sunrise",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
Optional.empty()))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -170,7 +187,14 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "default.example", clock.now(), 1, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"default.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -195,7 +219,14 @@ public class DomainPricingLogicTest {
|
||||
// 3 year create should be 5 (discount price) + 10*2 (regular price) = 25.
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "default.example", clock.now(), 3, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"default.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -218,7 +249,14 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "default.example", clock.now(), 1, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"default.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -1006,7 +1044,14 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "premium.example", clock.now(), 1, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -1015,7 +1060,14 @@ public class DomainPricingLogicTest {
|
||||
// Two-year create should be 13 (standard price) + 100 (premium price), and it's premium
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "premium.example", clock.now(), 2, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -1051,7 +1103,14 @@ public class DomainPricingLogicTest {
|
||||
// are standard
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "premium.example", clock.now(), 2, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -1060,7 +1119,14 @@ public class DomainPricingLogicTest {
|
||||
// Similarly, 3 years should be 13 + 10 + 10
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "premium.example", clock.now(), 3, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -1081,7 +1147,14 @@ public class DomainPricingLogicTest {
|
||||
// Two-year create should be 100 (premium 1st year) plus 10 (nonpremium 2nd year)
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "premium.example", clock.now(), 2, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -1090,7 +1163,14 @@ public class DomainPricingLogicTest {
|
||||
// Similarly, 3 years should be 100 + 10 + 10
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "premium.example", clock.now(), 3, false, false, Optional.of(allocationToken)))
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.now(),
|
||||
Optional.empty(),
|
||||
3,
|
||||
false,
|
||||
false,
|
||||
Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -1135,4 +1215,291 @@ public class DomainPricingLogicTest {
|
||||
.getRenewCost())
|
||||
.isEqualTo(Money.of(USD, 25));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetXapFeeFor_tier0_startOfXap() {
|
||||
Instant deletionTime = clock.now();
|
||||
Instant checkTime = deletionTime.plus(Duration.ofMinutes(30));
|
||||
Optional<Fee> xapFee = domainPricingLogic.getXapFeeFor(checkTime, deletionTime, USD);
|
||||
assertThat(xapFee)
|
||||
.hasValue(
|
||||
Fee.create(
|
||||
new BigDecimal("100000.00"),
|
||||
Fee.FeeType.XAP,
|
||||
false,
|
||||
Range.closedOpen(deletionTime, deletionTime.plus(Duration.ofHours(1))),
|
||||
deletionTime.plus(Duration.ofHours(1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetXapFeeFor_tier1() {
|
||||
Instant deletionTime = clock.now();
|
||||
Instant checkTime = deletionTime.plus(Duration.ofHours(1)).plus(Duration.ofMinutes(15));
|
||||
Optional<Fee> xapFee = domainPricingLogic.getXapFeeFor(checkTime, deletionTime, USD);
|
||||
assertThat(xapFee)
|
||||
.hasValue(
|
||||
Fee.create(
|
||||
new BigDecimal("96219.61"),
|
||||
Fee.FeeType.XAP,
|
||||
false,
|
||||
Range.closedOpen(
|
||||
deletionTime.plus(Duration.ofHours(1)), deletionTime.plus(Duration.ofHours(2))),
|
||||
deletionTime.plus(Duration.ofHours(2))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetXapFeeFor_tier239_finalTier() {
|
||||
Instant deletionTime = clock.now();
|
||||
Instant checkTime = deletionTime.plus(Duration.ofHours(239)).plus(Duration.ofMinutes(30));
|
||||
Optional<Fee> xapFee = domainPricingLogic.getXapFeeFor(checkTime, deletionTime, USD);
|
||||
assertThat(xapFee)
|
||||
.hasValue(
|
||||
Fee.create(
|
||||
new BigDecimal("10.00"),
|
||||
Fee.FeeType.XAP,
|
||||
false,
|
||||
Range.closedOpen(
|
||||
deletionTime.plus(Duration.ofHours(239)),
|
||||
deletionTime.plus(Duration.ofHours(240))),
|
||||
deletionTime.plus(Duration.ofHours(240))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetXapFeeFor_unconfiguredCurrency() {
|
||||
Instant deletionTime = clock.now();
|
||||
Instant checkTime = deletionTime.plus(Duration.ofMinutes(30));
|
||||
Optional<Fee> xapFee =
|
||||
domainPricingLogic.getXapFeeFor(checkTime, deletionTime, CurrencyUnit.EUR);
|
||||
assertThat(xapFee).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatePrice_xapEnabled_includesXapFeeAndRequiresExtension() throws Exception {
|
||||
Tld xapTld =
|
||||
persistResource(
|
||||
tld.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
Domain deletedDomain =
|
||||
new Domain.Builder()
|
||||
.setDomainName("deleted.example")
|
||||
.setDeletionTime(deletionTime)
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("2-EXAMPLE")
|
||||
.build();
|
||||
FeesAndCredits feesAndCredits =
|
||||
domainPricingLogic.getCreatePrice(
|
||||
xapTld,
|
||||
"deleted.example",
|
||||
clock.now(),
|
||||
Optional.of(deletedDomain),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.empty());
|
||||
assertThat(feesAndCredits.isFeeExtensionRequired()).isTrue();
|
||||
assertThat(feesAndCredits.getXapCost()).isEqualTo(Money.of(USD, new BigDecimal("96219.61")));
|
||||
assertThat(feesAndCredits.getTotalCost()).isEqualTo(Money.of(USD, new BigDecimal("96232.61")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetXapFeeFor_afterXapEnds_returnsEmpty() {
|
||||
Instant deletionTime = clock.now();
|
||||
Instant checkTime = deletionTime.plus(Duration.ofDays(10)).plus(Duration.ofMinutes(1));
|
||||
Optional<Fee> xapFee = domainPricingLogic.getXapFeeFor(checkTime, deletionTime, USD);
|
||||
assertThat(xapFee).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatePrice_agpDeletedRegularDomain_duringXap_noXapFee() throws Exception {
|
||||
// A regular domain deleted during AGP is exempt from XAP fee evaluation upon subsequent
|
||||
// creation during active XAP.
|
||||
Tld xapTld =
|
||||
persistResource(
|
||||
tld.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(
|
||||
START_INSTANT,
|
||||
Tld.ExpiryAccessPeriodMode.DISABLED,
|
||||
clock.now().minus(Duration.ofHours(1)),
|
||||
Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
Instant creationTime = clock.now().minus(Duration.ofDays(2));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
Domain deletedDomain =
|
||||
new Domain.Builder()
|
||||
.setDomainName("deleted.example")
|
||||
.setCreationTime(creationTime)
|
||||
.setDeletionTime(deletionTime)
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("2-EXAMPLE")
|
||||
.build();
|
||||
FeesAndCredits feesAndCredits =
|
||||
domainPricingLogic.getCreatePrice(
|
||||
xapTld,
|
||||
"deleted.example",
|
||||
clock.now(),
|
||||
Optional.of(deletedDomain),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.empty());
|
||||
assertThat(feesAndCredits.isFeeExtensionRequired()).isFalse();
|
||||
assertThat(feesAndCredits.getXapCost()).isEqualTo(Money.zero(USD));
|
||||
assertThat(feesAndCredits.getTotalCost()).isEqualTo(Money.of(USD, new BigDecimal("13.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatePrice_agpDeletedXapDomain_duringXap_noXapFee() throws Exception {
|
||||
// A domain originally registered with an XAP fee deleted during AGP is exempt from XAP fee
|
||||
// evaluation upon subsequent creation during active XAP.
|
||||
Tld xapTld =
|
||||
persistResource(
|
||||
tld.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
Instant creationTime = clock.now().minus(Duration.ofHours(2));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
Domain deletedDomain =
|
||||
new Domain.Builder()
|
||||
.setDomainName("deleted.example")
|
||||
.setCreationTime(creationTime)
|
||||
.setDeletionTime(deletionTime)
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("2-EXAMPLE")
|
||||
.build();
|
||||
FeesAndCredits feesAndCredits =
|
||||
domainPricingLogic.getCreatePrice(
|
||||
xapTld,
|
||||
"deleted.example",
|
||||
clock.now(),
|
||||
Optional.of(deletedDomain),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.empty());
|
||||
assertThat(feesAndCredits.isFeeExtensionRequired()).isFalse();
|
||||
assertThat(feesAndCredits.getXapCost()).isEqualTo(Money.zero(USD));
|
||||
assertThat(feesAndCredits.getTotalCost()).isEqualTo(Money.of(USD, new BigDecimal("13.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatePrice_anchorTenantDeletedAfterStandardAgp_duringXap_chargesXapFee()
|
||||
throws Exception {
|
||||
// An anchor tenant domain deleted after standard AGP (5 days), e.g. on day 10 of its 30-day
|
||||
// anchor tenant AGP, is subject to XAP fees upon re-registration (not exempt as an AGP delete).
|
||||
Tld xapTld =
|
||||
persistResource(
|
||||
tld.asBuilder()
|
||||
.setAddGracePeriodLength(Duration.ofDays(5))
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
Instant creationTime = clock.now().minus(Duration.ofDays(10));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
Domain deletedDomain =
|
||||
new Domain.Builder()
|
||||
.setDomainName("deleted.example")
|
||||
.setCreationTime(creationTime)
|
||||
.setDeletionTime(deletionTime)
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("2-EXAMPLE")
|
||||
.build();
|
||||
FeesAndCredits feesAndCredits =
|
||||
domainPricingLogic.getCreatePrice(
|
||||
xapTld,
|
||||
"deleted.example",
|
||||
clock.now(),
|
||||
Optional.of(deletedDomain),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.empty());
|
||||
assertThat(feesAndCredits.isFeeExtensionRequired()).isTrue();
|
||||
assertThat(feesAndCredits.getXapCost()).isEqualTo(Money.of(USD, new BigDecimal("96219.61")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatePrice_premiumDomainInXap_chargesPremiumAndXapFees() throws Exception {
|
||||
// Calculating create price for a recently deleted premium domain during active XAP should sum
|
||||
// both the premium create fee and the one-time XAP tier fee.
|
||||
Tld xapTld =
|
||||
persistResource(
|
||||
tld.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
Domain deletedDomain =
|
||||
new Domain.Builder()
|
||||
.setDomainName("premium.example")
|
||||
.setDeletionTime(deletionTime)
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("2-EXAMPLE")
|
||||
.build();
|
||||
FeesAndCredits feesAndCredits =
|
||||
domainPricingLogic.getCreatePrice(
|
||||
xapTld,
|
||||
"premium.example",
|
||||
clock.now(),
|
||||
Optional.of(deletedDomain),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.empty());
|
||||
assertThat(feesAndCredits.hasAnyPremiumFees()).isTrue();
|
||||
assertThat(feesAndCredits.isFeeExtensionRequired()).isTrue();
|
||||
assertThat(feesAndCredits.getCreateCost()).isEqualTo(Money.of(USD, new BigDecimal("100.00")));
|
||||
assertThat(feesAndCredits.getXapCost()).isEqualTo(Money.of(USD, new BigDecimal("96219.61")));
|
||||
assertThat(feesAndCredits.getTotalCost()).isEqualTo(Money.of(USD, new BigDecimal("96319.61")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatePrice_zeroXapFee_doesNotRequireExtension() throws Exception {
|
||||
// When an XAP tier fee is $0.00, it is included in the fee items but does not require a fee
|
||||
// extension acknowledgment from the registrar.
|
||||
Tld xapTld =
|
||||
persistResource(
|
||||
tld.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Tld.ExpiryAccessPeriodMode.ENABLED))
|
||||
.build());
|
||||
DomainPricingLogic zeroFeePricingLogic =
|
||||
new DomainPricingLogic(
|
||||
new DomainPricingCustomLogic(eppInput, sessionMetadata, null),
|
||||
Duration.ofDays(10),
|
||||
Duration.ofHours(1),
|
||||
ImmutableMap.of(USD, BigDecimal.ZERO),
|
||||
ImmutableMap.of(USD, BigDecimal.ZERO));
|
||||
Instant deletionTime = clock.now().minus(Duration.ofHours(1));
|
||||
Domain deletedDomain =
|
||||
new Domain.Builder()
|
||||
.setDomainName("deleted.example")
|
||||
.setDeletionTime(deletionTime)
|
||||
.setCreationRegistrarId("TheRegistrar")
|
||||
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
|
||||
.setRepoId("2-EXAMPLE")
|
||||
.build();
|
||||
FeesAndCredits feesAndCredits =
|
||||
zeroFeePricingLogic.getCreatePrice(
|
||||
xapTld,
|
||||
"deleted.example",
|
||||
clock.now(),
|
||||
Optional.of(deletedDomain),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Optional.empty());
|
||||
assertThat(feesAndCredits.isFeeExtensionRequired()).isFalse();
|
||||
assertThat(feesAndCredits.getXapCost()).isEqualTo(Money.zero(USD));
|
||||
assertThat(feesAndCredits.getTotalCost()).isEqualTo(Money.of(USD, new BigDecimal("13.00")));
|
||||
}
|
||||
}
|
||||
|
||||
+94
-44
@@ -22,6 +22,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenStatus.VAL
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
|
||||
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.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
@@ -36,6 +37,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
@@ -44,6 +46,7 @@ import google.registry.flows.custom.DomainPricingCustomLogic;
|
||||
import google.registry.flows.domain.DomainPricingLogic;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
|
||||
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
@@ -54,6 +57,8 @@ import google.registry.model.tld.Tld;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TestCacheExtension;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -69,11 +74,20 @@ class AllocationTokenFlowUtilsTest {
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
|
||||
|
||||
@RegisterExtension
|
||||
final TestCacheExtension testCacheExtension =
|
||||
new TestCacheExtension.Builder().withAllocationTokenCache(Duration.ofMinutes(10)).build();
|
||||
|
||||
private final AllocationTokenExtension allocationTokenExtension =
|
||||
mock(AllocationTokenExtension.class);
|
||||
|
||||
private final DomainPricingLogic domainPricingLogic =
|
||||
new DomainPricingLogic(new DomainPricingCustomLogic(null, null, null));
|
||||
new DomainPricingLogic(
|
||||
new DomainPricingCustomLogic(null, null, null),
|
||||
Duration.ofDays(30),
|
||||
Duration.ofDays(1),
|
||||
ImmutableMap.of(),
|
||||
ImmutableMap.of());
|
||||
|
||||
private Tld tld;
|
||||
|
||||
@@ -124,8 +138,13 @@ class AllocationTokenFlowUtilsTest {
|
||||
.build());
|
||||
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
|
||||
assertThat(
|
||||
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
|
||||
"TheRegistrar", "example.tld", clock.now(), Optional.of(allocationTokenExtension)))
|
||||
tm().transact(
|
||||
() ->
|
||||
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
|
||||
"TheRegistrar",
|
||||
"example.tld",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension))))
|
||||
.hasValue(token);
|
||||
}
|
||||
|
||||
@@ -141,15 +160,17 @@ class AllocationTokenFlowUtilsTest {
|
||||
.build());
|
||||
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
|
||||
assertThat(
|
||||
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
|
||||
"TheRegistrar",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension),
|
||||
tld,
|
||||
"example.tld",
|
||||
CommandName.CREATE,
|
||||
Optional.of(1),
|
||||
domainPricingLogic))
|
||||
tm().transact(
|
||||
() ->
|
||||
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
|
||||
"TheRegistrar",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension),
|
||||
tld,
|
||||
"example.tld",
|
||||
CommandName.CREATE,
|
||||
Optional.of(1),
|
||||
domainPricingLogic)))
|
||||
.hasValue(token);
|
||||
}
|
||||
|
||||
@@ -181,15 +202,17 @@ class AllocationTokenFlowUtilsTest {
|
||||
.build());
|
||||
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
|
||||
assertThat(
|
||||
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
|
||||
"TheRegistrar",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension),
|
||||
tld,
|
||||
"example.tld",
|
||||
CommandName.CREATE,
|
||||
Optional.of(1),
|
||||
domainPricingLogic))
|
||||
tm().transact(
|
||||
() ->
|
||||
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
|
||||
"TheRegistrar",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension),
|
||||
tld,
|
||||
"example.tld",
|
||||
CommandName.CREATE,
|
||||
Optional.of(1),
|
||||
domainPricingLogic)))
|
||||
.hasValue(defaultToken);
|
||||
}
|
||||
|
||||
@@ -261,6 +284,29 @@ class AllocationTokenFlowUtilsTest {
|
||||
assertLoadTokenFromExtensionThrowsException(NonexistentAllocationTokenException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_loadFromExtension_alreadyRedeemedToken() {
|
||||
persistResource(
|
||||
singleUseTokenBuilder().setRedemptionHistoryId(new HistoryEntryId("repoId", 1L)).build());
|
||||
assertLoadTokenFromExtensionThrowsException(AlreadyRedeemedAllocationTokenException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_loadFromExtension_singleUseTokenStaleCacheReloadsFromDatabase() {
|
||||
AllocationToken token = persistResource(singleUseTokenBuilder().build());
|
||||
assertThat(AllocationToken.get(token.createVKey())).hasValue(token);
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().put(
|
||||
token
|
||||
.asBuilder()
|
||||
.setRedemptionHistoryId(new HistoryEntryId("repoId", 1L))
|
||||
.build()));
|
||||
// cache still returns the old un-redeemed token
|
||||
assertThat(AllocationToken.get(token.createVKey()).get().isRedeemed()).isFalse();
|
||||
assertLoadTokenFromExtensionThrowsException(AlreadyRedeemedAllocationTokenException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_tokenInvalidForRegistrar() {
|
||||
persistResource(
|
||||
@@ -334,18 +380,20 @@ class AllocationTokenFlowUtilsTest {
|
||||
// Tokens tied to a domain should throw a catastrophic exception if used for a different domain
|
||||
persistResource(singleUseTokenBuilder().setDomainName("someotherdomain.tld").build());
|
||||
when(allocationTokenExtension.getAllocationToken()).thenReturn("tokeN");
|
||||
assertThrows(
|
||||
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
|
||||
() ->
|
||||
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
|
||||
"TheRegistrar",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension),
|
||||
tld,
|
||||
"example.tld",
|
||||
CommandName.CREATE,
|
||||
Optional.of(1),
|
||||
domainPricingLogic));
|
||||
tm().transact(
|
||||
() ->
|
||||
assertThrows(
|
||||
AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException.class,
|
||||
() ->
|
||||
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
|
||||
"TheRegistrar",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension),
|
||||
tld,
|
||||
"example.tld",
|
||||
CommandName.CREATE,
|
||||
Optional.of(1),
|
||||
domainPricingLogic)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -451,17 +499,19 @@ class AllocationTokenFlowUtilsTest {
|
||||
}
|
||||
|
||||
private void assertLoadTokenFromExtensionThrowsException(Class<? extends EppException> clazz) {
|
||||
assertAboutEppExceptions()
|
||||
.that(
|
||||
assertThrows(
|
||||
clazz,
|
||||
() ->
|
||||
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
|
||||
"TheRegistrar",
|
||||
"example.tld",
|
||||
clock.now(),
|
||||
Optional.of(allocationTokenExtension))))
|
||||
.marshalsToXml();
|
||||
tm().transact(
|
||||
() ->
|
||||
assertAboutEppExceptions()
|
||||
.that(
|
||||
assertThrows(
|
||||
clazz,
|
||||
() ->
|
||||
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
|
||||
"TheRegistrar",
|
||||
"example.tld",
|
||||
tm().getTxTime(),
|
||||
Optional.of(allocationTokenExtension))))
|
||||
.marshalsToXml());
|
||||
}
|
||||
|
||||
private AllocationToken.Builder singleUseTokenBuilder() {
|
||||
|
||||
@@ -144,4 +144,17 @@ class ForeignKeyUtilsTest {
|
||||
fakeClock.now()))
|
||||
.containsExactlyEntriesIn(ImmutableMap.of("ns1.example.com", host1.createVKey()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_loadResourcesByCache_skipsDeletedAndNonexistent() {
|
||||
Host host1 = persistActiveHost("ns1.example.com");
|
||||
Host host2 = persistActiveHost("ns2.example.com");
|
||||
persistResource(host2.asBuilder().setDeletionTime(minusDays(fakeClock.now(), 1)).build());
|
||||
assertThat(
|
||||
ForeignKeyUtils.loadResourcesByCache(
|
||||
Host.class,
|
||||
ImmutableList.of("ns1.example.com", "ns2.example.com", "ns3.example.com"),
|
||||
fakeClock.now()))
|
||||
.containsExactlyEntriesIn(ImmutableMap.of("ns1.example.com", host1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,4 +773,20 @@ class RegistrarTest extends EntityTestCase {
|
||||
assertThat(Registrar.loadByRegistrarId("registrar").toString())
|
||||
.contains("allowedTlds=[bar, baz, foo, gon, tri]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_expiryAccessPeriodEnabled() {
|
||||
assertThat(registrar.getExpiryAccessPeriodEnabled()).isFalse();
|
||||
persistResource(registrar.asBuilder().setExpiryAccessPeriodEnabled(true).build());
|
||||
assertThat(
|
||||
Registrar.loadByRegistrarId("registrar").orElseThrow().getExpiryAccessPeriodEnabled())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_toJsonMap_includesExpiryAccessPeriodEnabled() {
|
||||
assertThat(registrar.toJsonMap()).containsEntry("expiryAccessPeriodEnabled", false);
|
||||
Registrar modified = registrar.asBuilder().setExpiryAccessPeriodEnabled(true).build();
|
||||
assertThat(modified.toJsonMap()).containsEntry("expiryAccessPeriodEnabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,6 +746,37 @@ public final class TldTest extends EntityTestCase {
|
||||
.isEqualTo(new BigDecimal("50.00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExpiryAccessPeriodMode_undefined() {
|
||||
assertThat(Tld.get("tld").getExpiryAccessPeriodModeAt(fakeClock.now()))
|
||||
.isEqualTo(Tld.ExpiryAccessPeriodMode.DISABLED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExpiryAccessPeriodMode_specified() {
|
||||
Instant a = minusDays(fakeClock.now(), 1);
|
||||
Instant b = plusDays(fakeClock.now(), 1);
|
||||
Tld tld =
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setExpiryAccessPeriodTransitions(
|
||||
ImmutableSortedMap.of(
|
||||
START_INSTANT,
|
||||
Tld.ExpiryAccessPeriodMode.DISABLED,
|
||||
a,
|
||||
Tld.ExpiryAccessPeriodMode.ENABLED,
|
||||
b,
|
||||
Tld.ExpiryAccessPeriodMode.DISABLED))
|
||||
.build();
|
||||
|
||||
assertThat(tld.getExpiryAccessPeriodModeAt(fakeClock.now()))
|
||||
.isEqualTo(Tld.ExpiryAccessPeriodMode.ENABLED);
|
||||
assertThat(tld.getExpiryAccessPeriodModeAt(minusDays(fakeClock.now(), 2)))
|
||||
.isEqualTo(Tld.ExpiryAccessPeriodMode.DISABLED);
|
||||
assertThat(tld.getExpiryAccessPeriodModeAt(plusDays(fakeClock.now(), 2)))
|
||||
.isEqualTo(Tld.ExpiryAccessPeriodMode.DISABLED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_eapFee_wrongCurrency() {
|
||||
IllegalArgumentException thrown =
|
||||
|
||||
@@ -25,6 +25,7 @@ import static google.registry.util.DateTimeUtils.minusMonths;
|
||||
import static google.registry.util.DateTimeUtils.minusYears;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.rdap.RdapMetrics.EndpointType;
|
||||
import google.registry.rdap.RdapMetrics.SearchType;
|
||||
@@ -111,13 +112,16 @@ class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameserverActi
|
||||
@Test
|
||||
void testNameserver_tldTithHyphenOn3And4_works() {
|
||||
createTld("zz--main-2166");
|
||||
persistResource(makePunycodedHost("ns1.cat.zz--main-2166", "1.2.3.4", null, "TheRegistrar"));
|
||||
Host host =
|
||||
persistResource(
|
||||
makePunycodedHost("ns1.cat.zz--main-2166", "1.2.3.4", null, "TheRegistrar"));
|
||||
assertAboutJson()
|
||||
.that(generateActualJson("ns1.cat.zz--main-2166"))
|
||||
.isEqualTo(
|
||||
addPermanentBoilerplateNotices(
|
||||
jsonFileBuilder()
|
||||
.addNameserver("ns1.cat.zz--main-2166", "ns1.cat.zz--main-2166", "F-ROID")
|
||||
.addNameserver(
|
||||
"ns1.cat.zz--main-2166", "ns1.cat.zz--main-2166", host.getRepoId())
|
||||
.putAll("ADDRESSTYPE", "v4", "ADDRESS", "1.2.3.4", "STATUS", "active")
|
||||
.load("rdap_host.json")));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
@@ -29,6 +29,7 @@ import static google.registry.util.DateTimeUtils.minusDays;
|
||||
import static google.registry.util.DateTimeUtils.minusMonths;
|
||||
import static google.registry.util.DateTimeUtils.minusYears;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
@@ -46,6 +47,7 @@ import google.registry.rdap.RdapSearchResults.IncompletenessWarningType;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.FullFieldsTestEntityHelper;
|
||||
import java.net.URLDecoder;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -428,7 +430,7 @@ class RdapNameserverSearchActionTest extends RdapSearchActionTestCase<RdapNamese
|
||||
action.registrarParam = Optional.of("unicoderegistrar");
|
||||
generateActualJsonWithName("ns*.cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
verifyErrorMetrics(Optional.of(2L), 404);
|
||||
verifyErrorMetrics(Optional.of(0L), 404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -445,6 +447,30 @@ class RdapNameserverSearchActionTest extends RdapSearchActionTestCase<RdapNamese
|
||||
verifyMetrics(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNameMatch_star_cat_lol_usesForeignKeyCache() {
|
||||
generateActualJsonWithName("*.cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
verifyMetrics(2);
|
||||
clearInvocations(rdapMetrics);
|
||||
|
||||
Instant newTransferTime = clock.now();
|
||||
persistResource(hostNs1CatLol.asBuilder().setLastTransferTime(newTransferTime).build());
|
||||
clock.advanceOneMilli();
|
||||
|
||||
action.response = new FakeResponse();
|
||||
generateActualJsonWithName("*.cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
verifyMetrics(2);
|
||||
|
||||
JsonObject searchResults =
|
||||
parseJsonObject(response.getPayload())
|
||||
.getAsJsonArray("nameserverSearchResults")
|
||||
.get(0)
|
||||
.getAsJsonObject();
|
||||
assertThat(searchResults.toString()).doesNotContain(newTransferTime.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNameMatch_star_cat_lol_found_sameRegistrarRequested() {
|
||||
action.registrarParam = Optional.of("TheRegistrar");
|
||||
@@ -458,7 +484,7 @@ class RdapNameserverSearchActionTest extends RdapSearchActionTestCase<RdapNamese
|
||||
action.registrarParam = Optional.of("unicoderegistrar");
|
||||
generateActualJsonWithName("*.cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
verifyErrorMetrics(Optional.of(2L), 404);
|
||||
verifyErrorMetrics(Optional.of(0L), 404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -521,7 +547,7 @@ class RdapNameserverSearchActionTest extends RdapSearchActionTestCase<RdapNamese
|
||||
"rdap_truncated_hosts.json", "QUERY", "name=nsx*.cat.lol&cursor=bnN4NC5jYXQubG9s"));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
// When searching names, we look for additional matches, in case some are not visible.
|
||||
verifyMetrics(9, IncompletenessWarningType.TRUNCATED);
|
||||
verifyMetrics(5, IncompletenessWarningType.TRUNCATED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -536,7 +562,7 @@ class RdapNameserverSearchActionTest extends RdapSearchActionTestCase<RdapNamese
|
||||
.putAll("ADDRESSTYPE", "v6", "ADDRESS", "bad:f00d:cafe::15:beef")
|
||||
.load("rdap_host_linked.json")));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
verifyMetrics(2);
|
||||
verifyMetrics(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -15,14 +15,23 @@
|
||||
package google.registry.request;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.request.RequestModule.provideJsonBody;
|
||||
import static google.registry.request.RequestModule.provideJsonPayload;
|
||||
import static google.registry.request.RequestModule.providePayloadAsBytes;
|
||||
import static google.registry.request.RequestModule.providePayloadAsString;
|
||||
import static google.registry.security.JsonHttpTestUtils.createServletInputStream;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.gson.Gson;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.PayloadTooLargeException;
|
||||
import google.registry.request.HttpException.UnsupportedMediaTypeException;
|
||||
import google.registry.tools.GsonUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link RequestModule}. */
|
||||
@@ -72,4 +81,46 @@ final class RequestModuleTest {
|
||||
UnsupportedMediaTypeException.class,
|
||||
() -> provideJsonPayload(MediaType.JSON_UTF_8.withParameter("omg", "handel"), "{}", GSON));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvidePayloadAsBytes_contentLengthExceedsLimit_throws413() {
|
||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||
when(req.getContentLengthLong()).thenReturn((long) RequestModule.MAX_PAYLOAD_BYTES + 1);
|
||||
PayloadTooLargeException thrown =
|
||||
assertThrows(PayloadTooLargeException.class, () -> providePayloadAsBytes(req));
|
||||
assertThat(thrown).hasMessageThat().contains("exceeds limit");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvidePayloadAsBytes_streamExceedsLimit_throws413() throws Exception {
|
||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||
when(req.getContentLengthLong()).thenReturn(-1L);
|
||||
when(req.getInputStream())
|
||||
.thenReturn(createServletInputStream(new byte[RequestModule.MAX_PAYLOAD_BYTES + 1]));
|
||||
PayloadTooLargeException thrown =
|
||||
assertThrows(PayloadTooLargeException.class, () -> providePayloadAsBytes(req));
|
||||
assertThat(thrown).hasMessageThat().contains("exceeds maximum allowed size");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvidePayloadAsString_invalidCharset_throws415() {
|
||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||
when(req.getCharacterEncoding()).thenReturn("invalid-charset-name");
|
||||
UnsupportedMediaTypeException thrown =
|
||||
assertThrows(
|
||||
UnsupportedMediaTypeException.class,
|
||||
() -> providePayloadAsString("hello".getBytes(UTF_8), req));
|
||||
assertThat(thrown).hasMessageThat().contains("Unsupported charset: invalid-charset-name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideJsonBody_contentLengthExceedsLimit_throws413() {
|
||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||
when(req.getContentLengthLong()).thenReturn((long) RequestModule.MAX_PAYLOAD_BYTES + 1);
|
||||
PayloadTooLargeException thrown =
|
||||
assertThrows(
|
||||
PayloadTooLargeException.class,
|
||||
() -> provideJsonBody(providePayloadAsString(providePayloadAsBytes(req), req), GSON));
|
||||
assertThat(thrown).hasMessageThat().contains("exceeds limit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
@@ -80,13 +81,13 @@ public class UrlConnectionUtilsTest {
|
||||
"294");
|
||||
String payload =
|
||||
"""
|
||||
--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r
|
||||
Content-Disposition: form-data; name="lol"; filename="cat"\r
|
||||
Content-Type: text/csv; charset=utf-8\r
|
||||
\r
|
||||
The nice people at the store say hello. ヘ(◕。◕ヘ)\r
|
||||
--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA--\r
|
||||
""";
|
||||
--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r
|
||||
Content-Disposition: form-data; name="lol"; filename="cat"\r
|
||||
Content-Type: text/csv; charset=utf-8\r
|
||||
\r
|
||||
The nice people at the store say hello. ヘ(◕。◕ヘ)\r
|
||||
--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA--\r
|
||||
""";
|
||||
verify(connection).setDoOutput(true);
|
||||
verify(connection).getOutputStream();
|
||||
assertThat(connectionOutputStream.toByteArray()).isEqualTo(payload.getBytes(UTF_8));
|
||||
@@ -124,4 +125,27 @@ public class UrlConnectionUtilsTest {
|
||||
when(connection.getResponseCode()).thenReturn(400);
|
||||
assertThat(UrlConnectionUtils.getResponseBytes(connection)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetResponseBytes_contentLengthExceedsLimit_throwsException() {
|
||||
HttpsURLConnection connection = mock(HttpsURLConnection.class);
|
||||
when(connection.getContentLengthLong())
|
||||
.thenReturn((long) UrlConnectionUtils.MAX_RESPONSE_PAYLOAD_BYTES + 1);
|
||||
IOException thrown =
|
||||
assertThrows(IOException.class, () -> UrlConnectionUtils.getResponseBytes(connection));
|
||||
assertThat(thrown).hasMessageThat().contains("exceeds limit");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetResponseBytes_streamExceedsLimit_throwsException() throws Exception {
|
||||
HttpsURLConnection connection = mock(HttpsURLConnection.class);
|
||||
when(connection.getContentLengthLong()).thenReturn(-1L);
|
||||
when(connection.getResponseCode()).thenReturn(200);
|
||||
when(connection.getInputStream())
|
||||
.thenReturn(
|
||||
new ByteArrayInputStream(new byte[UrlConnectionUtils.MAX_RESPONSE_PAYLOAD_BYTES + 1]));
|
||||
IOException thrown =
|
||||
assertThrows(IOException.class, () -> UrlConnectionUtils.getResponseBytes(connection));
|
||||
assertThat(thrown).hasMessageThat().contains("exceeds maximum allowed size");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import google.registry.tools.GsonUtils;
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Map;
|
||||
@@ -84,4 +87,32 @@ public final class JsonHttpTestUtils {
|
||||
final StringWriter writer) {
|
||||
return memoize(() -> getJsonResponse(writer));
|
||||
}
|
||||
|
||||
public static ServletInputStream createServletInputStream(byte[] data) {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener listener) {}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return bais.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) {
|
||||
return bais.read(b, off, len);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package google.registry.testing;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.tld.label.PremiumListDao;
|
||||
import google.registry.model.tmch.ClaimsListDao;
|
||||
import java.time.Duration;
|
||||
@@ -80,6 +81,11 @@ public class TestCacheExtension implements BeforeEachCallback, AfterEachCallback
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAllocationTokenCache(Duration expiry) {
|
||||
cacheHandlers.add(new TestCacheHandler(AllocationToken::setCacheForTest, expiry));
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCacheExtension build() {
|
||||
return new TestCacheExtension(ImmutableList.copyOf(cacheHandlers));
|
||||
}
|
||||
|
||||
@@ -31,10 +31,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.cloud.tasks.v2.HttpMethod;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.batch.RelockDomainAction;
|
||||
import google.registry.model.billing.BillingBase;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.console.RegistrarRole;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.console.UserRoles;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.RegistryLock;
|
||||
@@ -67,6 +71,21 @@ public final class DomainLockUtilsTest {
|
||||
private static final String DOMAIN_NAME = "example.tld";
|
||||
private static final String POC_ID = "marla.singer@example.com";
|
||||
|
||||
private static final User NON_ADMIN_USER =
|
||||
new User.Builder()
|
||||
.setEmailAddress("user@theregistrar.com")
|
||||
.setUserRoles(
|
||||
new UserRoles.Builder()
|
||||
.setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private static final User ADMIN_USER =
|
||||
new User.Builder()
|
||||
.setEmailAddress("admin@theregistrar.com")
|
||||
.setUserRoles(new UserRoles.Builder().setIsAdmin(true).build())
|
||||
.build();
|
||||
|
||||
private final FakeClock clock = new FakeClock(Instant.now());
|
||||
private DomainLockUtils domainLockUtils;
|
||||
private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(clock);
|
||||
@@ -122,7 +141,7 @@ public final class DomainLockUtilsTest {
|
||||
clock.advanceBy(Duration.ofDays(1));
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
|
||||
verifyProperlyLockedDomain(false);
|
||||
}
|
||||
|
||||
@@ -135,7 +154,7 @@ public final class DomainLockUtilsTest {
|
||||
RegistryLock unlockRequest =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
domainLockUtils.verifyVerificationCode(unlockRequest.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(unlockRequest.getVerificationCode(), NON_ADMIN_USER);
|
||||
assertThat(loadByEntity(domain).getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES);
|
||||
}
|
||||
|
||||
@@ -143,7 +162,7 @@ public final class DomainLockUtilsTest {
|
||||
void testSuccess_applyLockDomain() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
|
||||
verifyProperlyLockedDomain(false);
|
||||
}
|
||||
|
||||
@@ -153,7 +172,7 @@ public final class DomainLockUtilsTest {
|
||||
RegistryLock unlock =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), NON_ADMIN_USER);
|
||||
verifyProperlyUnlockedDomain(false);
|
||||
}
|
||||
|
||||
@@ -161,7 +180,7 @@ public final class DomainLockUtilsTest {
|
||||
void testSuccess_applyAdminLock_onlyHistoryEntry() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
|
||||
verifyProperlyLockedDomain(true);
|
||||
}
|
||||
|
||||
@@ -169,11 +188,11 @@ public final class DomainLockUtilsTest {
|
||||
void testSuccess_applyAdminUnlock_onlyHistoryEntry() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
|
||||
RegistryLock unlock =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", true, Optional.empty());
|
||||
domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), true);
|
||||
domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), ADMIN_USER);
|
||||
verifyProperlyUnlockedDomain(true);
|
||||
}
|
||||
|
||||
@@ -194,7 +213,7 @@ public final class DomainLockUtilsTest {
|
||||
void testSuccess_administrativelyUnlock_nonAdmin() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
|
||||
domainLockUtils.administrativelyApplyUnlock(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
verifyProperlyUnlockedDomain(false);
|
||||
@@ -204,7 +223,7 @@ public final class DomainLockUtilsTest {
|
||||
void testSuccess_administrativelyUnlock_admin() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
|
||||
domainLockUtils.administrativelyApplyUnlock(
|
||||
DOMAIN_NAME, "TheRegistrar", true, Optional.empty());
|
||||
verifyProperlyUnlockedDomain(true);
|
||||
@@ -218,7 +237,7 @@ public final class DomainLockUtilsTest {
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
RegistryLock newLock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
newLock = domainLockUtils.verifyVerificationCode(newLock.getVerificationCode(), false);
|
||||
newLock = domainLockUtils.verifyVerificationCode(newLock.getVerificationCode(), NON_ADMIN_USER);
|
||||
assertThat(
|
||||
getRegistryLockByRevisionId(oldLock.getRevisionId()).get().getRelock().getRevisionId())
|
||||
.isEqualTo(newLock.getRevisionId());
|
||||
@@ -252,7 +271,7 @@ public final class DomainLockUtilsTest {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.of(Duration.ofHours(6)));
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
|
||||
cloudTasksHelper.assertTasksEnqueued(
|
||||
QUEUE_ASYNC_ACTIONS,
|
||||
new TaskMatcher()
|
||||
@@ -302,7 +321,7 @@ public final class DomainLockUtilsTest {
|
||||
void testFailure_createUnlock_alreadyPendingUnlock() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
|
||||
@@ -321,7 +340,7 @@ public final class DomainLockUtilsTest {
|
||||
void testFailure_createUnlock_nonAdminUnlockingAdmin() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), true);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
@@ -385,12 +404,13 @@ public final class DomainLockUtilsTest {
|
||||
void testFailure_applyLock_alreadyApplied() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
|
||||
domain = loadByEntity(domain);
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false));
|
||||
() ->
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Lock/unlock with code 123456789ABCDEFGHJKLMNPQRSTUVWXY is already completed");
|
||||
@@ -405,7 +425,7 @@ public final class DomainLockUtilsTest {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), true));
|
||||
() -> domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("The pending lock has expired; please try again");
|
||||
assertNoDomainChanges();
|
||||
}
|
||||
@@ -417,31 +437,146 @@ public final class DomainLockUtilsTest {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false));
|
||||
() ->
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Non-admin user cannot complete admin lock");
|
||||
assertNoDomainChanges();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_applyLock_noPermission() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
User userWithoutPermission =
|
||||
new User.Builder()
|
||||
.setEmailAddress("unauthorized@example.com")
|
||||
.setUserRoles(new UserRoles.Builder().build())
|
||||
.build();
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
domainLockUtils.verifyVerificationCode(
|
||||
lock.getVerificationCode(), userWithoutPermission));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"User unauthorized@example.com does not have registry lock permission on registrar"
|
||||
+ " TheRegistrar");
|
||||
assertNoDomainChanges();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_applyUnlock_noPermission() {
|
||||
domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
RegistryLock unlock =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
User userWithoutPermission =
|
||||
new User.Builder()
|
||||
.setEmailAddress("unauthorized@example.com")
|
||||
.setUserRoles(new UserRoles.Builder().build())
|
||||
.build();
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
domainLockUtils.verifyVerificationCode(
|
||||
unlock.getVerificationCode(), userWithoutPermission));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"User unauthorized@example.com does not have registry lock permission on registrar"
|
||||
+ " TheRegistrar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_applyUnlock_alreadyUnlocked() {
|
||||
RegistryLock lock =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
|
||||
RegistryLock unlock =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), false);
|
||||
domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), NON_ADMIN_USER);
|
||||
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), false));
|
||||
() ->
|
||||
domainLockUtils.verifyVerificationCode(
|
||||
unlock.getVerificationCode(), NON_ADMIN_USER));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Lock/unlock with code Zabcdefghijkmnopqrstuvwxyz123456 is already completed");
|
||||
assertNoDomainChanges();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_applyUnlock_supersededByNewerLock() {
|
||||
domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
|
||||
RegistryLock unlockRequest =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
|
||||
clock.advanceOneMilli();
|
||||
domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", null, true);
|
||||
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
domainLockUtils.verifyVerificationCode(
|
||||
unlockRequest.getVerificationCode(), NON_ADMIN_USER));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"The pending unlock on example.tld has been superseded; please request a new unlock");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_applyUnlock_supersededByInterleavedUnlockAndLock() {
|
||||
// Lock the domain initially
|
||||
domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", null, true);
|
||||
|
||||
// 1. Unlock A requested
|
||||
clock.advanceOneMilli();
|
||||
RegistryLock unlockA =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", true, Optional.empty());
|
||||
|
||||
// 2. Unlock B requested
|
||||
clock.advanceOneMilli();
|
||||
RegistryLock unlockB =
|
||||
domainLockUtils.saveNewRegistryUnlockRequest(
|
||||
DOMAIN_NAME, "TheRegistrar", true, Optional.empty());
|
||||
|
||||
// 3. Unlock B completed
|
||||
clock.advanceOneMilli();
|
||||
domainLockUtils.verifyVerificationCode(unlockB.getVerificationCode(), ADMIN_USER);
|
||||
|
||||
// 4. Lock C requested
|
||||
clock.advanceOneMilli();
|
||||
RegistryLock lockC =
|
||||
domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, true);
|
||||
|
||||
// 5. Lock C applied
|
||||
clock.advanceOneMilli();
|
||||
domainLockUtils.verifyVerificationCode(lockC.getVerificationCode(), ADMIN_USER);
|
||||
|
||||
// 6. Unlock A completion attempt should fail since it has been superseded -- the second unlock
|
||||
// request rewrote the verification code
|
||||
clock.advanceOneMilli();
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
domainLockUtils.verifyVerificationCode(unlockA.getVerificationCode(), ADMIN_USER));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
String.format("Invalid verification code \"%s\"", unlockA.getVerificationCode()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_applyLock_alreadyLocked() {
|
||||
RegistryLock lock =
|
||||
@@ -453,7 +588,7 @@ public final class DomainLockUtilsTest {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> domainLockUtils.verifyVerificationCode(verificationCode, false));
|
||||
() -> domainLockUtils.verifyVerificationCode(verificationCode, NON_ADMIN_USER));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Domain example.tld is already locked");
|
||||
|
||||
// Failure during the lock acquisition portion shouldn't affect the SQL object
|
||||
|
||||
@@ -27,6 +27,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.console.UserRoles;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.RegistryLock;
|
||||
import google.registry.model.registrar.Registrar.Type;
|
||||
@@ -56,11 +58,17 @@ class UnlockDomainCommandTest extends CommandTestCase<UnlockDomainCommand> {
|
||||
command.printStream = System.out;
|
||||
}
|
||||
|
||||
private static final User ADMIN_USER =
|
||||
new User.Builder()
|
||||
.setEmailAddress("admin@theregistrar.com")
|
||||
.setUserRoles(new UserRoles.Builder().setIsAdmin(true).build())
|
||||
.build();
|
||||
|
||||
private Domain persistLockedDomain(String domainName, String registrarId) {
|
||||
Domain domain = persistResource(DatabaseHelper.newDomain(domainName));
|
||||
RegistryLock lock =
|
||||
command.domainLockUtils.saveNewRegistryLockRequest(domainName, registrarId, null, true);
|
||||
command.domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), true);
|
||||
command.domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
|
||||
return reloadResource(domain);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ import com.google.gson.Gson;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.security.JsonHttpTestUtils;
|
||||
import google.registry.testing.ConsoleApiParamsUtils;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.tools.GsonUtils;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -51,4 +54,8 @@ public abstract class ConsoleActionBaseTestCase {
|
||||
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
|
||||
response = (FakeResponse) consoleApiParams.response();
|
||||
}
|
||||
|
||||
protected static ServletInputStream createServletInputStream(String data) {
|
||||
return JsonHttpTestUtils.createServletInputStream(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-11
@@ -23,7 +23,6 @@ import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -47,9 +46,7 @@ import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordDat
|
||||
import google.registry.util.EmailMessage;
|
||||
import jakarta.mail.internet.AddressException;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -167,16 +164,13 @@ class ConsoleEppPasswordActionTest extends ConsoleActionBaseTestCase {
|
||||
AuthenticatedRegistrarAccessor.createForTesting(
|
||||
ImmutableSetMultimap.of("TheRegistrar", OWNER));
|
||||
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
|
||||
doReturn(
|
||||
new BufferedReader(
|
||||
new StringReader(
|
||||
String.format(
|
||||
eppPostData, registrarId, oldPassword, newPassword, newPasswordRepeat))))
|
||||
.when(consoleApiParams.request())
|
||||
.getReader();
|
||||
Optional<EppPasswordData> maybePasswordChangeRequest =
|
||||
ConsoleModule.provideEppPasswordChangeRequest(
|
||||
GSON, RequestModule.provideJsonBody(consoleApiParams.request(), GSON));
|
||||
GSON,
|
||||
RequestModule.provideJsonBody(
|
||||
String.format(
|
||||
eppPostData, registrarId, oldPassword, newPassword, newPasswordRepeat),
|
||||
GSON));
|
||||
|
||||
return new ConsoleEppPasswordAction(
|
||||
consoleApiParams, authenticatedRegistrarAccessor, maybePasswordChangeRequest);
|
||||
|
||||
+15
-1
@@ -182,6 +182,20 @@ public class ConsoleRegistryLockVerifyActionTest extends ConsoleActionBaseTestCa
|
||||
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_noPermission() {
|
||||
saveRegistryLock(createDefaultLockBuilder().build());
|
||||
user = user.asBuilder().setUserRoles(new UserRoles.Builder().build()).build();
|
||||
action = createAction(DEFAULT_CODE);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"User user@theregistrar.com does not have registry lock permission on registrar"
|
||||
+ " TheRegistrar");
|
||||
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
|
||||
}
|
||||
|
||||
private RegistryLock.Builder createDefaultLockBuilder() {
|
||||
return new RegistryLock.Builder()
|
||||
.setRepoId(defaultDomain.getRepoId())
|
||||
@@ -194,7 +208,7 @@ public class ConsoleRegistryLockVerifyActionTest extends ConsoleActionBaseTestCa
|
||||
private ConsoleRegistryLockVerifyAction createAction(String verificationCode) {
|
||||
AuthResult authResult = AuthResult.createUser(user);
|
||||
ConsoleApiParams params = ConsoleApiParamsUtils.createFake(authResult);
|
||||
when(params.request().getMethod()).thenReturn("GET");
|
||||
when(params.request().getMethod()).thenReturn("POST");
|
||||
when(params.request().getServerName()).thenReturn("registrarconsole.tld");
|
||||
DomainLockUtils domainLockUtils =
|
||||
new DomainLockUtils(
|
||||
|
||||
+1
-8
@@ -21,7 +21,6 @@ import static google.registry.testing.DatabaseHelper.loadSingleton;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -43,9 +42,7 @@ import google.registry.util.EmailMessage;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import jakarta.mail.internet.AddressException;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -219,12 +216,8 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
private ConsoleUpdateRegistrarAction createAction(String requestData) throws IOException {
|
||||
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
|
||||
doReturn(new BufferedReader(new StringReader(requestData)))
|
||||
.when(consoleApiParams.request())
|
||||
.getReader();
|
||||
Optional<Registrar> maybeRegistrarUpdateData =
|
||||
ConsoleModule.provideRegistrar(
|
||||
GSON, RequestModule.provideJsonBody(consoleApiParams.request(), GSON));
|
||||
ConsoleModule.provideRegistrar(GSON, RequestModule.provideJsonBody(requestData, GSON));
|
||||
return new ConsoleUpdateRegistrarAction(consoleApiParams, maybeRegistrarUpdateData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ import static jakarta.servlet.http.HttpServletResponse.SC_CREATED;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.api.services.directory.Directory;
|
||||
@@ -243,7 +246,8 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_deletesUser() throws IOException {
|
||||
void testSuccess_deletesUser_nonConsoleMintedAddress_skipsWorkspaceAccountDeletion()
|
||||
throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
@@ -265,6 +269,44 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, "test2@test.com")))
|
||||
.isEmpty();
|
||||
verify(users, never()).delete(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_deletesUser_consoleMintedAddress_deletesWorkspaceAccount() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
.asBuilder()
|
||||
.setUserRoles(user1.getUserRoles().asBuilder().setIsAdmin(true).build())
|
||||
.build());
|
||||
String mintedEmail = "abc.TheRegistrar@email.com";
|
||||
DatabaseHelper.persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress(mintedEmail)
|
||||
.setUserRoles(
|
||||
new UserRoles()
|
||||
.asBuilder()
|
||||
.setRegistrarRoles(
|
||||
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ConsoleUsersAction action =
|
||||
createAction(
|
||||
Optional.of(ConsoleApiParamsUtils.createFake(authResult)),
|
||||
Optional.of("DELETE"),
|
||||
Optional.of(
|
||||
new UserData(mintedEmail, null, RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
|
||||
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
|
||||
when(directory.users()).thenReturn(users);
|
||||
when(users.delete(mintedEmail)).thenReturn(delete);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, mintedEmail))).isEmpty();
|
||||
verify(users).delete(mintedEmail);
|
||||
verify(delete).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -23,7 +23,6 @@ import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.SqlHelper.saveRegistrar;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -42,9 +41,6 @@ import google.registry.testing.ConsoleApiParamsUtils;
|
||||
import google.registry.testing.DeterministicStringGenerator;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.util.StringGenerator;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -222,20 +218,9 @@ class RegistrarsActionTest extends ConsoleActionBaseTestCase {
|
||||
return new RegistrarsAction(
|
||||
consoleApiParams, Optional.ofNullable(null), passwordGenerator, passcodeGenerator);
|
||||
} else {
|
||||
try {
|
||||
doReturn(new BufferedReader(new StringReader(registrarParamMap.toString())))
|
||||
.when(consoleApiParams.request())
|
||||
.getReader();
|
||||
} catch (IOException e) {
|
||||
return new RegistrarsAction(
|
||||
consoleApiParams,
|
||||
Optional.ofNullable(null),
|
||||
passwordGenerator,
|
||||
passcodeGenerator);
|
||||
}
|
||||
Optional<Registrar> maybeRegistrar =
|
||||
ConsoleModule.provideRegistrar(
|
||||
GSON, RequestModule.provideJsonBody(consoleApiParams.request(), GSON));
|
||||
GSON, RequestModule.provideJsonBody(registrarParamMap.toString(), GSON));
|
||||
return new RegistrarsAction(
|
||||
consoleApiParams, maybeRegistrar, passwordGenerator, passcodeGenerator);
|
||||
}
|
||||
|
||||
+1
-7
@@ -19,7 +19,6 @@ import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableO
|
||||
import static google.registry.testing.DatabaseHelper.loadSingleton;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -41,9 +40,7 @@ import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.ui.server.console.ConsoleActionBaseTestCase;
|
||||
import google.registry.ui.server.console.ConsoleModule;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.HashMap;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -143,13 +140,10 @@ public class RdapRegistrarFieldsActionTest extends ConsoleActionBaseTestCase {
|
||||
consoleApiParams = ConsoleApiParamsUtils.createFake(AuthResult.createUser(user));
|
||||
response = (FakeResponse) consoleApiParams.response();
|
||||
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
|
||||
doReturn(new BufferedReader(new StringReader(uiRegistrarMap.toString())))
|
||||
.when(consoleApiParams.request())
|
||||
.getReader();
|
||||
return new RdapRegistrarFieldsAction(
|
||||
consoleApiParams,
|
||||
registrarAccessor,
|
||||
ConsoleModule.provideRegistrar(
|
||||
GSON, RequestModule.provideJsonBody(consoleApiParams.request(), GSON)));
|
||||
GSON, RequestModule.provideJsonBody(uiRegistrarMap.toString(), GSON)));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -22,7 +22,6 @@ import static google.registry.testing.SqlHelper.saveRegistrar;
|
||||
import static google.registry.util.DateTimeUtils.START_INSTANT;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -37,9 +36,7 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.ui.server.console.ConsoleActionBaseTestCase;
|
||||
import google.registry.ui.server.console.ConsoleModule;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -170,12 +167,8 @@ class SecurityActionTest extends ConsoleActionBaseTestCase {
|
||||
String registrarId, String jsonBody, CertificateChecker certificateChecker)
|
||||
throws IOException {
|
||||
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
|
||||
doReturn(new BufferedReader(new StringReader(jsonBody)))
|
||||
.when(consoleApiParams.request())
|
||||
.getReader();
|
||||
Optional<Registrar> maybeRegistrar =
|
||||
ConsoleModule.provideRegistrar(
|
||||
GSON, RequestModule.provideJsonBody(consoleApiParams.request(), GSON));
|
||||
ConsoleModule.provideRegistrar(GSON, RequestModule.provideJsonBody(jsonBody, GSON));
|
||||
return new SecurityAction(
|
||||
consoleApiParams, certificateChecker, registrarAccessor, registrarId, maybeRegistrar);
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:cd>
|
||||
<domain:name avail="true">example1.tld</domain:name>
|
||||
</domain:cd>
|
||||
<domain:cd>
|
||||
<domain:name avail="true">example2.tld</domain:name>
|
||||
</domain:cd>
|
||||
<domain:cd>
|
||||
<domain:name avail="true">example3.tld</domain:name>
|
||||
</domain:cd>
|
||||
</domain:chkData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:cd>
|
||||
<fee:objID>example1.tld</fee:objID>
|
||||
<fee:class>standard</fee:class>
|
||||
<fee:command name="create">
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="Expiry Access Period, fee expires: 2009-01-06T01:00:00Z">110.00</fee:fee>
|
||||
</fee:command>
|
||||
</fee:cd>
|
||||
<fee:cd>
|
||||
<fee:objID>example2.tld</fee:objID>
|
||||
<fee:class>standard</fee:class>
|
||||
<fee:command name="create">
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
</fee:command>
|
||||
</fee:cd>
|
||||
<fee:cd>
|
||||
<fee:objID>example3.tld</fee:objID>
|
||||
<fee:class>standard</fee:class>
|
||||
<fee:command name="create">
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
</fee:command>
|
||||
</fee:cd>
|
||||
</fee:chkData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -0,0 +1,26 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<create>
|
||||
<domain:create
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>rich.example</domain:name>
|
||||
<domain:period unit="y">2</domain:period>
|
||||
<domain:ns>
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
</domain:create>
|
||||
</create>
|
||||
<extension>
|
||||
<fee:create xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:fee description="create">200.00</fee:fee>
|
||||
<fee:fee description="Expiry Access Period">100.00</fee:fee>
|
||||
</fee:create>
|
||||
</extension>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:creData
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>rich.example</domain:name>
|
||||
<domain:crDate>1999-04-03T22:00:00.0Z</domain:crDate>
|
||||
<domain:exDate>2001-04-03T22:00:00.0Z</domain:exDate>
|
||||
</domain:creData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:creData xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:fee description="create">200.00</fee:fee>
|
||||
<fee:fee description="Expiry Access Period, fee expires: 1999-04-03T23:00:00Z">100.00</fee:fee>
|
||||
</fee:creData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:creData
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>example.tld</domain:name>
|
||||
<domain:crDate>1999-04-03T22:00:00.0Z</domain:crDate>
|
||||
<domain:exDate>2001-04-03T22:00:00.0Z</domain:exDate>
|
||||
</domain:creData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:creData xmlns:fee="urn:ietf:params:xml:ns:epp:fee-1.0">
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:fee description="create">24.00</fee:fee>
|
||||
<fee:fee description="Expiry Access Period, fee expires: 1999-04-03T23:00:00Z">100.00</fee:fee>
|
||||
</fee:creData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -32,6 +32,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables:
|
||||
- "EXTENDED_LATIN"
|
||||
- "JA"
|
||||
|
||||
@@ -82,7 +82,7 @@ CONSOLE /console-api/password-reset-verify PasswordResetVerifyA
|
||||
CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
|
||||
CONSOLE /console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
|
||||
CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
|
||||
CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
|
||||
CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction POST n USER PUBLIC
|
||||
CONSOLE /console-api/settings/contacts ContactAction GET,POST,DELETE,PUT n USER PUBLIC
|
||||
CONSOLE /console-api/settings/rdap-fields RdapRegistrarFieldsAction POST n USER PUBLIC
|
||||
CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC
|
||||
|
||||
@@ -21,15 +21,12 @@
|
||||
-- "tld":"","tlds":["a.how"],"icannActivityReportField":"srs-dom-check"}
|
||||
|
||||
SELECT
|
||||
-- Remove quotation marks from tld fields.
|
||||
REGEXP_EXTRACT(tld, '^"(.*)"$') AS tld,
|
||||
tld,
|
||||
activityReportField AS metricName,
|
||||
COUNT(*) AS count
|
||||
FROM (
|
||||
SELECT
|
||||
-- TODO(b/32486667): Replace with JSON.parse() UDF when available for views
|
||||
SPLIT(
|
||||
REGEXP_EXTRACT(JSON_EXTRACT(json, '$.tlds'), r'^\[(.*)\]$')) AS tlds,
|
||||
JSON_EXTRACT_STRING_ARRAY(json, '$.tlds') AS tlds,
|
||||
JSON_EXTRACT_SCALAR(json,
|
||||
'$.resourceType') AS resourceType,
|
||||
JSON_EXTRACT_SCALAR(json,
|
||||
@@ -43,10 +40,10 @@ FROM (
|
||||
WHERE
|
||||
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
|
||||
AND _TABLE_SUFFIX BETWEEN '20170901' AND '20170930')
|
||||
) AS regexes
|
||||
) AS json_parsed
|
||||
JOIN
|
||||
-- Unnest the JSON-parsed tlds.
|
||||
UNNEST(regexes.tlds) AS tld
|
||||
UNNEST(json_parsed.tlds) AS tld
|
||||
-- Exclude cases that can't be tabulated correctly, where activityReportField
|
||||
-- is null/empty, or TLD is null/empty despite being a domain flow.
|
||||
WHERE
|
||||
|
||||
@@ -19,6 +19,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -23,6 +23,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables:
|
||||
- "foo"
|
||||
invoicingEnabled: false
|
||||
|
||||
@@ -19,6 +19,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -23,6 +23,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -23,6 +23,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -27,6 +27,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables:
|
||||
- "EXTENDED_LATIN"
|
||||
- "JA"
|
||||
|
||||
@@ -23,6 +23,8 @@ eapFeeSchedule:
|
||||
currency: "JPY"
|
||||
amount: 0
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -22,6 +22,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -26,6 +26,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -18,6 +18,8 @@ reservedListNames: null
|
||||
premiumListName: null
|
||||
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
dnsPaused: false
|
||||
addGracePeriodLength: "PT720H"
|
||||
anchorTenantAddGracePeriodLength: "PT720H"
|
||||
|
||||
@@ -5,6 +5,8 @@ reservedListNames: []
|
||||
dnsPaused: false
|
||||
tldType: "REAL"
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
anchorTenantAddGracePeriodLength: "PT720H"
|
||||
dnsNsTtl: null
|
||||
tldStr: "outoforderfields"
|
||||
|
||||
@@ -26,6 +26,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -23,6 +23,8 @@ eapFeeSchedule:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
idnTables: []
|
||||
invoicingEnabled: false
|
||||
lordnUsername: null
|
||||
|
||||
@@ -17,6 +17,8 @@ creationTime: "2022-09-01T00:00:00.000Z"
|
||||
reservedListNames: []
|
||||
premiumListName: "test"
|
||||
escrowEnabled: false
|
||||
expiryAccessPeriodTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "DISABLED"
|
||||
dnsPaused: false
|
||||
addGracePeriodLength: 432000000
|
||||
anchorTenantAddGracePeriodLength: 2592000000
|
||||
|
||||
@@ -261,11 +261,11 @@ td.section {
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">generated on</td>
|
||||
<td class="property_value">2026-07-06 17:57:29</td>
|
||||
<td class="property_value">2026-07-14 19:18:20</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">last flyway file</td>
|
||||
<td id="lastFlywayFile" class="property_value">V224__add_registrar_expiry_access_period_enabled.sql</td>
|
||||
<td id="lastFlywayFile" class="property_value">V225__user_registry_lock_email_address_index.sql</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -273,9 +273,9 @@ td.section {
|
||||
<p> </p>
|
||||
<svg viewBox="0.00 0.00 4783.00 3613.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3608.5)">
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.11.1</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-07-06 17:57:29</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.11.1</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-07-14 19:18:20</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<g id="node1" class="node">
|
||||
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="none" points="479.25,-1017.62 479.25,-1037.38 664.25,-1037.38 664.25,-1017.62 479.25,-1017.62" /> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1023.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."AllocationToken"</text> <polygon fill="#e9c2f2" stroke="none" points="664.25,-1017.62 664.25,-1037.38 737.25,-1037.38 737.25,-1017.62 664.25,-1017.62" /> <text xml:space="preserve" text-anchor="start" x="698.5" y="-1022.08" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1003.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">token</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-1002.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-1002.33" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-982.58" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-982.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-982.58" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-962.83" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-962.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-962.83" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-943.08" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-943.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-943.08" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="478.25,-937.62 478.25,-1038.38 738.25,-1038.38 738.25,-937.62 478.25,-937.62" />
|
||||
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="none" points="479.25,-1014.62 479.25,-1034.38 664.25,-1034.38 664.25,-1014.62 479.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1020.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."AllocationToken"</text> <polygon fill="#e9c2f2" stroke="none" points="664.25,-1014.62 664.25,-1034.38 737.25,-1034.38 737.25,-1014.62 664.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="698.5" y="-1019.08" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1000.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">token</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="478.25,-934.62 478.25,-1035.38 738.25,-1035.38 738.25,-934.62 478.25,-934.62" />
|
||||
</g>
|
||||
<!-- billingevent_a57d1815 -->
|
||||
<g id="node2" class="node">
|
||||
@@ -283,23 +283,23 @@ td.section {
|
||||
</g>
|
||||
<!-- billingevent_a57d1815->allocationtoken_a08ccbef -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>billingevent_a57d1815:w->allocationtoken_a08ccbef:e</title> <path fill="none" stroke="black" d="M1159.65,-977.35C982.41,-979.24 929.83,-1006.76 748.13,-1007.72" /> <polygon fill="black" stroke="black" points="1168.67,-977.3 1178.69,-981.75 1174,-977.28 1178.33,-977.25 1178.33,-977.25 1178.33,-977.25 1174,-977.28 1178.64,-972.75 1168.67,-977.3" /> <ellipse fill="none" stroke="black" cx="1162.95" cy="-977.33" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="739.26,-1012.75 739.24,-1002.75 741.24,-1002.74 741.26,-1012.74 739.26,-1012.75" /> <polyline fill="none" stroke="black" points="738.25,-1007.75 743.25,-1007.74" /> <polygon fill="black" stroke="black" points="744.26,-1012.73 744.24,-1002.73 746.24,-1002.73 746.26,-1012.73 744.26,-1012.73" /> <polyline fill="none" stroke="black" points="743.25,-1007.74 748.25,-1007.72" /> <text xml:space="preserve" text-anchor="start" x="870.62" y="-1009.35" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_allocation_token</text>
|
||||
<title>billingevent_a57d1815:w->allocationtoken_a08ccbef:e</title> <path fill="none" stroke="black" d="M1159.66,-977.34C982.48,-979.04 929.75,-1003.85 748.12,-1004.73" /> <polygon fill="black" stroke="black" points="1168.67,-977.3 1178.69,-981.75 1174,-977.27 1178.33,-977.25 1178.33,-977.25 1178.33,-977.25 1174,-977.27 1178.64,-972.75 1168.67,-977.3" /> <ellipse fill="none" stroke="black" cx="1162.95" cy="-977.33" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="739.26,-1009.75 739.24,-999.75 741.24,-999.74 741.26,-1009.74 739.26,-1009.75" /> <polyline fill="none" stroke="black" points="738.25,-1004.75 743.25,-1004.74" /> <polygon fill="black" stroke="black" points="744.26,-1009.74 744.24,-999.74 746.24,-999.73 746.26,-1009.73 744.26,-1009.74" /> <polyline fill="none" stroke="black" points="743.25,-1004.74 748.25,-1004.73" /> <text xml:space="preserve" text-anchor="start" x="870.62" y="-1006.43" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_allocation_token</text>
|
||||
</g>
|
||||
<!-- billingrecurrence_5fa2cb01 -->
|
||||
<g id="node7" class="node">
|
||||
<title>billingrecurrence_5fa2cb01</title> <polygon fill="#e9c2f2" stroke="none" points="457.25,-891.12 457.25,-910.88 633.75,-910.88 633.75,-891.12 457.25,-891.12" /> <text xml:space="preserve" text-anchor="start" x="459.25" y="-896.58" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."BillingRecurrence"</text> <polygon fill="#e9c2f2" stroke="none" points="633.75,-891.12 633.75,-910.88 759.25,-910.88 759.25,-891.12 633.75,-891.12" /> <text xml:space="preserve" text-anchor="start" x="720.5" y="-895.58" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-876.83" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">billing_recurrence_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-875.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-875.83" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-856.08" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-856.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-856.08" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-836.33" font-family="Helvetica,sans-Serif" font-size="14.00">domain_history_revision_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-836.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-836.33" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-816.58" font-family="Helvetica,sans-Serif" font-size="14.00">domain_repo_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-816.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-816.58" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-796.83" font-family="Helvetica,sans-Serif" font-size="14.00">event_time</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-796.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-796.83" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-777.08" font-family="Helvetica,sans-Serif" font-size="14.00">recurrence_end_time</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-777.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-777.08" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-757.33" font-family="Helvetica,sans-Serif" font-size="14.00">recurrence_time_of_year</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-757.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-757.33" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-737.58" font-family="Helvetica,sans-Serif" font-size="14.00">recurrence_last_expansion</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-737.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-737.58" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text> <polygon fill="none" stroke="#888888" points="456.25,-732.12 456.25,-911.88 760.25,-911.88 760.25,-732.12 456.25,-732.12" />
|
||||
<title>billingrecurrence_5fa2cb01</title> <polygon fill="#e9c2f2" stroke="none" points="457.25,-888.12 457.25,-907.88 633.75,-907.88 633.75,-888.12 457.25,-888.12" /> <text xml:space="preserve" text-anchor="start" x="459.25" y="-893.58" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."BillingRecurrence"</text> <polygon fill="#e9c2f2" stroke="none" points="633.75,-888.12 633.75,-907.88 759.25,-907.88 759.25,-888.12 633.75,-888.12" /> <text xml:space="preserve" text-anchor="start" x="720.5" y="-892.58" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-873.83" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">billing_recurrence_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-872.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-872.83" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-853.08" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-853.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-853.08" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-833.33" font-family="Helvetica,sans-Serif" font-size="14.00">domain_history_revision_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-833.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-833.33" font-family="Helvetica,sans-Serif" font-size="14.00">int8 not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-813.58" font-family="Helvetica,sans-Serif" font-size="14.00">domain_repo_id</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-813.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-813.58" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-793.83" font-family="Helvetica,sans-Serif" font-size="14.00">event_time</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-793.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-793.83" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-774.08" font-family="Helvetica,sans-Serif" font-size="14.00">recurrence_end_time</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-774.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-774.08" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-754.33" font-family="Helvetica,sans-Serif" font-size="14.00">recurrence_time_of_year</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-754.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-754.33" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="459.25" y="-734.58" font-family="Helvetica,sans-Serif" font-size="14.00">recurrence_last_expansion</text> <text xml:space="preserve" text-anchor="start" x="627.75" y="-734.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="635.75" y="-734.58" font-family="Helvetica,sans-Serif" font-size="14.00">timestamptz not null</text> <polygon fill="none" stroke="#888888" points="456.25,-729.12 456.25,-908.88 760.25,-908.88 760.25,-729.12 456.25,-729.12" />
|
||||
</g>
|
||||
<!-- billingevent_a57d1815->billingrecurrence_5fa2cb01 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>billingevent_a57d1815:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M1159.72,-937.55C991.18,-933.92 943.22,-883.19 770.2,-881.3" /> <polygon fill="black" stroke="black" points="1168.67,-937.64 1178.62,-942.25 1174,-937.7 1178.33,-937.74 1178.33,-937.74 1178.33,-937.74 1174,-937.7 1178.71,-933.25 1168.67,-937.64" /> <ellipse fill="none" stroke="black" cx="1162.95" cy="-937.58" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="761.22,-886.26 761.28,-876.26 763.28,-876.27 763.22,-886.27 761.22,-886.26" /> <polyline fill="none" stroke="black" points="760.25,-881.25 765.25,-881.28" /> <polygon fill="black" stroke="black" points="766.22,-886.28 766.28,-876.28 768.28,-876.29 768.22,-886.29 766.22,-886.28" /> <polyline fill="none" stroke="black" points="765.25,-881.28 770.25,-881.3" /> <text xml:space="preserve" text-anchor="start" x="786.25" y="-939.61" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_cancellation_matching_billing_recurrence_id</text>
|
||||
<title>billingevent_a57d1815:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M1159.7,-937.53C991.02,-933.71 943.38,-880.29 770.21,-878.31" /> <polygon fill="black" stroke="black" points="1168.67,-937.63 1178.62,-942.25 1174,-937.69 1178.33,-937.74 1178.33,-937.74 1178.33,-937.74 1174,-937.69 1178.72,-933.25 1168.67,-937.63" /> <ellipse fill="none" stroke="black" cx="1162.95" cy="-937.57" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="761.22,-883.26 761.28,-873.26 763.28,-873.27 763.22,-883.27 761.22,-883.26" /> <polyline fill="none" stroke="black" points="760.25,-878.25 765.25,-878.28" /> <polygon fill="black" stroke="black" points="766.22,-883.28 766.28,-873.28 768.28,-873.3 768.22,-883.3 766.22,-883.28" /> <polyline fill="none" stroke="black" points="765.25,-878.28 770.25,-878.31" /> <text xml:space="preserve" text-anchor="start" x="786.25" y="-939.58" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_cancellation_matching_billing_recurrence_id</text>
|
||||
</g>
|
||||
<!-- registrar_6e1503e3 -->
|
||||
<g id="node34" class="node">
|
||||
<title>registrar_6e1503e3</title> <polygon fill="#e9c2f2" stroke="none" points="9,-672.75 9,-692.5 129.25,-692.5 129.25,-672.75 9,-672.75" /> <text xml:space="preserve" text-anchor="start" x="11" y="-678.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."Registrar"</text> <polygon fill="#e9c2f2" stroke="none" points="129.25,-672.75 129.25,-692.5 202.25,-692.5 202.25,-672.75 129.25,-672.75" /> <text xml:space="preserve" text-anchor="start" x="163.5" y="-677.2" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="11" y="-658.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">registrar_id</text> <text xml:space="preserve" text-anchor="start" x="115" y="-657.45" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="131.25" y="-657.45" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="11" y="-637.7" font-family="Helvetica,sans-Serif" font-size="14.00">iana_identifier</text> <text xml:space="preserve" text-anchor="start" x="115" y="-637.7" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="131.25" y="-637.7" font-family="Helvetica,sans-Serif" font-size="14.00">int8</text> <text xml:space="preserve" text-anchor="start" x="11" y="-617.95" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_name</text> <text xml:space="preserve" text-anchor="start" x="115" y="-617.95" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="131.25" y="-617.95" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <polygon fill="none" stroke="#888888" points="8,-612.5 8,-693.5 203.25,-693.5 203.25,-612.5 8,-612.5" />
|
||||
<title>registrar_6e1503e3</title> <polygon fill="#e9c2f2" stroke="none" points="9,-669.75 9,-689.5 129.25,-689.5 129.25,-669.75 9,-669.75" /> <text xml:space="preserve" text-anchor="start" x="11" y="-675.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."Registrar"</text> <polygon fill="#e9c2f2" stroke="none" points="129.25,-669.75 129.25,-689.5 202.25,-689.5 202.25,-669.75 129.25,-669.75" /> <text xml:space="preserve" text-anchor="start" x="163.5" y="-674.2" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="11" y="-655.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">registrar_id</text> <text xml:space="preserve" text-anchor="start" x="115" y="-654.45" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="131.25" y="-654.45" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="11" y="-634.7" font-family="Helvetica,sans-Serif" font-size="14.00">iana_identifier</text> <text xml:space="preserve" text-anchor="start" x="115" y="-634.7" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="131.25" y="-634.7" font-family="Helvetica,sans-Serif" font-size="14.00">int8</text> <text xml:space="preserve" text-anchor="start" x="11" y="-614.95" font-family="Helvetica,sans-Serif" font-size="14.00">registrar_name</text> <text xml:space="preserve" text-anchor="start" x="115" y="-614.95" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="131.25" y="-614.95" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <polygon fill="none" stroke="#888888" points="8,-609.5 8,-690.5 203.25,-690.5 203.25,-609.5 8,-609.5" />
|
||||
</g>
|
||||
<!-- billingevent_a57d1815->registrar_6e1503e3 -->
|
||||
<g id="edge30" class="edge">
|
||||
<title>billingevent_a57d1815:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M1163.94,-1056.32C851.06,-1059.37 750.18,-1156.13 448.25,-1051 331.63,-1010.39 289.91,-987.56 229.25,-880 207.15,-840.81 245.33,-688.09 212.57,-665.64" /> <polygon fill="black" stroke="black" points="1168.67,-1056.3 1178.69,-1060.75 1174,-1056.27 1178.33,-1056.25 1178.33,-1056.25 1178.33,-1056.25 1174,-1056.27 1178.64,-1051.75 1168.67,-1056.3" /> <polygon fill="black" stroke="black" points="1166.42,-1051.31 1166.47,-1061.31 1164.47,-1061.32 1164.42,-1051.32 1166.42,-1051.31" /> <polyline fill="none" stroke="black" points="1167.45,-1056.31 1162.45,-1056.33" /> <polygon fill="black" stroke="black" points="202.79,-667.95 205.63,-658.37 207.55,-658.93 204.7,-668.52 202.79,-667.95" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.04,-664.3" /> <polygon fill="black" stroke="black" points="207.58,-669.37 210.42,-659.79 212.34,-660.36 209.5,-669.94 207.58,-669.37" /> <polyline fill="none" stroke="black" points="208.04,-664.3 212.84,-665.72" /> <text xml:space="preserve" text-anchor="start" x="524.25" y="-1102.32" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_registrar_id</text>
|
||||
<title>billingevent_a57d1815:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M1163.94,-1056.32C851.04,-1059.3 749.98,-1153.75 448.25,-1048 331.71,-1007.15 289.91,-984.56 229.25,-877 207.15,-837.81 245.33,-685.09 212.57,-662.64" /> <polygon fill="black" stroke="black" points="1168.67,-1056.3 1178.69,-1060.75 1174,-1056.27 1178.33,-1056.25 1178.33,-1056.25 1178.33,-1056.25 1174,-1056.27 1178.64,-1051.75 1168.67,-1056.3" /> <polygon fill="black" stroke="black" points="1166.42,-1051.31 1166.47,-1061.31 1164.47,-1061.32 1164.42,-1051.32 1166.42,-1051.31" /> <polyline fill="none" stroke="black" points="1167.45,-1056.3 1162.45,-1056.33" /> <polygon fill="black" stroke="black" points="202.79,-664.95 205.63,-655.37 207.55,-655.93 204.7,-665.52 202.79,-664.95" /> <polyline fill="none" stroke="black" points="203.25,-659.88 208.04,-661.3" /> <polygon fill="black" stroke="black" points="207.58,-666.37 210.42,-656.79 212.34,-657.36 209.5,-666.94 207.58,-666.37" /> <polyline fill="none" stroke="black" points="208.04,-661.3 212.84,-662.72" /> <text xml:space="preserve" text-anchor="start" x="524.25" y="-1100.43" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_event_registrar_id</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa -->
|
||||
<g id="node3" class="node">
|
||||
@@ -307,7 +307,7 @@ td.section {
|
||||
</g>
|
||||
<!-- domain_6c51cffa->allocationtoken_a08ccbef -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>domain_6c51cffa:w->allocationtoken_a08ccbef:e</title> <path fill="none" stroke="black" d="M2585.78,-694.39C2580.42,-683.62 2580.11,-667.06 2565,-660 2553.58,-654.66 1918.04,-651.19 1610.5,-660.25 1414.92,-666.01 1360.06,-635.57 1171,-686 986.35,-735.26 913.86,-735.73 786.25,-878 747.68,-921 793.48,-997.72 748.19,-1006.85" /> <polygon fill="black" stroke="black" points="2593.31,-698.99 2599.49,-708.04 2597.86,-701.77 2601.55,-704.03 2601.55,-704.03 2601.55,-704.03 2597.86,-701.77 2604.19,-700.36 2593.31,-698.99" /> <ellipse fill="none" stroke="black" cx="2588.43" cy="-696.01" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="739.7,-1012.64 738.8,-1002.68 740.79,-1002.5 741.69,-1012.46 739.7,-1012.64" /> <polyline fill="none" stroke="black" points="738.25,-1007.75 743.23,-1007.3" /> <polygon fill="black" stroke="black" points="744.67,-1012.19 743.78,-1002.23 745.77,-1002.05 746.67,-1012.01 744.67,-1012.19" /> <polyline fill="none" stroke="black" points="743.23,-1007.3 748.21,-1006.85" /> <text xml:space="preserve" text-anchor="start" x="1628.88" y="-662.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_current_package_token</text>
|
||||
<title>domain_6c51cffa:w->allocationtoken_a08ccbef:e</title> <path fill="none" stroke="black" d="M2585.78,-694.39C2580.42,-683.62 2580.11,-667.06 2565,-660 2553.58,-654.66 1918.04,-651.19 1610.5,-660.25 1414.92,-666.01 1360.21,-636.16 1171,-686 986.77,-734.53 913.9,-733.57 786.25,-875 747.55,-917.88 793.47,-994.71 748.19,-1003.85" /> <polygon fill="black" stroke="black" points="2593.31,-698.99 2599.49,-708.04 2597.86,-701.77 2601.55,-704.03 2601.55,-704.03 2601.55,-704.03 2597.86,-701.77 2604.19,-700.36 2593.31,-698.99" /> <ellipse fill="none" stroke="black" cx="2588.43" cy="-696.01" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="739.7,-1009.64 738.8,-999.68 740.79,-999.5 741.69,-1009.46 739.7,-1009.64" /> <polyline fill="none" stroke="black" points="738.25,-1004.75 743.23,-1004.3" /> <polygon fill="black" stroke="black" points="744.68,-1009.19 743.78,-999.23 745.77,-999.05 746.67,-1009.01 744.68,-1009.19" /> <polyline fill="none" stroke="black" points="743.23,-1004.3 748.21,-1003.85" /> <text xml:space="preserve" text-anchor="start" x="1628.88" y="-662.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_current_package_token</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa->billingevent_a57d1815 -->
|
||||
<g id="edge6" class="edge">
|
||||
@@ -323,31 +323,31 @@ td.section {
|
||||
</g>
|
||||
<!-- domain_6c51cffa->billingrecurrence_5fa2cb01 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>domain_6c51cffa:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M2585.7,-733.86C2580.34,-723.03 2580.1,-706.34 2565,-699 2423.8,-630.38 2015.73,-683.22 1858.75,-681.25 1748.43,-679.86 1720.83,-680.65 1610.5,-681.25 1415.16,-682.31 1364.5,-659.24 1171,-686 995.53,-710.26 912.35,-665.6 786.25,-790 759.04,-816.84 794.43,-869.97 769.92,-879.7" /> <polygon fill="black" stroke="black" points="2593.3,-738.49 2599.5,-747.54 2597.86,-741.27 2601.55,-743.53 2601.55,-743.53 2601.55,-743.53 2597.86,-741.27 2604.18,-739.86 2593.3,-738.49" /> <ellipse fill="none" stroke="black" cx="2588.42" cy="-735.51" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="762.03,-886.03 760.45,-876.15 762.42,-875.84 764,-885.71 762.03,-886.03" /> <polyline fill="none" stroke="black" points="760.25,-881.25 765.19,-880.46" /> <polygon fill="black" stroke="black" points="766.97,-885.24 765.38,-875.36 767.36,-875.05 768.94,-884.92 766.97,-885.24" /> <polyline fill="none" stroke="black" points="765.19,-880.46 770.12,-879.67" /> <text xml:space="preserve" text-anchor="start" x="1637.12" y="-683.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_billing_recurrence_id</text>
|
||||
<title>domain_6c51cffa:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M2585.7,-733.86C2580.34,-723.03 2580.1,-706.34 2565,-699 2423.8,-630.38 2015.73,-683.22 1858.75,-681.25 1748.43,-679.86 1720.83,-680.65 1610.5,-681.25 1415.16,-682.31 1364.5,-659.24 1171,-686 995.53,-710.26 912.91,-666.16 786.25,-790 759.87,-815.79 793.22,-866.89 769.94,-876.63" /> <polygon fill="black" stroke="black" points="2593.3,-738.49 2599.5,-747.54 2597.86,-741.27 2601.55,-743.53 2601.55,-743.53 2601.55,-743.53 2597.86,-741.27 2604.18,-739.86 2593.3,-738.49" /> <ellipse fill="none" stroke="black" cx="2588.42" cy="-735.51" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="762.06,-883.02 760.41,-873.15 762.38,-872.82 764.03,-882.69 762.06,-883.02" /> <polyline fill="none" stroke="black" points="760.25,-878.25 765.18,-877.42" /> <polygon fill="black" stroke="black" points="766.99,-882.19 765.34,-872.33 767.31,-872 768.97,-881.86 766.99,-882.19" /> <polyline fill="none" stroke="black" points="765.18,-877.42 770.11,-876.6" /> <text xml:space="preserve" text-anchor="start" x="1637.12" y="-683.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_billing_recurrence_id</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa->billingrecurrence_5fa2cb01 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>domain_6c51cffa:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M2582.85,-803.01C2322.71,-800.26 959.27,-751.28 786.25,-844 772.35,-851.45 776.62,-870 769.53,-877.75" /> <polygon fill="black" stroke="black" points="2591.79,-803.06 2601.76,-807.62 2597.13,-803.09 2601.46,-803.12 2601.46,-803.12 2601.46,-803.12 2597.13,-803.09 2601.82,-798.62 2591.79,-803.06" /> <ellipse fill="none" stroke="black" cx="2586.07" cy="-803.03" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="762.95,-885.58 759.42,-876.22 761.29,-875.51 764.82,-884.87 762.95,-885.58" /> <polyline fill="none" stroke="black" points="760.25,-881.25 764.93,-879.49" /> <polygon fill="black" stroke="black" points="767.63,-883.81 764.1,-874.45 765.97,-873.75 769.5,-883.11 767.63,-883.81" /> <polyline fill="none" stroke="black" points="764.93,-879.49 769.61,-877.72" /> <text xml:space="preserve" text-anchor="start" x="1610.5" y="-791.59" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_billing_recurrence_id</text>
|
||||
<title>domain_6c51cffa:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M2582.86,-803.01C2322.78,-800.33 960.49,-752.6 786.25,-843 772.75,-850 776.36,-867.58 769.3,-874.93" /> <polygon fill="black" stroke="black" points="2591.79,-803.06 2601.76,-807.62 2597.13,-803.1 2601.46,-803.12 2601.46,-803.12 2601.46,-803.12 2597.13,-803.1 2601.82,-798.62 2591.79,-803.06" /> <ellipse fill="none" stroke="black" cx="2586.07" cy="-803.03" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="762.91,-882.6 759.47,-873.21 761.35,-872.52 764.79,-881.91 762.91,-882.6" /> <polyline fill="none" stroke="black" points="760.25,-878.25 764.94,-876.53" /> <polygon fill="black" stroke="black" points="767.6,-880.88 764.16,-871.49 766.04,-870.8 769.48,-880.19 767.6,-880.88" /> <polyline fill="none" stroke="black" points="764.94,-876.53 769.64,-874.81" /> <text xml:space="preserve" text-anchor="start" x="1610.5" y="-791.95" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_billing_recurrence_id</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa->registrar_6e1503e3 -->
|
||||
<g id="edge32" class="edge">
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2587.34,-1063.7C2566.55,-1078.68 2596.55,-1133.72 2565,-1159 2444.44,-1255.59 2353.04,-1127.22 2220.75,-1207 2209.03,-1214.07 2214.81,-1225.51 2202.75,-1232 2009.66,-1335.95 387.21,-1403.1 229.25,-1251 184.52,-1207.93 260.22,-716.72 212.08,-666.94" /> <polygon fill="black" stroke="black" points="2592.12,-1062.47 2602.93,-1064.32 2597.28,-1061.13 2601.48,-1060.04 2601.48,-1060.04 2601.48,-1060.04 2597.28,-1061.13 2600.67,-1055.6 2592.12,-1062.47" /> <polygon fill="black" stroke="black" points="2588.72,-1058.18 2591.23,-1067.86 2589.29,-1068.37 2586.78,-1058.68 2588.72,-1058.18" /> <polyline fill="none" stroke="black" points="2590.94,-1062.77 2586.1,-1064.03" /> <polygon fill="black" stroke="black" points="202.07,-667.84 206.25,-658.75 208.07,-659.59 203.88,-668.67 202.07,-667.84" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.79,-664.97" /> <polygon fill="black" stroke="black" points="206.61,-669.93 210.79,-660.84 212.61,-661.68 208.43,-670.76 206.61,-669.93" /> <polyline fill="none" stroke="black" points="207.79,-664.97 212.33,-667.06" /> <text xml:space="preserve" text-anchor="start" x="1290.62" y="-1340.68" font-family="Helvetica,sans-Serif" font-size="14.00">fk2jc69qyg2tv9hhnmif6oa1cx1</text>
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2587.34,-1063.7C2566.55,-1078.68 2596.55,-1133.72 2565,-1159 2444.44,-1255.59 2353.04,-1127.22 2220.75,-1207 2209.03,-1214.07 2214.81,-1225.51 2202.75,-1232 2009.66,-1335.95 387.2,-1403.12 229.25,-1251 184.3,-1207.71 260.49,-713.99 212.12,-663.96" /> <polygon fill="black" stroke="black" points="2592.12,-1062.47 2602.93,-1064.32 2597.28,-1061.13 2601.48,-1060.04 2601.48,-1060.04 2601.48,-1060.04 2597.28,-1061.13 2600.67,-1055.6 2592.12,-1062.47" /> <polygon fill="black" stroke="black" points="2588.72,-1058.18 2591.23,-1067.86 2589.29,-1068.37 2586.78,-1058.68 2588.72,-1058.18" /> <polyline fill="none" stroke="black" points="2590.94,-1062.77 2586.1,-1064.03" /> <polygon fill="black" stroke="black" points="202.07,-664.84 206.25,-655.75 208.07,-656.59 203.88,-665.67 202.07,-664.84" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.79,-661.97" /> <polygon fill="black" stroke="black" points="206.61,-666.93 210.79,-657.84 212.61,-658.68 208.43,-667.76 206.61,-666.93" /> <polyline fill="none" stroke="black" points="207.79,-661.97 212.33,-664.06" /> <text xml:space="preserve" text-anchor="start" x="1290.62" y="-1340.68" font-family="Helvetica,sans-Serif" font-size="14.00">fk2jc69qyg2tv9hhnmif6oa1cx1</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa->registrar_6e1503e3 -->
|
||||
<g id="edge33" class="edge">
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2587.23,-1024.33C2566.46,-1039.81 2597.86,-1096.54 2565,-1121 2442.07,-1212.5 2011.61,-1091.23 1876.75,-1164 1865.02,-1170.33 1870.48,-1181.66 1858.75,-1188 1754.44,-1244.38 1711.01,-1201.22 1592.5,-1205 1405.26,-1210.97 1358.33,-1205.6 1171,-1205 1066.36,-1204.67 304.78,-1271.42 229.25,-1199 188.64,-1160.06 254.59,-717.48 212.12,-667.47" /> <polygon fill="black" stroke="black" points="2592.14,-1023.03 2602.96,-1024.81 2597.29,-1021.66 2601.48,-1020.55 2601.48,-1020.55 2601.48,-1020.55 2597.29,-1021.66 2600.65,-1016.11 2592.14,-1023.03" /> <polygon fill="black" stroke="black" points="2588.71,-1018.77 2591.28,-1028.43 2589.34,-1028.94 2586.78,-1019.28 2588.71,-1018.77" /> <polyline fill="none" stroke="black" points="2590.96,-1023.34 2586.13,-1024.63" /> <polygon fill="black" stroke="black" points="201.84,-667.78 206.44,-658.89 208.21,-659.81 203.62,-668.69 201.84,-667.78" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.69,-665.17" /> <polygon fill="black" stroke="black" points="206.28,-670.07 210.88,-661.19 212.65,-662.11 208.06,-670.99 206.28,-670.07" /> <polyline fill="none" stroke="black" points="207.69,-665.17 212.13,-667.47" /> <text xml:space="preserve" text-anchor="start" x="1288.38" y="-1210.24" font-family="Helvetica,sans-Serif" font-size="14.00">fk2u3srsfbei272093m3b3xwj23</text>
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2587.23,-1024.33C2566.46,-1039.81 2597.86,-1096.54 2565,-1121 2442.07,-1212.5 2011.61,-1091.23 1876.75,-1164 1865.02,-1170.33 1870.48,-1181.66 1858.75,-1188 1754.44,-1244.38 1711.01,-1201.22 1592.5,-1205 1405.26,-1210.97 1358.33,-1205.6 1171,-1205 1066.36,-1204.67 304.77,-1271.43 229.25,-1199 188.33,-1159.76 255.13,-712.94 211.9,-664.18" /> <polygon fill="black" stroke="black" points="2592.14,-1023.03 2602.96,-1024.81 2597.29,-1021.66 2601.48,-1020.55 2601.48,-1020.55 2601.48,-1020.55 2597.29,-1021.66 2600.65,-1016.11 2592.14,-1023.03" /> <polygon fill="black" stroke="black" points="2588.71,-1018.77 2591.28,-1028.43 2589.34,-1028.94 2586.78,-1019.28 2588.71,-1018.77" /> <polyline fill="none" stroke="black" points="2590.96,-1023.34 2586.13,-1024.63" /> <polygon fill="black" stroke="black" points="201.92,-664.8 206.37,-655.85 208.16,-656.74 203.71,-665.69 201.92,-664.8" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.73,-662.1" /> <polygon fill="black" stroke="black" points="206.39,-667.03 210.85,-658.07 212.64,-658.97 208.18,-667.92 206.39,-667.03" /> <polyline fill="none" stroke="black" points="207.73,-662.1 212.2,-664.33" /> <text xml:space="preserve" text-anchor="start" x="1288.38" y="-1210.24" font-family="Helvetica,sans-Serif" font-size="14.00">fk2u3srsfbei272093m3b3xwj23</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa->registrar_6e1503e3 -->
|
||||
<g id="edge34" class="edge">
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2583.97,-987.06C2566.04,-1006.5 2599.77,-1066.56 2565,-1093 2533.75,-1116.77 1192.26,-1156.29 1153,-1157 839.77,-1162.7 755.05,-1206.41 448.25,-1143 346.16,-1121.9 291.09,-1142.92 229.25,-1059 204.98,-1026.06 241.33,-713.3 211.49,-668.27" /> <polygon fill="black" stroke="black" points="2592.34,-984.21 2603.26,-985.24 2597.39,-982.49 2601.49,-981.09 2601.49,-981.09 2601.49,-981.09 2597.39,-982.49 2600.36,-976.72 2592.34,-984.21" /> <ellipse fill="none" stroke="black" cx="2586.93" cy="-986.05" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="201.35,-667.61 206.82,-659.24 208.5,-660.33 203.02,-668.7 201.35,-667.61" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.43,-665.61" /> <polygon fill="black" stroke="black" points="205.53,-670.34 211.01,-661.98 212.68,-663.07 207.21,-671.44 205.53,-670.34" /> <polyline fill="none" stroke="black" points="207.43,-665.61 211.62,-668.35" /> <text xml:space="preserve" text-anchor="start" x="1293.25" y="-1158.93" font-family="Helvetica,sans-Serif" font-size="14.00">fkjc0r9r5y1lfbt4gpbqw4wsuvq</text>
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2583.97,-987.07C2566.04,-1006.5 2599.78,-1066.58 2565,-1093 2543.47,-1109.36 1619.53,-1136.18 1592.5,-1137 1397.2,-1142.89 1348.37,-1144.78 1153,-1147 839.76,-1150.56 755.79,-1195.59 448.25,-1136 346.52,-1116.29 291.39,-1138.93 229.25,-1056 204.71,-1023.25 241.29,-710.33 211.48,-665.27" /> <polygon fill="black" stroke="black" points="2592.34,-984.21 2603.26,-985.24 2597.39,-982.49 2601.49,-981.09 2601.49,-981.09 2601.49,-981.09 2597.39,-982.49 2600.36,-976.72 2592.34,-984.21" /> <ellipse fill="none" stroke="black" cx="2586.93" cy="-986.06" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="201.35,-664.61 206.83,-656.24 208.5,-657.34 203.02,-665.7 201.35,-664.61" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.43,-662.61" /> <polygon fill="black" stroke="black" points="205.53,-667.35 211.01,-658.98 212.68,-660.08 207.2,-668.44 205.53,-667.35" /> <polyline fill="none" stroke="black" points="207.43,-662.61 211.61,-665.35" /> <text xml:space="preserve" text-anchor="start" x="1293.25" y="-1149.22" font-family="Helvetica,sans-Serif" font-size="14.00">fkjc0r9r5y1lfbt4gpbqw4wsuvq</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa->registrar_6e1503e3 -->
|
||||
<g id="edge35" class="edge">
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2582.72,-783.37C1791.14,-782.9 1586.29,-753.81 786.25,-711 538.56,-697.75 468.63,-740.99 229.25,-676 220.86,-673.72 217.85,-668.58 212.85,-665.45" /> <polygon fill="black" stroke="black" points="2591.79,-783.37 2601.79,-787.87 2597.13,-783.37 2601.46,-783.37 2601.46,-783.37 2601.46,-783.37 2597.13,-783.37 2601.79,-778.87 2591.79,-783.37" /> <ellipse fill="none" stroke="black" cx="2586.07" cy="-783.37" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="202.92,-667.96 205.51,-658.3 207.44,-658.82 204.85,-668.48 202.92,-667.96" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.08,-664.17" /> <polygon fill="black" stroke="black" points="207.75,-669.26 210.34,-659.6 212.27,-660.12 209.68,-669.78 207.75,-669.26" /> <polyline fill="none" stroke="black" points="208.08,-664.17 212.91,-665.47" /> <text xml:space="preserve" text-anchor="start" x="1260.62" y="-760.12" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_gaining_registrar_id</text>
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2582.72,-783.37C1791.14,-782.94 1586.18,-755.95 786.25,-711 538.51,-697.08 468.54,-738.66 229.25,-673 220.86,-670.7 217.85,-665.56 212.85,-662.44" /> <polygon fill="black" stroke="black" points="2591.79,-783.37 2601.79,-787.87 2597.13,-783.37 2601.46,-783.37 2601.46,-783.37 2601.46,-783.37 2597.13,-783.37 2601.79,-778.87 2591.79,-783.37" /> <ellipse fill="none" stroke="black" cx="2586.07" cy="-783.37" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="202.92,-664.96 205.51,-655.3 207.44,-655.82 204.86,-665.48 202.92,-664.96" /> <polyline fill="none" stroke="black" points="203.25,-659.88 208.08,-661.17" /> <polygon fill="black" stroke="black" points="207.75,-666.26 210.34,-656.6 212.27,-657.11 209.69,-666.77 207.75,-666.26" /> <polyline fill="none" stroke="black" points="208.08,-661.17 212.91,-662.46" /> <text xml:space="preserve" text-anchor="start" x="1260.62" y="-761.03" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_gaining_registrar_id</text>
|
||||
</g>
|
||||
<!-- domain_6c51cffa->registrar_6e1503e3 -->
|
||||
<g id="edge36" class="edge">
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2582.92,-763.61C2149.56,-762.73 2034.9,-732.31 1592.5,-713.25 978.54,-686.8 822.77,-663.13 213.17,-662.88" /> <polygon fill="black" stroke="black" points="2591.79,-763.61 2601.79,-768.12 2597.13,-763.62 2601.46,-763.62 2601.46,-763.62 2601.46,-763.62 2597.13,-763.62 2601.8,-759.12 2591.79,-763.61" /> <ellipse fill="none" stroke="black" cx="2586.07" cy="-763.61" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="204.25,-667.88 204.25,-657.88 206.25,-657.88 206.25,-667.88 204.25,-667.88" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.25,-662.88" /> <polygon fill="black" stroke="black" points="209.25,-667.88 209.25,-657.88 211.25,-657.88 211.25,-667.88 209.25,-667.88" /> <polyline fill="none" stroke="black" points="208.25,-662.88 213.25,-662.88" /> <text xml:space="preserve" text-anchor="start" x="1264.75" y="-715.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_losing_registrar_id</text>
|
||||
<title>domain_6c51cffa:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M2582.92,-763.61C2149.58,-762.77 2034.86,-733.72 1592.5,-714.25 978.5,-687.22 822.84,-660.17 213.17,-659.88" /> <polygon fill="black" stroke="black" points="2591.79,-763.62 2601.79,-768.12 2597.13,-763.62 2601.46,-763.62 2601.46,-763.62 2601.46,-763.62 2597.13,-763.62 2601.79,-759.12 2591.79,-763.62" /> <ellipse fill="none" stroke="black" cx="2586.07" cy="-763.61" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="204.25,-664.88 204.25,-654.88 206.25,-654.88 206.25,-664.88 204.25,-664.88" /> <polyline fill="none" stroke="black" points="203.25,-659.88 208.25,-659.88" /> <polygon fill="black" stroke="black" points="209.25,-664.88 209.25,-654.88 211.25,-654.88 211.25,-664.88 209.25,-664.88" /> <polyline fill="none" stroke="black" points="208.25,-659.88 213.25,-659.88" /> <text xml:space="preserve" text-anchor="start" x="1264.75" y="-716.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_transfer_losing_registrar_id</text>
|
||||
</g>
|
||||
<!-- tld_f1fa57e2 -->
|
||||
<g id="node45" class="node">
|
||||
@@ -363,7 +363,7 @@ td.section {
|
||||
</g>
|
||||
<!-- domainhistory_a54cc226->allocationtoken_a08ccbef -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>domainhistory_a54cc226:w->allocationtoken_a08ccbef:e</title> <path fill="none" stroke="black" d="M3184.99,-1290.94C2720.81,-1294.19 1607.58,-1410.17 1171,-1321 988.87,-1283.8 900.96,-1303.27 786.25,-1157 745.52,-1105.06 801.41,-1017.6 748.19,-1008.52" /> <polygon fill="black" stroke="black" points="3193.92,-1290.91 3203.93,-1295.38 3199.25,-1290.89 3203.58,-1290.88 3203.58,-1290.88 3203.58,-1290.88 3199.25,-1290.89 3203.9,-1286.38 3193.92,-1290.91" /> <ellipse fill="none" stroke="black" cx="3188.2" cy="-1290.93" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="738.86,-1012.81 739.63,-1002.84 741.63,-1003 740.86,-1012.97 738.86,-1012.81" /> <polyline fill="none" stroke="black" points="738.25,-1007.75 743.24,-1008.14" /> <polygon fill="black" stroke="black" points="743.85,-1013.2 744.62,-1003.23 746.61,-1003.38 745.84,-1013.35 743.85,-1013.2" /> <polyline fill="none" stroke="black" points="743.24,-1008.14 748.22,-1008.52" /> <text xml:space="preserve" text-anchor="start" x="1910.38" y="-1353.93" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_history_current_package_token</text>
|
||||
<title>domainhistory_a54cc226:w->allocationtoken_a08ccbef:e</title> <path fill="none" stroke="black" d="M3184.99,-1290.95C2720.78,-1294.32 1606.58,-1414.92 1171,-1321 987.54,-1281.44 901.25,-1295.31 786.25,-1147 747.6,-1097.15 798.76,-1014.43 748.1,-1005.53" /> <polygon fill="black" stroke="black" points="3193.92,-1290.91 3203.93,-1295.38 3199.25,-1290.89 3203.58,-1290.88 3203.58,-1290.88 3203.58,-1290.88 3199.25,-1290.89 3203.9,-1286.38 3193.92,-1290.91" /> <ellipse fill="none" stroke="black" cx="3188.2" cy="-1290.94" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="738.85,-1009.81 739.64,-999.84 741.64,-1000 740.84,-1009.97 738.85,-1009.81" /> <polyline fill="none" stroke="black" points="738.25,-1004.75 743.23,-1005.15" /> <polygon fill="black" stroke="black" points="743.84,-1010.21 744.63,-1000.24 746.62,-1000.4 745.83,-1010.37 743.84,-1010.21" /> <polyline fill="none" stroke="black" points="743.23,-1005.15 748.22,-1005.54" /> <text xml:space="preserve" text-anchor="start" x="1910.38" y="-1356.05" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_history_current_package_token</text>
|
||||
</g>
|
||||
<!-- domainhistory_a54cc226->domain_6c51cffa -->
|
||||
<g id="edge16" class="edge">
|
||||
@@ -371,7 +371,7 @@ td.section {
|
||||
</g>
|
||||
<!-- domainhistory_a54cc226->registrar_6e1503e3 -->
|
||||
<g id="edge37" class="edge">
|
||||
<title>domainhistory_a54cc226:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3185.01,-1389.62C3005.57,-1389.58 2953.65,-1389 2765,-1389 2765,-1389 2765,-1389 607.25,-1389 434.13,-1389 338.65,-1429.16 229.25,-1295 208.51,-1269.57 234.47,-740.66 209.41,-670.53" /> <polygon fill="black" stroke="black" points="3193.92,-1389.62 3203.92,-1394.12 3199.25,-1389.62 3203.58,-1389.62 3203.58,-1389.62 3203.58,-1389.62 3199.25,-1389.62 3203.92,-1385.12 3193.92,-1389.62" /> <ellipse fill="none" stroke="black" cx="3188.2" cy="-1389.62" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="199.98,-666.79 207.77,-660.52 209.03,-662.08 201.23,-668.35 199.98,-666.79" /> <polyline fill="none" stroke="black" points="203.25,-662.88 206.38,-666.77" /> <polygon fill="black" stroke="black" points="203.11,-670.68 210.91,-664.42 212.16,-665.98 204.37,-672.24 203.11,-670.68" /> <polyline fill="none" stroke="black" points="206.38,-666.77 209.52,-670.67" /> <text xml:space="preserve" text-anchor="start" x="1642" y="-1391.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_history_registrar_id</text>
|
||||
<title>domainhistory_a54cc226:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3185.01,-1389.62C3005.57,-1389.58 2953.65,-1389 2765,-1389 2765,-1389 2765,-1389 607.25,-1389 434.13,-1389 338.64,-1429.18 229.25,-1295 208.42,-1269.45 234.6,-738.03 209.44,-667.57" /> <polygon fill="black" stroke="black" points="3193.92,-1389.62 3203.92,-1394.12 3199.25,-1389.62 3203.58,-1389.62 3203.58,-1389.62 3203.58,-1389.62 3199.25,-1389.62 3203.92,-1385.12 3193.92,-1389.62" /> <ellipse fill="none" stroke="black" cx="3188.2" cy="-1389.62" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="199.98,-663.79 207.77,-657.52 209.03,-659.08 201.23,-665.35 199.98,-663.79" /> <polyline fill="none" stroke="black" points="203.25,-659.88 206.38,-663.77" /> <polygon fill="black" stroke="black" points="203.11,-667.68 210.91,-661.42 212.16,-662.98 204.37,-669.24 203.11,-667.68" /> <polyline fill="none" stroke="black" points="206.38,-663.77 209.52,-667.67" /> <text xml:space="preserve" text-anchor="start" x="1642" y="-1391.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_domain_history_registrar_id</text>
|
||||
</g>
|
||||
<!-- billingcancellation_6eedf614->billingevent_a57d1815 -->
|
||||
<g id="edge5" class="edge">
|
||||
@@ -379,11 +379,11 @@ td.section {
|
||||
</g>
|
||||
<!-- billingcancellation_6eedf614->billingrecurrence_5fa2cb01 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>billingcancellation_6eedf614:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M1865.49,-902.74C1750.06,-899.53 1716.87,-867.72 1592.5,-858.25 1235.09,-831.05 1143.31,-846.56 786.25,-878 778.82,-878.65 775.02,-879.8 770.18,-880.55" /> <polygon fill="black" stroke="black" points="1874.42,-902.86 1884.35,-907.5 1879.75,-902.93 1884.08,-902.99 1884.08,-902.99 1884.08,-902.99 1879.75,-902.93 1884.48,-898.5 1874.42,-902.86" /> <ellipse fill="none" stroke="black" cx="1868.7" cy="-902.78" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="761.6,-886.17 760.89,-876.19 762.89,-876.05 763.6,-886.03 761.6,-886.17" /> <polyline fill="none" stroke="black" points="760.25,-881.25 765.24,-880.9" /> <polygon fill="black" stroke="black" points="766.59,-885.81 765.88,-875.84 767.88,-875.7 768.58,-885.67 766.59,-885.81" /> <polyline fill="none" stroke="black" points="765.24,-880.9 770.22,-880.54" /> <text xml:space="preserve" text-anchor="start" x="1250.12" y="-860.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_cancellation_billing_recurrence_id</text>
|
||||
<title>billingcancellation_6eedf614:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M1865.49,-902.74C1750.06,-899.53 1716.87,-867.72 1592.5,-858.25 1235.12,-831.05 1143.4,-844.95 786.25,-875 778.82,-875.63 775.02,-876.78 770.18,-877.53" /> <polygon fill="black" stroke="black" points="1874.42,-902.86 1884.35,-907.5 1879.75,-902.93 1884.08,-902.99 1884.08,-902.99 1884.08,-902.99 1879.75,-902.93 1884.48,-898.5 1874.42,-902.86" /> <ellipse fill="none" stroke="black" cx="1868.7" cy="-902.78" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="761.61,-883.17 760.89,-873.19 762.88,-873.05 763.6,-883.02 761.61,-883.17" /> <polyline fill="none" stroke="black" points="760.25,-878.25 765.24,-877.89" /> <polygon fill="black" stroke="black" points="766.59,-882.81 765.88,-872.83 767.87,-872.69 768.59,-882.66 766.59,-882.81" /> <polyline fill="none" stroke="black" points="765.24,-877.89 770.22,-877.53" /> <text xml:space="preserve" text-anchor="start" x="1250.12" y="-860.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_cancellation_billing_recurrence_id</text>
|
||||
</g>
|
||||
<!-- billingcancellation_6eedf614->registrar_6e1503e3 -->
|
||||
<g id="edge29" class="edge">
|
||||
<title>billingcancellation_6eedf614:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M1871.01,-1027.94C1863.1,-1042.26 1879.59,-1076.77 1858.75,-1093 1764.9,-1166.07 1711.29,-1113.99 1592.5,-1120 1465.52,-1126.42 570.05,-1156.46 448.25,-1120 336.65,-1086.6 288.74,-1076.15 229.25,-976 212.99,-948.62 233.81,-710.69 211.1,-669.07" /> <polygon fill="black" stroke="black" points="1875.39,-1025.88 1886.36,-1025.72 1880.22,-1023.62 1884.14,-1021.78 1884.14,-1021.78 1884.14,-1021.78 1880.22,-1023.62 1882.54,-1017.57 1875.39,-1025.88" /> <polygon fill="black" stroke="black" points="1871.26,-1022.3 1875.5,-1031.35 1873.69,-1032.2 1869.45,-1023.15 1871.26,-1022.3" /> <polyline fill="none" stroke="black" points="1874.29,-1026.4 1869.76,-1028.52" /> <polygon fill="black" stroke="black" points="200.94,-667.42 207.13,-659.57 208.7,-660.81 202.51,-668.66 200.94,-667.42" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.17,-665.97" /> <polygon fill="black" stroke="black" points="204.86,-670.52 211.06,-662.67 212.63,-663.91 206.43,-671.76 204.86,-670.52" /> <polyline fill="none" stroke="black" points="207.17,-665.97 211.1,-669.07" /> <text xml:space="preserve" text-anchor="start" x="866.5" y="-1140.18" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_cancellation_registrar_id</text>
|
||||
<title>billingcancellation_6eedf614:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M1871.01,-1027.94C1863.1,-1042.26 1879.59,-1076.77 1858.75,-1093 1764.9,-1166.07 1711.29,-1113.99 1592.5,-1120 1405.62,-1129.45 491.29,-1131.02 448.25,-1118 336.52,-1084.19 288.71,-1073.46 229.25,-973 213,-945.54 233.9,-706.66 210.95,-665.81" /> <polygon fill="black" stroke="black" points="1875.39,-1025.88 1886.36,-1025.72 1880.22,-1023.62 1884.14,-1021.78 1884.14,-1021.78 1884.14,-1021.78 1880.22,-1023.62 1882.54,-1017.57 1875.39,-1025.88" /> <polygon fill="black" stroke="black" points="1871.26,-1022.3 1875.5,-1031.35 1873.69,-1032.2 1869.45,-1023.15 1871.26,-1022.3" /> <polyline fill="none" stroke="black" points="1874.29,-1026.4 1869.76,-1028.52" /> <polygon fill="black" stroke="black" points="200.99,-664.45 207.09,-656.52 208.68,-657.74 202.58,-665.67 200.99,-664.45" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.21,-662.93" /> <polygon fill="black" stroke="black" points="204.95,-667.5 211.05,-659.57 212.64,-660.79 206.54,-668.72 204.95,-667.5" /> <polyline fill="none" stroke="black" points="207.21,-662.93 211.17,-665.98" /> <text xml:space="preserve" text-anchor="start" x="866.5" y="-1129.88" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_cancellation_registrar_id</text>
|
||||
</g>
|
||||
<!-- graceperiod_cd3b2e8f -->
|
||||
<g id="node6" class="node">
|
||||
@@ -399,15 +399,15 @@ td.section {
|
||||
</g>
|
||||
<!-- graceperiod_cd3b2e8f->billingrecurrence_5fa2cb01 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>graceperiod_cd3b2e8f:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M3222.93,-691.41C3113.42,-694.69 3071.21,-723.99 2963,-678 2952.87,-673.7 2955.15,-665.24 2945,-661 2763.12,-585.01 1367.79,-640.73 1171,-652 999.57,-661.82 910.21,-568.18 786.25,-687 757.51,-714.55 795.98,-854.03 769.54,-877.8" /> <polygon fill="black" stroke="black" points="3231.92,-691.28 3241.98,-695.63 3237.25,-691.2 3241.58,-691.13 3241.58,-691.13 3241.58,-691.13 3237.25,-691.2 3241.85,-686.63 3231.92,-691.28" /> <ellipse fill="none" stroke="black" cx="3226.2" cy="-691.36" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="762.93,-885.59 759.45,-876.21 761.32,-875.52 764.8,-884.89 762.93,-885.59" /> <polyline fill="none" stroke="black" points="760.25,-881.25 764.94,-879.51" /> <polygon fill="black" stroke="black" points="767.61,-883.85 764.14,-874.47 766.01,-873.78 769.49,-883.15 767.61,-883.85" /> <polyline fill="none" stroke="black" points="764.94,-879.51 769.63,-877.77" /> <text xml:space="preserve" text-anchor="start" x="1925.38" y="-629.59" font-family="Helvetica,sans-Serif" font-size="14.00">fk_grace_period_billing_recurrence_id</text>
|
||||
<title>graceperiod_cd3b2e8f:w->billingrecurrence_5fa2cb01:e</title> <path fill="none" stroke="black" d="M3222.93,-691.41C3113.42,-694.69 3071.21,-723.99 2963,-678 2952.87,-673.7 2955.15,-665.24 2945,-661 2763.12,-585.01 1367.8,-641.84 1171,-653 999.61,-662.72 910.45,-568.5 786.25,-687 757.88,-714.06 795.49,-851.44 769.4,-874.86" /> <polygon fill="black" stroke="black" points="3231.92,-691.28 3241.98,-695.63 3237.25,-691.2 3241.58,-691.13 3241.58,-691.13 3241.58,-691.13 3237.25,-691.2 3241.85,-686.63 3231.92,-691.28" /> <ellipse fill="none" stroke="black" cx="3226.2" cy="-691.36" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="762.93,-882.59 759.45,-873.21 761.32,-872.52 764.8,-881.89 762.93,-882.59" /> <polyline fill="none" stroke="black" points="760.25,-878.25 764.94,-876.51" /> <polygon fill="black" stroke="black" points="767.61,-880.85 764.14,-871.48 766.01,-870.78 769.49,-880.16 767.61,-880.85" /> <polyline fill="none" stroke="black" points="764.94,-876.51 769.63,-874.77" /> <text xml:space="preserve" text-anchor="start" x="1925.38" y="-630.25" font-family="Helvetica,sans-Serif" font-size="14.00">fk_grace_period_billing_recurrence_id</text>
|
||||
</g>
|
||||
<!-- graceperiod_cd3b2e8f->registrar_6e1503e3 -->
|
||||
<g id="edge38" class="edge">
|
||||
<title>graceperiod_cd3b2e8f:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3227.25,-671.58C3156.88,-673.26 3011.82,-683.75 2963,-653 2949.19,-644.3 2959.04,-629.32 2945,-621 2737.34,-497.91 2100.15,-609.67 1858.75,-608.25 1748.42,-607.6 1720.83,-607.33 1610.5,-608.25 1244.13,-611.31 1152.32,-606.8 786.25,-622 538.36,-632.29 475.93,-632.47 229.25,-659 221.81,-659.8 218.03,-661.17 213.2,-662.04" /> <polygon fill="black" stroke="black" points="3231.92,-671.52 3241.98,-675.88 3237.25,-671.44 3241.58,-671.38 3241.58,-671.38 3241.58,-671.38 3237.25,-671.44 3241.85,-666.88 3231.92,-671.52" /> <polygon fill="black" stroke="black" points="3229.63,-666.55 3229.77,-676.55 3227.77,-676.58 3227.63,-666.58 3229.63,-666.55" /> <polyline fill="none" stroke="black" points="3230.7,-671.53 3225.7,-671.6" /> <polygon fill="black" stroke="black" points="204.66,-667.77 203.83,-657.81 205.82,-657.64 206.66,-667.61 204.66,-667.77" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.23,-662.46" /> <polygon fill="black" stroke="black" points="209.65,-667.36 208.81,-657.39 210.81,-657.23 211.64,-667.19 209.65,-667.36" /> <polyline fill="none" stroke="black" points="208.23,-662.46 213.22,-662.04" /> <text xml:space="preserve" text-anchor="start" x="1648.75" y="-610.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_grace_period_registrar_id</text>
|
||||
<title>graceperiod_cd3b2e8f:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3227.25,-671.58C3156.88,-673.26 3011.82,-683.75 2963,-653 2949.19,-644.3 2959.04,-629.32 2945,-621 2737.34,-497.91 2100.15,-609.67 1858.75,-608.25 1748.42,-607.6 1720.83,-607.26 1610.5,-608.25 1244.12,-611.53 1152.37,-608.88 786.25,-623 538.44,-632.55 475.91,-630.36 229.25,-656 221.81,-656.77 218.02,-658.15 213.2,-659.03" /> <polygon fill="black" stroke="black" points="3231.92,-671.52 3241.98,-675.88 3237.25,-671.44 3241.58,-671.38 3241.58,-671.38 3241.58,-671.38 3237.25,-671.44 3241.85,-666.88 3231.92,-671.52" /> <polygon fill="black" stroke="black" points="3229.63,-666.55 3229.77,-676.55 3227.77,-676.58 3227.63,-666.58 3229.63,-666.55" /> <polyline fill="none" stroke="black" points="3230.7,-671.53 3225.7,-671.6" /> <polygon fill="black" stroke="black" points="204.67,-664.77 203.82,-654.81 205.82,-654.64 206.66,-664.6 204.67,-664.77" /> <polyline fill="none" stroke="black" points="203.25,-659.88 208.23,-659.45" /> <polygon fill="black" stroke="black" points="209.65,-664.35 208.81,-654.39 210.8,-654.22 211.64,-664.18 209.65,-664.35" /> <polyline fill="none" stroke="black" points="208.23,-659.45 213.21,-659.03" /> <text xml:space="preserve" text-anchor="start" x="1648.75" y="-610.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_grace_period_registrar_id</text>
|
||||
</g>
|
||||
<!-- billingrecurrence_5fa2cb01->registrar_6e1503e3 -->
|
||||
<g id="edge31" class="edge">
|
||||
<title>billingrecurrence_5fa2cb01:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M440.92,-861.17C333.52,-856.34 306.18,-799.52 229.25,-714 215,-698.16 224.22,-673.08 212.74,-665.27" /> <polygon fill="black" stroke="black" points="445.92,-861.27 455.82,-865.99 451.25,-861.39 455.58,-861.49 455.58,-861.49 455.58,-861.49 451.25,-861.39 456.01,-856.99 445.92,-861.27" /> <polygon fill="black" stroke="black" points="443.81,-856.23 443.59,-866.23 441.59,-866.18 441.81,-856.18 443.81,-856.23" /> <polyline fill="none" stroke="black" points="444.7,-861.25 439.7,-861.14" /> <polygon fill="black" stroke="black" points="202.99,-667.97 205.44,-658.27 207.38,-658.76 204.93,-668.46 202.99,-667.97" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.1,-664.1" /> <polygon fill="black" stroke="black" points="207.84,-669.19 210.29,-659.5 212.23,-659.99 209.78,-669.68 207.84,-669.19" /> <polyline fill="none" stroke="black" points="208.1,-664.1 212.95,-665.33" /> <text xml:space="preserve" text-anchor="start" x="230" y="-862.85" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_recurrence_registrar_id</text>
|
||||
<title>billingrecurrence_5fa2cb01:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M440.92,-858.17C333.52,-853.34 306.18,-796.52 229.25,-711 215,-695.16 224.22,-670.08 212.74,-662.27" /> <polygon fill="black" stroke="black" points="445.92,-858.27 455.82,-862.99 451.25,-858.39 455.58,-858.49 455.58,-858.49 455.58,-858.49 451.25,-858.39 456.01,-853.99 445.92,-858.27" /> <polygon fill="black" stroke="black" points="443.81,-853.23 443.59,-863.23 441.59,-863.18 441.81,-853.18 443.81,-853.23" /> <polyline fill="none" stroke="black" points="444.7,-858.25 439.7,-858.14" /> <polygon fill="black" stroke="black" points="202.99,-664.97 205.44,-655.27 207.38,-655.76 204.93,-665.46 202.99,-664.97" /> <polyline fill="none" stroke="black" points="203.25,-659.88 208.1,-661.1" /> <polygon fill="black" stroke="black" points="207.84,-666.19 210.29,-656.5 212.23,-656.99 209.78,-666.68 207.84,-666.19" /> <polyline fill="none" stroke="black" points="208.1,-661.1 212.95,-662.33" /> <text xml:space="preserve" text-anchor="start" x="230" y="-859.85" font-family="Helvetica,sans-Serif" font-size="14.00">fk_billing_recurrence_registrar_id</text>
|
||||
</g>
|
||||
<!-- bsadomainrefresh_c8f4c45d -->
|
||||
<g id="node8" class="node">
|
||||
@@ -451,11 +451,11 @@ td.section {
|
||||
</g>
|
||||
<!-- user_f2216f01 -->
|
||||
<g id="node47" class="node">
|
||||
<title>user_f2216f01</title> <polygon fill="#e9c2f2" stroke="none" points="3840,-1807 3840,-1826.75 3946.25,-1826.75 3946.25,-1807 3840,-1807" /> <text xml:space="preserve" text-anchor="start" x="3842" y="-1812.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."User"</text> <polygon fill="#e9c2f2" stroke="none" points="3946.25,-1807 3946.25,-1826.75 4019.25,-1826.75 4019.25,-1807 3946.25,-1807" /> <text xml:space="preserve" text-anchor="start" x="3980.5" y="-1811.45" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="3842" y="-1792.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">email_address</text> <text xml:space="preserve" text-anchor="start" x="3940.5" y="-1791.7" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="3948.25" y="-1791.7" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <polygon fill="none" stroke="#888888" points="3839,-1786.25 3839,-1827.75 4020.25,-1827.75 4020.25,-1786.25 3839,-1786.25" />
|
||||
<title>user_f2216f01</title> <polygon fill="#e9c2f2" stroke="none" points="3801.38,-1807.88 3801.38,-1827.62 3984.88,-1827.62 3984.88,-1807.88 3801.38,-1807.88" /> <text xml:space="preserve" text-anchor="start" x="3803.38" y="-1813.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."User"</text> <polygon fill="#e9c2f2" stroke="none" points="3984.88,-1807.88 3984.88,-1827.62 4057.88,-1827.62 4057.88,-1807.88 3984.88,-1807.88" /> <text xml:space="preserve" text-anchor="start" x="4019.12" y="-1812.33" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="3803.38" y="-1793.58" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">email_address</text> <text xml:space="preserve" text-anchor="start" x="3979.12" y="-1792.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="3986.88" y="-1792.58" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="3803.38" y="-1772.83" font-family="Helvetica,sans-Serif" font-size="14.00">registry_lock_email_address</text> <text xml:space="preserve" text-anchor="start" x="3979.12" y="-1772.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="3986.88" y="-1772.83" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="3800.38,-1767.38 3800.38,-1828.62 4058.88,-1828.62 4058.88,-1767.38 3800.38,-1767.38" />
|
||||
</g>
|
||||
<!-- consoleupdatehistory_5237b2aa->user_f2216f01 -->
|
||||
<g id="edge55" class="edge">
|
||||
<title>consoleupdatehistory_5237b2aa:w->user_f2216f01:e</title> <path fill="none" stroke="black" d="M4397.75,-1797.5C4238.8,-1797.48 4191.43,-1797.14 4030.03,-1797.13" /> <polygon fill="black" stroke="black" points="4402.42,-1797.5 4412.42,-1802 4407.75,-1797.5 4412.08,-1797.5 4412.08,-1797.5 4412.08,-1797.5 4407.75,-1797.5 4412.42,-1793 4402.42,-1797.5" /> <polygon fill="black" stroke="black" points="4400.2,-1792.5 4400.2,-1802.5 4398.2,-1802.5 4398.2,-1792.5 4400.2,-1792.5" /> <polyline fill="none" stroke="black" points="4401.2,-1797.5 4396.2,-1797.5" /> <polygon fill="black" stroke="black" points="4021.25,-1802.13 4021.25,-1792.13 4023.25,-1792.13 4023.25,-1802.13 4021.25,-1802.13" /> <polyline fill="none" stroke="black" points="4020.25,-1797.12 4025.25,-1797.13" /> <polygon fill="black" stroke="black" points="4026.25,-1802.13 4026.25,-1792.13 4028.25,-1792.13 4028.25,-1802.13 4026.25,-1802.13" /> <polyline fill="none" stroke="black" points="4025.25,-1797.13 4030.25,-1797.13" /> <text xml:space="preserve" text-anchor="start" x="4129" y="-1799.94" font-family="Helvetica,sans-Serif" font-size="14.00">fk_console_update_history_acting_user</text>
|
||||
<title>consoleupdatehistory_5237b2aa:w->user_f2216f01:e</title> <path fill="none" stroke="black" d="M4397.47,-1797.5C4255.84,-1797.53 4212.77,-1797.98 4068.52,-1798" /> <polygon fill="black" stroke="black" points="4402.42,-1797.5 4412.42,-1802 4407.75,-1797.5 4412.08,-1797.5 4412.08,-1797.5 4412.08,-1797.5 4407.75,-1797.5 4412.42,-1793 4402.42,-1797.5" /> <polygon fill="black" stroke="black" points="4400.2,-1792.5 4400.2,-1802.5 4398.2,-1802.5 4398.2,-1792.5 4400.2,-1792.5" /> <polyline fill="none" stroke="black" points="4401.2,-1797.5 4396.2,-1797.5" /> <polygon fill="black" stroke="black" points="4059.88,-1803 4059.87,-1793 4061.87,-1793 4061.88,-1803 4059.88,-1803" /> <polyline fill="none" stroke="black" points="4058.88,-1798 4063.87,-1798" /> <polygon fill="black" stroke="black" points="4064.88,-1803 4064.87,-1793 4066.87,-1793 4066.88,-1803 4064.88,-1803" /> <polyline fill="none" stroke="black" points="4063.87,-1798 4068.87,-1798" /> <text xml:space="preserve" text-anchor="start" x="4129" y="-1800.41" font-family="Helvetica,sans-Serif" font-size="14.00">fk_console_update_history_acting_user</text>
|
||||
</g>
|
||||
<!-- cursor_6af40e8c -->
|
||||
<g id="node16" class="node">
|
||||
@@ -495,15 +495,15 @@ td.section {
|
||||
</g>
|
||||
<!-- host_f21b78de->registrar_6e1503e3 -->
|
||||
<g id="edge39" class="edge">
|
||||
<title>host_f21b78de:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3198.12,-512.36C3012.86,-512.16 2959.48,-509 2765,-509 2765,-509 2765,-509 607.25,-509 430.02,-509 379.12,-541.4 229.25,-636 218.71,-642.65 218.7,-654.44 212.35,-659.92" /> <polygon fill="black" stroke="black" points="3207.04,-512.37 3217.04,-516.87 3212.38,-512.37 3216.71,-512.37 3216.71,-512.37 3216.71,-512.37 3212.38,-512.37 3217.04,-507.87 3207.04,-512.37" /> <ellipse fill="none" stroke="black" cx="3201.32" cy="-512.37" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="205.74,-667.32 202.66,-657.81 204.56,-657.19 207.65,-666.71 205.74,-667.32" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.01,-661.33" /> <polygon fill="black" stroke="black" points="210.5,-665.78 207.41,-656.27 209.32,-655.65 212.4,-665.16 210.5,-665.78" /> <polyline fill="none" stroke="black" points="208.01,-661.33 212.76,-659.79" /> <text xml:space="preserve" text-anchor="start" x="1647.62" y="-511.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_host_creation_registrar_id</text>
|
||||
<title>host_f21b78de:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3198.12,-512.36C3012.86,-512.16 2959.48,-509 2765,-509 2765,-509 2765,-509 607.25,-509 430.3,-509 379.99,-541.33 229.25,-634 219.05,-640.27 218.6,-651.29 212.63,-656.7" /> <polygon fill="black" stroke="black" points="3207.04,-512.37 3217.04,-516.87 3212.38,-512.37 3216.71,-512.37 3216.71,-512.37 3216.71,-512.37 3212.38,-512.37 3217.04,-507.87 3207.04,-512.37" /> <ellipse fill="none" stroke="black" cx="3201.32" cy="-512.37" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="205.8,-664.29 202.59,-654.82 204.49,-654.18 207.7,-663.65 205.8,-664.29" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.99,-658.27" /> <polygon fill="black" stroke="black" points="210.54,-662.68 207.33,-653.21 209.22,-652.57 212.43,-662.04 210.54,-662.68" /> <polyline fill="none" stroke="black" points="207.99,-658.27 212.72,-656.66" /> <text xml:space="preserve" text-anchor="start" x="1647.62" y="-511.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_host_creation_registrar_id</text>
|
||||
</g>
|
||||
<!-- host_f21b78de->registrar_6e1503e3 -->
|
||||
<g id="edge40" class="edge">
|
||||
<title>host_f21b78de:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3198.12,-472.87C3012.87,-472.69 2959.48,-470 2765,-470 2765,-470 2765,-470 607.25,-470 437.42,-470 351.76,-408.37 229.25,-526 187.29,-566.29 255.87,-651.68 212.99,-661.87" /> <polygon fill="black" stroke="black" points="3207.04,-472.87 3217.04,-477.37 3212.38,-472.87 3216.71,-472.87 3216.71,-472.87 3216.71,-472.87 3212.38,-472.87 3217.04,-468.37 3207.04,-472.87" /> <ellipse fill="none" stroke="black" cx="3201.32" cy="-472.87" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="204.76,-667.75 203.73,-657.8 205.72,-657.59 206.75,-667.54 204.76,-667.75" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.22,-662.36" /> <polygon fill="black" stroke="black" points="209.73,-667.24 208.71,-657.29 210.7,-657.08 211.72,-667.03 209.73,-667.24" /> <polyline fill="none" stroke="black" points="208.22,-662.36 213.2,-661.85" /> <text xml:space="preserve" text-anchor="start" x="1622.88" y="-472.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_host_current_sponsor_registrar_id</text>
|
||||
<title>host_f21b78de:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3198.12,-472.87C3012.87,-472.69 2959.48,-470 2765,-470 2765,-470 2765,-470 607.25,-470 437.42,-470 352,-408.63 229.25,-526 188.18,-565.27 254.54,-648.57 213.05,-658.83" /> <polygon fill="black" stroke="black" points="3207.04,-472.87 3217.04,-477.37 3212.38,-472.87 3216.71,-472.87 3216.71,-472.87 3216.71,-472.87 3212.38,-472.87 3217.04,-468.37 3207.04,-472.87" /> <ellipse fill="none" stroke="black" cx="3201.32" cy="-472.87" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="204.77,-664.74 203.72,-654.8 205.7,-654.59 206.76,-664.53 204.77,-664.74" /> <polyline fill="none" stroke="black" points="203.25,-659.88 208.22,-659.35" /> <polygon fill="black" stroke="black" points="209.75,-664.21 208.69,-654.27 210.68,-654.06 211.73,-664 209.75,-664.21" /> <polyline fill="none" stroke="black" points="208.22,-659.35 213.19,-658.82" /> <text xml:space="preserve" text-anchor="start" x="1622.88" y="-472.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_host_current_sponsor_registrar_id</text>
|
||||
</g>
|
||||
<!-- host_f21b78de->registrar_6e1503e3 -->
|
||||
<g id="edge41" class="edge">
|
||||
<title>host_f21b78de:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3198.12,-433.37C3012.87,-433.22 2959.48,-431 2765,-431 2765,-431 2765,-431 607.25,-431 439.04,-431 352.36,-335.37 229.25,-450 197.1,-479.94 242.63,-635.59 212.61,-659.73" /> <polygon fill="black" stroke="black" points="3207.04,-433.37 3217.04,-437.87 3212.38,-433.37 3216.71,-433.37 3216.71,-433.37 3216.71,-433.37 3212.38,-433.37 3217.04,-428.87 3207.04,-433.37" /> <ellipse fill="none" stroke="black" cx="3201.32" cy="-433.37" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="205.79,-667.3 202.6,-657.82 204.5,-657.18 207.69,-666.66 205.79,-667.3" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.99,-661.28" /> <polygon fill="black" stroke="black" points="210.53,-665.7 207.34,-656.22 209.24,-655.59 212.43,-665.06 210.53,-665.7" /> <polyline fill="none" stroke="black" points="207.99,-661.28 212.73,-659.69" /> <text xml:space="preserve" text-anchor="start" x="1621.75" y="-433.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_host_last_epp_update_registrar_id</text>
|
||||
<title>host_f21b78de:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3198.12,-433.37C3012.87,-433.22 2959.48,-431 2765,-431 2765,-431 2765,-431 607.25,-431 439.04,-431 352.46,-335.48 229.25,-450 197.52,-479.5 242.13,-632.97 212.48,-656.77" /> <polygon fill="black" stroke="black" points="3207.04,-433.37 3217.04,-437.87 3212.38,-433.37 3216.71,-433.37 3216.71,-433.37 3216.71,-433.37 3212.38,-433.37 3217.04,-428.87 3207.04,-433.37" /> <ellipse fill="none" stroke="black" cx="3201.32" cy="-433.37" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="205.79,-664.3 202.6,-654.82 204.5,-654.18 207.69,-663.66 205.79,-664.3" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.99,-658.28" /> <polygon fill="black" stroke="black" points="210.53,-662.7 207.34,-653.22 209.24,-652.59 212.43,-662.07 210.53,-662.7" /> <polyline fill="none" stroke="black" points="207.99,-658.28 212.73,-656.69" /> <text xml:space="preserve" text-anchor="start" x="1621.75" y="-433.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_host_last_epp_update_registrar_id</text>
|
||||
</g>
|
||||
<!-- pollmessage_614a523e -->
|
||||
<g id="node21" class="node">
|
||||
@@ -531,15 +531,15 @@ td.section {
|
||||
</g>
|
||||
<!-- pollmessage_614a523e->registrar_6e1503e3 -->
|
||||
<g id="edge43" class="edge">
|
||||
<title>pollmessage_614a523e:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M4385.96,-584.85C4342.62,-692.42 4119.17,-1408 4111,-1416 4051.03,-1474.73 4014.57,-1472 3930.62,-1472 3930.62,-1472 3930.62,-1472 607.25,-1472 431.9,-1472 336.59,-1497.65 229.25,-1359 207.04,-1330.31 237.64,-743.59 209.66,-670.36" /> <polygon fill="black" stroke="black" points="4388.1,-580.6 4396.62,-573.7 4390.5,-575.84 4392.45,-571.97 4392.45,-571.97 4392.45,-571.97 4390.5,-575.84 4388.58,-569.65 4388.1,-580.6" /> <polygon fill="black" stroke="black" points="4382.64,-580.34 4391.57,-584.84 4390.67,-586.62 4381.74,-582.12 4382.64,-580.34" /> <polyline fill="none" stroke="black" points="4387.55,-581.69 4385.3,-586.16" /> <polygon fill="black" stroke="black" points="200.1,-666.89 207.7,-660.38 209,-661.9 201.4,-668.41 200.1,-666.89" /> <polyline fill="none" stroke="black" points="203.25,-662.88 206.5,-666.67" /> <polygon fill="black" stroke="black" points="203.36,-670.68 210.95,-664.18 212.25,-665.7 204.66,-672.2 203.36,-670.68" /> <polyline fill="none" stroke="black" points="206.5,-666.67 209.76,-670.47" /> <text xml:space="preserve" text-anchor="start" x="2304.38" y="-1474.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_poll_message_registrar_id</text>
|
||||
<title>pollmessage_614a523e:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M4385.96,-584.85C4342.62,-692.42 4119.17,-1408 4111,-1416 4051.03,-1474.73 4014.57,-1472 3930.62,-1472 3930.62,-1472 3930.62,-1472 607.25,-1472 431.9,-1472 336.58,-1497.66 229.25,-1359 206.95,-1330.19 237.77,-740.93 209.69,-667.39" /> <polygon fill="black" stroke="black" points="4388.1,-580.6 4396.62,-573.7 4390.5,-575.84 4392.45,-571.97 4392.45,-571.97 4392.45,-571.97 4390.5,-575.84 4388.58,-569.65 4388.1,-580.6" /> <polygon fill="black" stroke="black" points="4382.64,-580.34 4391.57,-584.84 4390.67,-586.62 4381.74,-582.12 4382.64,-580.34" /> <polyline fill="none" stroke="black" points="4387.55,-581.69 4385.3,-586.16" /> <polygon fill="black" stroke="black" points="200.1,-663.89 207.7,-657.38 209,-658.9 201.4,-665.41 200.1,-663.89" /> <polyline fill="none" stroke="black" points="203.25,-659.88 206.5,-663.67" /> <polygon fill="black" stroke="black" points="203.36,-667.68 210.95,-661.18 212.25,-662.7 204.66,-669.2 203.36,-667.68" /> <polyline fill="none" stroke="black" points="206.5,-663.67 209.76,-667.47" /> <text xml:space="preserve" text-anchor="start" x="2304.38" y="-1474.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_poll_message_registrar_id</text>
|
||||
</g>
|
||||
<!-- pollmessage_614a523e->registrar_6e1503e3 -->
|
||||
<g id="edge44" class="edge">
|
||||
<title>pollmessage_614a523e:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M4376.88,-441.94C4374.48,-438.2 4371.72,-434.17 4366.75,-431 4261.03,-363.61 4231.82,-345.47 4111,-312 4033.32,-290.48 4011.24,-293 3930.62,-293 3930.62,-293 3930.62,-293 607.25,-293 523.12,-293 289.3,-255.08 229.25,-314 203.73,-339.05 236.05,-613.25 211.14,-657" /> <polygon fill="black" stroke="black" points="4384.24,-447.01 4389.92,-456.39 4388.63,-450.04 4392.2,-452.5 4392.2,-452.5 4392.2,-452.5 4388.63,-450.04 4395.03,-448.98 4384.24,-447.01" /> <ellipse fill="none" stroke="black" cx="4379.53" cy="-443.77" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="207.04,-666.29 201.07,-658.27 202.67,-657.07 208.64,-665.09 207.04,-666.29" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.26,-659.89" /> <polygon fill="black" stroke="black" points="211.05,-663.3 205.08,-655.28 206.68,-654.09 212.65,-662.11 211.05,-663.3" /> <polyline fill="none" stroke="black" points="207.26,-659.89 211.27,-656.9" /> <text xml:space="preserve" text-anchor="start" x="2220.75" y="-295.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_poll_message_transfer_response_gaining_registrar_id</text>
|
||||
<title>pollmessage_614a523e:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M4376.88,-441.94C4374.48,-438.2 4371.72,-434.17 4366.75,-431 4261.03,-363.61 4231.82,-345.47 4111,-312 4033.32,-290.48 4011.24,-293 3930.62,-293 3930.62,-293 3930.62,-293 607.25,-293 523.12,-293 289.32,-255.1 229.25,-314 203.94,-338.82 235.81,-610.67 211.07,-654.05" /> <polygon fill="black" stroke="black" points="4384.24,-447.01 4389.92,-456.39 4388.63,-450.04 4392.2,-452.5 4392.2,-452.5 4392.2,-452.5 4388.63,-450.04 4395.03,-448.98 4384.24,-447.01" /> <ellipse fill="none" stroke="black" cx="4379.53" cy="-443.77" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="207.04,-663.29 201.07,-655.27 202.67,-654.07 208.64,-662.09 207.04,-663.29" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.26,-656.89" /> <polygon fill="black" stroke="black" points="211.05,-660.3 205.08,-652.28 206.68,-651.09 212.65,-659.11 211.05,-660.3" /> <polyline fill="none" stroke="black" points="207.26,-656.89 211.27,-653.9" /> <text xml:space="preserve" text-anchor="start" x="2220.75" y="-295.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_poll_message_transfer_response_gaining_registrar_id</text>
|
||||
</g>
|
||||
<!-- pollmessage_614a523e->registrar_6e1503e3 -->
|
||||
<g id="edge45" class="edge">
|
||||
<title>pollmessage_614a523e:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M4374.69,-426.76C4358.35,-406.75 4399.85,-344.61 4366.75,-314 4223.1,-181.16 4126.28,-254 3930.62,-254 3930.62,-254 3930.62,-254 607.25,-254 523.09,-254 288.95,-217.69 229.25,-277 201.03,-305.04 239.61,-611.88 211.39,-657.28" /> <polygon fill="black" stroke="black" points="4383,-429.69 4390.94,-437.26 4388.03,-431.46 4392.12,-432.9 4392.12,-432.9 4392.12,-432.9 4388.03,-431.46 4393.93,-428.77 4383,-429.69" /> <ellipse fill="none" stroke="black" cx="4377.61" cy="-427.79" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="206.91,-666.43 201.24,-658.19 202.89,-657.06 208.55,-665.3 206.91,-666.43" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.37,-660.04" /> <polygon fill="black" stroke="black" points="211.03,-663.6 205.36,-655.36 207.01,-654.22 212.68,-662.46 211.03,-663.6" /> <polyline fill="none" stroke="black" points="207.37,-660.04 211.49,-657.21" /> <text xml:space="preserve" text-anchor="start" x="2224.88" y="-256.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_poll_message_transfer_response_losing_registrar_id</text>
|
||||
<title>pollmessage_614a523e:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M4374.69,-426.76C4358.35,-406.75 4399.85,-344.61 4366.75,-314 4223.1,-181.16 4126.28,-254 3930.62,-254 3930.62,-254 3930.62,-254 607.25,-254 523.09,-254 288.97,-217.7 229.25,-277 201.24,-304.81 239.35,-609.28 211.33,-654.32" /> <polygon fill="black" stroke="black" points="4383,-429.69 4390.94,-437.26 4388.03,-431.46 4392.12,-432.9 4392.12,-432.9 4392.12,-432.9 4388.03,-431.46 4393.93,-428.77 4383,-429.69" /> <ellipse fill="none" stroke="black" cx="4377.61" cy="-427.79" rx="4" ry="4" /> <polygon fill="black" stroke="black" points="206.91,-663.43 201.24,-655.19 202.89,-654.06 208.55,-662.3 206.91,-663.43" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.37,-657.04" /> <polygon fill="black" stroke="black" points="211.03,-660.6 205.36,-652.36 207.01,-651.22 212.68,-659.46 211.03,-660.6" /> <polyline fill="none" stroke="black" points="207.37,-657.04 211.49,-654.21" /> <text xml:space="preserve" text-anchor="start" x="2224.88" y="-256.45" font-family="Helvetica,sans-Serif" font-size="14.00">fk_poll_message_transfer_response_losing_registrar_id</text>
|
||||
</g>
|
||||
<!-- domaindsdatahistory_995b060d -->
|
||||
<g id="node22" class="node">
|
||||
@@ -579,7 +579,7 @@ td.section {
|
||||
</g>
|
||||
<!-- hosthistory_56210c2->registrar_6e1503e3 -->
|
||||
<g id="edge42" class="edge">
|
||||
<title>hosthistory_56210c2:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3767.77,-434.3C3652.11,-429.35 3627.65,-368.61 3505.25,-350 3116.46,-290.88 2128.88,-339 1735.62,-339 1735.62,-339 1735.62,-339 607.25,-339 437.73,-339 346.75,-267.81 229.25,-390 189.58,-431.25 255.26,-635.83 212.85,-660.45" /> <polygon fill="black" stroke="black" points="3772.79,-434.41 3782.7,-439.12 3778.13,-434.52 3782.46,-434.61 3782.46,-434.61 3782.46,-434.61 3778.13,-434.52 3782.88,-430.12 3772.79,-434.41" /> <polygon fill="black" stroke="black" points="3770.68,-429.36 3770.47,-439.36 3768.47,-439.32 3768.68,-429.32 3770.68,-429.36" /> <polyline fill="none" stroke="black" points="3771.57,-434.38 3766.58,-434.28" /> <polygon fill="black" stroke="black" points="205.44,-667.48 202.99,-657.78 204.93,-657.29 207.38,-666.99 205.44,-667.48" /> <polyline fill="none" stroke="black" points="203.25,-662.88 208.1,-661.65" /> <polygon fill="black" stroke="black" points="210.29,-666.25 207.84,-656.56 209.78,-656.07 212.23,-665.76 210.29,-666.25" /> <polyline fill="none" stroke="black" points="208.1,-661.65 212.95,-660.43" /> <text xml:space="preserve" text-anchor="start" x="1973.38" y="-340" font-family="Helvetica,sans-Serif" font-size="14.00">fk_history_registrar_id</text>
|
||||
<title>hosthistory_56210c2:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M3767.77,-434.3C3652.11,-429.35 3627.65,-368.61 3505.25,-350 3116.46,-290.88 2128.88,-339 1735.62,-339 1735.62,-339 1735.62,-339 607.25,-339 437.73,-339 346.82,-267.87 229.25,-390 190,-430.78 254.72,-633.12 212.75,-657.48" /> <polygon fill="black" stroke="black" points="3772.79,-434.41 3782.7,-439.12 3778.13,-434.52 3782.46,-434.61 3782.46,-434.61 3782.46,-434.61 3778.13,-434.52 3782.88,-430.12 3772.79,-434.41" /> <polygon fill="black" stroke="black" points="3770.68,-429.36 3770.47,-439.36 3768.47,-439.32 3768.68,-429.32 3770.68,-429.36" /> <polyline fill="none" stroke="black" points="3771.57,-434.38 3766.58,-434.28" /> <polygon fill="black" stroke="black" points="205.44,-664.48 203,-654.78 204.93,-654.29 207.38,-663.99 205.44,-664.48" /> <polyline fill="none" stroke="black" points="203.25,-659.88 208.1,-658.65" /> <polygon fill="black" stroke="black" points="210.29,-663.25 207.84,-653.56 209.78,-653.07 212.23,-662.76 210.29,-663.25" /> <polyline fill="none" stroke="black" points="208.1,-658.65 212.95,-657.43" /> <text xml:space="preserve" text-anchor="start" x="1973.38" y="-340" font-family="Helvetica,sans-Serif" font-size="14.00">fk_history_registrar_id</text>
|
||||
</g>
|
||||
<!-- lock_f21d4861 -->
|
||||
<g id="node28" class="node">
|
||||
@@ -615,7 +615,7 @@ td.section {
|
||||
</g>
|
||||
<!-- registrarpoc_ab47054d->registrar_6e1503e3 -->
|
||||
<g id="edge46" class="edge">
|
||||
<title>registrarpoc_ab47054d:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M482.84,-171.05C370.61,-168.16 305.95,-136.4 229.25,-224.25 199.27,-258.59 245.16,-611.11 211.66,-657.78" /> <polygon fill="black" stroke="black" points="487.92,-171.11 497.86,-175.75 493.25,-171.18 497.58,-171.24 497.58,-171.24 497.58,-171.24 493.25,-171.18 497.97,-166.75 487.92,-171.11" /> <polygon fill="black" stroke="black" points="485.76,-166.09 485.63,-176.09 483.63,-176.06 483.76,-166.06 485.76,-166.09" /> <polyline fill="none" stroke="black" points="486.7,-171.1 481.7,-171.03" /> <polygon fill="black" stroke="black" points="206.7,-666.63 201.52,-658.08 203.23,-657.04 208.41,-665.6 206.7,-666.63" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.53,-660.29" /> <polygon fill="black" stroke="black" points="210.97,-664.04 205.79,-655.49 207.5,-654.45 212.68,-663.01 210.97,-664.04" /> <polyline fill="none" stroke="black" points="207.53,-660.29 211.8,-657.7" /> <text xml:space="preserve" text-anchor="start" x="243.88" y="-226.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_registrar_poc_registrar_id</text>
|
||||
<title>registrarpoc_ab47054d:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M482.84,-171.05C370.62,-168.16 305.97,-136.42 229.25,-224.25 199.47,-258.34 244.9,-608.47 211.6,-654.82" /> <polygon fill="black" stroke="black" points="487.92,-171.11 497.86,-175.75 493.25,-171.18 497.58,-171.24 497.58,-171.24 497.58,-171.24 493.25,-171.18 497.97,-166.75 487.92,-171.11" /> <polygon fill="black" stroke="black" points="485.76,-166.09 485.63,-176.09 483.63,-176.06 483.76,-166.06 485.76,-166.09" /> <polyline fill="none" stroke="black" points="486.7,-171.1 481.7,-171.03" /> <polygon fill="black" stroke="black" points="206.69,-663.63 201.52,-655.08 203.23,-654.04 208.41,-662.6 206.69,-663.63" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.53,-657.29" /> <polygon fill="black" stroke="black" points="210.97,-661.05 205.79,-652.49 207.5,-651.45 212.68,-660.01 210.97,-661.05" /> <polyline fill="none" stroke="black" points="207.53,-657.29 211.8,-654.7" /> <text xml:space="preserve" text-anchor="start" x="243.88" y="-226.7" font-family="Helvetica,sans-Serif" font-size="14.00">fk_registrar_poc_registrar_id</text>
|
||||
</g>
|
||||
<!-- registrarupdatehistory_8a38bed4 -->
|
||||
<g id="node36" class="node">
|
||||
@@ -623,7 +623,7 @@ td.section {
|
||||
</g>
|
||||
<!-- registrarupdatehistory_8a38bed4->registrar_6e1503e3 -->
|
||||
<g id="edge47" class="edge">
|
||||
<title>registrarupdatehistory_8a38bed4:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M449.35,-83.97C351.02,-82.05 294.37,-63.91 229.25,-143.25 194.65,-185.4 253.64,-610.45 211.9,-658.47" /> <polygon fill="black" stroke="black" points="454.17,-84.02 464.12,-88.62 459.5,-84.07 463.83,-84.12 463.83,-84.12 463.83,-84.12 459.5,-84.07 464.21,-79.62 454.17,-84.02" /> <polygon fill="black" stroke="black" points="452,-79 451.9,-89 449.9,-88.98 450,-78.98 452,-79" /> <polyline fill="none" stroke="black" points="452.95,-84.01 447.95,-83.96" /> <polygon fill="black" stroke="black" points="206.41,-666.88 201.87,-657.97 203.65,-657.06 208.19,-665.97 206.41,-666.88" /> <polyline fill="none" stroke="black" points="203.25,-662.88 207.71,-660.61" /> <polygon fill="black" stroke="black" points="210.87,-664.61 206.33,-655.7 208.11,-654.79 212.65,-663.7 210.87,-664.61" /> <polyline fill="none" stroke="black" points="207.71,-660.61 212.16,-658.34" /> <text xml:space="preserve" text-anchor="start" x="229.25" y="-145.7" font-family="Helvetica,sans-Serif" font-size="14.00">fkregistrarupdatehistoryregistrarid</text>
|
||||
<title>registrarupdatehistory_8a38bed4:w->registrar_6e1503e3:e</title> <path fill="none" stroke="black" d="M449.35,-83.97C351.02,-82.06 294.39,-63.91 229.25,-143.25 194.84,-185.16 253.36,-607.76 211.85,-655.49" /> <polygon fill="black" stroke="black" points="454.17,-84.02 464.12,-88.62 459.5,-84.07 463.83,-84.12 463.83,-84.12 463.83,-84.12 459.5,-84.07 464.21,-79.62 454.17,-84.02" /> <polygon fill="black" stroke="black" points="452,-79 451.9,-89 449.9,-88.98 450,-78.98 452,-79" /> <polyline fill="none" stroke="black" points="452.95,-84.01 447.95,-83.96" /> <polygon fill="black" stroke="black" points="206.41,-663.88 201.87,-654.97 203.65,-654.06 208.19,-662.97 206.41,-663.88" /> <polyline fill="none" stroke="black" points="203.25,-659.88 207.71,-657.61" /> <polygon fill="black" stroke="black" points="210.87,-661.61 206.33,-652.7 208.11,-651.79 212.65,-660.7 210.87,-661.61" /> <polyline fill="none" stroke="black" points="207.71,-657.61 212.16,-655.34" /> <text xml:space="preserve" text-anchor="start" x="229.25" y="-145.7" font-family="Helvetica,sans-Serif" font-size="14.00">fkregistrarupdatehistoryregistrarid</text>
|
||||
</g>
|
||||
<!-- registrarpocupdatehistory_31e5d9aa -->
|
||||
<g id="node37" class="node">
|
||||
@@ -4378,6 +4378,11 @@ td.section {
|
||||
<td class="minwidth"><b><i>email_address</i></b></td>
|
||||
<td class="minwidth">text not null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="spacer"></td>
|
||||
<td class="minwidth">registry_lock_email_address</td>
|
||||
<td class="minwidth">text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -222,3 +222,4 @@ V221__remove_contact_history.sql
|
||||
V222__remove_contact.sql
|
||||
V223__tld_change_xap_enabled_to_transitions.sql
|
||||
V224__add_registrar_expiry_access_period_enabled.sql
|
||||
V225__user_registry_lock_email_address_index.sql
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Copyright 2026 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.
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS user_registry_lock_email_address_idx ON "User"
|
||||
USING hash (registry_lock_email_address);
|
||||
@@ -43,7 +43,7 @@
|
||||
domain_repo_id text not null,
|
||||
event_time timestamp(6) with time zone not null,
|
||||
flags text[],
|
||||
reason text not null check ((reason in ('CREATE','ERROR','FEE_EARLY_ACCESS','RENEW','RESTORE','SERVER_STATUS','TRANSFER'))),
|
||||
reason text not null check ((reason in ('CREATE','ERROR','FEE_EARLY_ACCESS','FEE_EXPIRY_ACCESS','RENEW','RESTORE','SERVER_STATUS','TRANSFER'))),
|
||||
domain_name text not null,
|
||||
billing_event_id bigint,
|
||||
billing_recurrence_id bigint,
|
||||
@@ -58,7 +58,7 @@
|
||||
domain_repo_id text not null,
|
||||
event_time timestamp(6) with time zone not null,
|
||||
flags text[],
|
||||
reason text not null check ((reason in ('CREATE','ERROR','FEE_EARLY_ACCESS','RENEW','RESTORE','SERVER_STATUS','TRANSFER'))),
|
||||
reason text not null check ((reason in ('CREATE','ERROR','FEE_EARLY_ACCESS','FEE_EXPIRY_ACCESS','RENEW','RESTORE','SERVER_STATUS','TRANSFER'))),
|
||||
domain_name text not null,
|
||||
allocation_token text,
|
||||
billing_time timestamp(6) with time zone,
|
||||
@@ -78,7 +78,7 @@
|
||||
domain_repo_id text not null,
|
||||
event_time timestamp(6) with time zone not null,
|
||||
flags text[],
|
||||
reason text not null check ((reason in ('CREATE','ERROR','FEE_EARLY_ACCESS','RENEW','RESTORE','SERVER_STATUS','TRANSFER'))),
|
||||
reason text not null check ((reason in ('CREATE','ERROR','FEE_EARLY_ACCESS','FEE_EXPIRY_ACCESS','RENEW','RESTORE','SERVER_STATUS','TRANSFER'))),
|
||||
domain_name text not null,
|
||||
recurrence_end_time timestamp(6) with time zone,
|
||||
recurrence_last_expansion timestamp(6) with time zone not null,
|
||||
@@ -507,6 +507,7 @@
|
||||
creation_time timestamp(6) with time zone not null,
|
||||
drive_folder_id text,
|
||||
email_address text,
|
||||
expiry_access_period_enabled boolean not null,
|
||||
failover_client_certificate text,
|
||||
failover_client_certificate_hash text,
|
||||
fax_number text,
|
||||
@@ -646,6 +647,7 @@
|
||||
drive_folder_id text,
|
||||
eap_fee_schedule hstore not null,
|
||||
escrow_enabled boolean not null,
|
||||
expiry_access_period_transitions hstore not null,
|
||||
idn_tables text[],
|
||||
invoicing_enabled boolean not null,
|
||||
lordn_username text,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 17.4
|
||||
-- Dumped by pg_dump version 17.4
|
||||
-- Dumped from database version 17.10
|
||||
-- Dumped by pg_dump version 17.10
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
@@ -2611,6 +2611,13 @@ CREATE INDEX spec11threatmatch_registrar_id_idx ON public."Spec11ThreatMatch" US
|
||||
CREATE INDEX spec11threatmatch_tld_idx ON public."Spec11ThreatMatch" USING btree (tld);
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_registry_lock_email_address_idx; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX user_registry_lock_email_address_idx ON public."User" USING hash (registry_lock_email_address);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Domain fk2jc69qyg2tv9hhnmif6oa1cx1; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
Reference in New Issue
Block a user