1
0
mirror of https://github.com/google/nomulus synced 2026-07-19 14:32:44 +00:00

Compare commits

...

28 Commits

Author SHA1 Message Date
Ben McIlwain 847ef12a4f Remove Tld.allowedRegistrantContactIds field (#2867)
We no longer need this now that no contacts can be applied to any domains at all.

A follow-up PR in subsequent weeks will delete the column from the DB schema.

BUG= http://b/448619572
2025-10-31 15:52:10 +00:00
gbrodman d9349be18e Modify the way we load resources via foreign keys (#2852)
Previously, we would have separate database calls for mapping from
foreign key to repo ID and then from repo ID to object. This PR modifies
those calls to load the resource directly (the old system was an
artifact of the Datastore key-value storage system).

In this PR, we merge the load-resource-by-foreign-key calls into a
single database load, as well as adding a separate cache object for
(foreign key) -> (resource). Now we cache, and have separate cleaner
code paths, for fk -> resource, fk -> repo ID, and repo ID -> resource.

Also removes the unused RdeFragmenter class
2025-10-29 19:21:27 +00:00
gbrodman 0c74883428 Remove WHOIS references from activity reporting (#2865)
This also changes a screenshot test filename since there wasn't a great
other place to put that change
2025-10-29 16:42:16 +00:00
Weimin Yu b357fc79f7 Support Fee Extension standard in rfc 8748 (#2855)
* Support Fee Extension standard in rfc 8748

Adding support to the final version of RFC 8748.

Compared with draft-0.12, the only meaningful change is in the namespace.
The rest is either schema-tightening that reflects actual usage, or
optional server-side features that we do not support.

We reuse draft-0.12 tests, only changing namespace uris in the input and
output files for the new version.

* Addressing reviews
2025-10-28 20:54:02 +00:00
gbrodman 754e7fbddc Remove old console soy/js and related files (#2861)
We haven't been serving this for a while, let's finally get rid of them.

We keep some Soy rules around in the presubmits file because we use some
Soy files as XML templates for EPP actions.
2025-10-28 20:34:34 +00:00
Ben McIlwain ad07b32638 Refactor EppResourceUtils.loadByForeignKey(...) -> ForeignKeyUtils.loadResource(...) (#2864)
This doesn't make any underlying implementation details, and is mainly useful to
reduce the number of diffs in PR #2852 (which does change implementation
details) thus making that easier to review.
2025-10-28 19:47:37 +00:00
Ben McIlwain 8f69b48e87 Add a @GetterDelegate annotation for better handling of ImmutableObject fields (#2860)
This allows us to specify a getter delegation to bypass Hibernate's limitations
on field types for the purposes of, e.g., using a sorted set in toString()
output rather than the base Hibernate unsorted HashSet type.

BUG=http://b/448631639
2025-10-28 17:10:27 +00:00
Ben McIlwain c33f0dc07f Remove all foreign key constraints pointing to contact tables (#2857)
This affects FKs pointing to both Contact and ContactHistory. This is in
preparation to us deleting all rows in those two tables, and then subsequently
removing all application logic having to do with contacts entirely.
2025-10-27 20:34:50 +00:00
Juan Celhay 969353d4e2 Fix documentation for renew probers cb file (#2862) 2025-10-27 20:24:07 +00:00
gbrodman 6cd351ec7c Remove WHOIS classes and configuration (#2859)
This is steps one and two of b/454947209

We already haven't been serving WHOIS for a while, so there's no point
in keeping the old code around. This can simplify some code paths in the
future (like, certain foreign-key-loads that are only used in WHOIS
queries).
2025-10-27 18:57:25 +00:00
Pavlo Tkach 19e03dbd2e Update proxy and nomulus cluster resources (#2858) 2025-10-24 20:19:41 +00:00
Pavlo Tkach fc1eb162f2 Remove Primary Contact from users editing screen (#2856) 2025-10-24 20:12:18 +00:00
gbrodman ed25854fbc Add unique index for not-deleted domain names (#2853)
This is a backstop against multiple domain creations for the same domain
name getting through
2025-10-24 15:38:23 +00:00
Juan Celhay 0aa6bc6aaa Change regex format in release cb file (#2854) 2025-10-23 19:49:29 +00:00
Juan Celhay ff4c326ebe Delete step to push to release repo, trigger next release steps based on tag format (#2833)
* Change release cb file

* Add brackets around tag variable

* Redo tag matching

* Have tag matcher like the one in cb dev
2025-10-21 18:52:42 +00:00
Pavlo Tkach 51b579871a Anonymize support users in console history, add minor UI updates (#2851) 2025-10-17 18:57:40 +00:00
gbrodman b144aafb22 Use transaction time for deletion time cache ticker (#2848)
Basically, what happened is that the cache's expireAfterWrite was being
called some number of milliseconds (say, 50-100) after the transaction
was started. That method used the transaction time instead of the
current time, so as a result the entries were sticking around 50-100ms
longer in the cache than they should have been.

This fix contains two parts, each of which I believe would be sufficient
on their own to fix the issue:
1. Use the currentTime passed in in Expiry::expireAfterCreate
2. Use the transaction time in the cache's Ticker. This keeps everything
   on the same schedule.
2025-10-16 20:01:17 +00:00
Weimin Yu ddd955e156 Fix dependency of Gradle task for schema test (#2849)
Problem not showing up because all use cases run this test after
`build`.
2025-10-16 15:33:28 +00:00
gbrodman 6863f678f1 Allow Gradle to use more heap space (#2847)
During the release process, we are seeing the message "Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)" which seemingly can be caused by OOMs
2025-10-13 18:25:08 +00:00
gbrodman 6bd90e967b Add more hash indexes used during common flows (#2845)
I analyzed SQL statements run during the following flows and EXPLAIN
ANALYZEd each of them to figure out if there are any additional hash
indexes we could add that could be particularly helpful. Note: it's not
worth adding a hash index on the host_repo_id field in DomainHost
because so many rows (domains) use the same host.

- domain create
- domain delete
- domain info
- domain renew
- domain update
- host create
- host delete
- host update

I skipped the ones that use the read-only replica, as well as contact
flows (we're getting rid of them), and domain transfer/restore-related
flows as those are extremely infrequent.
2025-10-13 18:07:47 +00:00
gbrodman 5faf3d283c Differentiate between inserts and updates in flows (#2846)
Updates (AKA merges) run an extra SELECT statement to figure out if the
resource exists so that it can merge the entity into the existing object
in Hibernate's schema. When we're inserting new rows (such as new poll
messages or resource creates), we know that we don't need to do that
merge. Doing this should save us some SELECT statements (this has borne
out to be the truth in alpha)
2025-10-13 15:43:18 +00:00
gbrodman 149fb66ac5 Add cache for deletion times of existing domains (#2840)
This should help in instances of popular domains dropping, since we
won't need to do an additional two database loads every time (assuming
the deletion time is in the future).
2025-10-09 17:22:24 +00:00
gbrodman 8c96940a27 Only load from ClaimsList once when filling the cache (#2843) 2025-10-09 16:57:21 +00:00
Ben McIlwain 9c5510f05d Add a rate limiter to remove all domain contacts action (#2838)
The maximum QPS defaults to 10, but can also be specified at runtime through
use of a query-string parameter.

BUG = http://b/439636188
2025-10-02 22:15:19 +00:00
gbrodman 84884de77b Verify existence of TLDs and registrars for tokens (#2837)
Just in case someone makes a typo when running the commands
2025-10-02 20:10:58 +00:00
Ben McIlwain d6c35df9bc Ignore single domain failures in remove contacts from all domains action (#2836)
When running the action in sandbox on 1.5M domains, it failed a few times
updating individual domains (requiring a manual restart of the entire action).
It's better to just log the individual failures for manual inspection and then
otherwise continue running the action to process the vast majority of other
updates that won't fail.

BUG = http://b/439636188
2025-10-02 18:58:23 +00:00
Juan Celhay 7caa0ec9d6 Add environment configuration files to .gitignore (#2830)
* Add environment configuration files to .gitignore

* Delete config files from repo

* Refactor release cb file to delete config file lines from gitignore

* Reorder env files

* Add README for config files
2025-10-02 18:36:43 +00:00
Weimin Yu ee3866ec4a Allow top level tld creation in Sandbox (#2835)
Add a flag to the CreateCdnsTld command to bypass the dns name format
check in Sandbox (limiting names to `*.test.`). With this flag, we
can create TLDs for RST testing in Sandbox.

Note that if the new flag is wrongly set for a disallowed name, the
request to the Cloud DNS API will fail. The format check in the command
just provides a user-friendly error message.
2025-10-01 14:20:33 +00:00
365 changed files with 3526 additions and 24725 deletions
+7
View File
@@ -18,6 +18,13 @@ gjf.out
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Environment-specific configuration files
core/src/main/java/google/registry/config/files/nomulus-config-alpha.yaml
core/src/main/java/google/registry/config/files/nomulus-config-crash.yaml
core/src/main/java/google/registry/config/files/nomulus-config-production.yaml
core/src/main/java/google/registry/config/files/nomulus-config-qa.yaml
core/src/main/java/google/registry/config/files/nomulus-config-sandbox.yaml
######################################################################
# Eclipse Ignores
+1 -1
View File
@@ -56,7 +56,7 @@ PROPERTIES_HEADER = """\
# nom_build), run ./nom_build --help.
#
# DO NOT EDIT THIS FILE BY HAND
org.gradle.jvmargs=-Xmx1024m
org.gradle.jvmargs=-Xmx2048m
org.gradle.caching=true
org.gradle.parallel=true
"""
+3 -62
View File
@@ -105,9 +105,8 @@ PRESUBMITS = {
# System.(out|err).println should only appear in tools/ or load-testing/
PresubmitCheck(
r".*\bSystem\.(out|err)\.print", "java", {
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
"/load-testing/", "RegistryTestServerMain.java",
"TestServerExtension.java", "FlowDocumentationTool.java"
"/tools/", "/example/", "/load-testing/",
"RegistryTestServerMain.java", "TestServerExtension.java"
}):
"System.(out|err).println is only allowed in tools/ packages. Please "
"use a logger instead.",
@@ -120,7 +119,7 @@ PRESUBMITS = {
):
"In SOY please use the ({@param name: string} /** User name. */) style"
" parameter passing instead of the ( * @param name User name.) style "
"parameter pasing.",
"parameter passing.",
PresubmitCheck(
r'.*\{[^}]+\w+:\s+"',
"soy",
@@ -139,41 +138,6 @@ PRESUBMITS = {
{},
):
"All soy templates must use strict autoescaping",
# various JS linting checks
PresubmitCheck(
r".*goog\.base\(",
"js",
{"/node_modules/"},
):
"Use of goog.base is not allowed.",
PresubmitCheck(
r".*goog\.dom\.classes",
"js",
{"/node_modules/"},
):
"Instead of goog.dom.classes, use goog.dom.classlist which is smaller "
"and faster.",
PresubmitCheck(
r".*goog\.getMsg",
"js",
{"/node_modules/"},
):
"Put messages in Soy, instead of using goog.getMsg().",
PresubmitCheck(
r".*(innerHTML|outerHTML)\s*(=|[+]=)([^=]|$)",
"js",
{"/node_modules/", "registrar_bin."},
):
"Do not assign directly to the dom. Use goog.dom.setTextContent to set"
" to plain text, goog.dom.removeChildren to clear, or "
"soy.renderElement to render anything else",
PresubmitCheck(
r".*console\.(log|info|warn|error)",
"js",
{"/node_modules/", "google/registry/ui/js/util.js", "registrar_bin."},
):
"JavaScript files should not include console logging.",
PresubmitCheck(
r".*\nimport (static )?.*\.shaded\..*",
"java",
@@ -303,26 +267,6 @@ def verify_flyway_index():
return not success
def verify_javascript_deps():
"""Verifies that we haven't introduced any new javascript dependencies."""
with open('package.json') as f:
package = json.load(f)
deps = list(package['dependencies'].keys())
if deps != EXPECTED_JS_PACKAGES:
print('Unexpected javascript dependencies. Was expecting '
'%s, got %s.' % (EXPECTED_JS_PACKAGES, deps))
print(textwrap.dedent("""
* If the new dependencies are intentional, please verify that the
* license is one of the allowed licenses (see
* config/dependency-license/allowed_licenses.json) and add an entry
* for the package (with the license in a comment) to the
* EXPECTED_JS_PACKAGES variable in config/presubmits.py.
"""))
return True
return False
def get_files():
for root, dirnames, filenames in os.walk("."):
for filename in filenames:
@@ -347,8 +291,5 @@ if __name__ == "__main__":
# when we put it here it fails fast before all of the tests are run.
failed |= verify_flyway_index()
# Make sure we haven't introduced any javascript dependencies.
failed |= verify_javascript_deps()
if failed:
sys.exit(1)
@@ -56,7 +56,7 @@ export class HistoryListComponent {
return { main: 'N/A', detail: null };
}
const parts = description.split('|');
const detail = parts.length > 1 ? parts[1].replace(/_/g, ' ') : null;
const detail = parts.length > 1 ? parts[1].replace(/_/g, ' ') : parts[0];
return {
main: parts[0],
@@ -57,6 +57,11 @@
[(ngModel)]="contactService.contactInEdit.emailAddress"
[ngModelOptions]="{ standalone: true }"
[disabled]="emailAddressIsDisabled()"
[matTooltip]="
emailAddressIsDisabled()
? 'Reach out to registry customer support to update email address'
: ''
"
/>
</mat-form-field>
@@ -84,6 +89,7 @@
<h1>Contact Type</h1>
<p class="console-app__contact-required">
<mat-icon color="accent">error</mat-icon>Required to select at least one
(primary contact can't be updated)
</p>
<div class="">
<ng-container
@@ -29,7 +29,7 @@
></mat-label
>
<mat-select [(ngModel)]="user().role" name="userRole">
<mat-option value="PRIMARY_CONTACT">Editor</mat-option>
<mat-option value="TECH_CONTACT">Editor</mat-option>
<mat-option value="ACCOUNT_MANAGER">Viewer</mat-option>
</mat-select>
</mat-form-field>
-4
View File
@@ -30,7 +30,6 @@ def screenshotsForGoldensDir = "${project.buildDir}/screenshots_for_goldens"
def newGoldensDir = "${project.buildDir}/new_golden_images"
def goldensDir =
"${javaTestDir}/google/registry/webdriver/goldens/chrome-linux"
def jsDir = "${project.projectDir}/src/main/javascript"
// Tests that fail when running Gradle in a docker container, e. g. when
// building the release artifacts in Google Cloud Build.
@@ -55,9 +54,6 @@ def dockerIncompatibleTestPatterns = [
// objects retained by frameworks.
// TODO(weiminyu): identify cause and fix offending tests.
def fragileTestPatterns = [
// Changes cache timeouts and for some reason appears to have contention
// with other tests.
"google/registry/whois/WhoisCommandFactoryTest.*",
// Breaks random other tests when running with standardTests.
"google/registry/bsa/UploadBsaUnavailableDomainsActionTest.*",
// Currently changes a global configuration parameter that for some reason
@@ -29,9 +29,11 @@ import static google.registry.request.RequestParameters.extractRequiredParameter
import static google.registry.request.RequestParameters.extractSetOfDatetimeParameters;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.RateLimiter;
import dagger.Module;
import dagger.Provides;
import google.registry.request.Parameter;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.joda.time.DateTime;
@@ -137,4 +139,18 @@ public class BatchModule {
static boolean provideIsFast(HttpServletRequest req) {
return extractBooleanParameter(req, PARAM_FAST);
}
private static final int DEFAULT_MAX_QPS = 10;
@Provides
@Parameter("maxQps")
static int provideMaxQps(HttpServletRequest req) {
return extractOptionalIntParameter(req, "maxQps").orElse(DEFAULT_MAX_QPS);
}
@Provides
@Named("removeAllDomainContacts")
static RateLimiter provideRemoveAllDomainContactsRateLimiter(@Parameter("maxQps") int maxQps) {
return RateLimiter.create(maxQps);
}
}
@@ -30,6 +30,7 @@ import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
@@ -48,6 +49,7 @@ import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Level;
@@ -79,6 +81,7 @@ public class RemoveAllDomainContactsAction implements Runnable {
private final EppController eppController;
private final String registryAdminClientId;
private final LockHandler lockHandler;
private final RateLimiter rateLimiter;
private final Response response;
private final String updateDomainXml;
private int successes = 0;
@@ -91,10 +94,12 @@ public class RemoveAllDomainContactsAction implements Runnable {
EppController eppController,
@Config("registryAdminClientId") String registryAdminClientId,
LockHandler lockHandler,
@Named("removeAllDomainContacts") RateLimiter rateLimiter,
Response response) {
this.eppController = eppController;
this.registryAdminClientId = registryAdminClientId;
this.lockHandler = lockHandler;
this.rateLimiter = rateLimiter;
this.response = response;
this.updateDomainXml =
readResourceUtf8(RemoveAllDomainContactsAction.class, "domain_remove_contacts.xml");
@@ -146,7 +151,10 @@ public class RemoveAllDomainContactsAction implements Runnable {
.setMaxResults(BATCH_SIZE)
.getResultList());
domainRepoIdsBatch.forEach(this::runDomainUpdateFlow);
for (String domainRepoId : domainRepoIdsBatch) {
rateLimiter.acquire();
runDomainUpdateFlow(domainRepoId);
}
} while (!domainRepoIdsBatch.isEmpty());
String msg =
String.format(
@@ -159,12 +167,18 @@ public class RemoveAllDomainContactsAction implements Runnable {
private void runDomainUpdateFlow(String repoId) {
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
// transactionally. This way we can ensure that nothing else has modified the domain in question
// in the intervening period since the query above found it.
boolean success = tm().transact(() -> runDomainUpdateFlowInner(repoId));
if (success) {
successes++;
} else {
failures++;
// in the intervening period since the query above found it. If a single domain update fails
// permanently, log it and move on to not block processing all the other domains.
try {
boolean success = tm().transact(() -> runDomainUpdateFlowInner(repoId));
if (success) {
successes++;
} else {
failures++;
}
} catch (Throwable t) {
logger.atWarning().withCause(t).log(
"Failed updating domain with repoId %s; skipping.", repoId);
}
}
@@ -28,7 +28,6 @@ import static google.registry.bsa.persistence.Queries.queryMissedRegisteredUnblo
import static google.registry.bsa.persistence.Queries.queryUnblockableDomainByLabels;
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
import static google.registry.model.tld.Tlds.getTldEntitiesOfType;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.BatchedStreams.toBatches;
@@ -53,7 +52,6 @@ import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldType;
import google.registry.persistence.VKey;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Response;
@@ -185,8 +183,8 @@ public class BsaValidateAction implements Runnable {
ImmutableList<UnblockableDomain> batch;
do {
batch = Queries.batchReadUnblockableDomains(lastRead, transactionBatchSize);
ImmutableMap<String, VKey<Domain>> activeDomains =
ForeignKeyUtils.load(
ImmutableMap<String, Domain> activeDomains =
ForeignKeyUtils.loadResources(
Domain.class,
batch.stream().map(UnblockableDomain::domainName).collect(toImmutableList()),
clock.nowUtc());
@@ -201,7 +199,7 @@ public class BsaValidateAction implements Runnable {
}
Optional<String> verifyDomainStillUnblockableWithReason(
UnblockableDomain domain, ImmutableMap<String, VKey<Domain>> activeDomains) {
UnblockableDomain domain, ImmutableMap<String, Domain> activeDomains) {
DateTime now = clock.nowUtc();
boolean isRegistered = activeDomains.containsKey(domain.domainName());
boolean isReserved = isReservedDomain(domain.domainName(), now);
@@ -230,10 +228,8 @@ public class BsaValidateAction implements Runnable {
domain.reason()));
}
boolean isStalenessAllowed(VKey<Domain> domainVKey) {
Domain domain = bsaQuery(() -> replicaTm().loadByKey(domainVKey));
var now = clock.nowUtc();
return domain.getCreationTime().plus(maxStaleness).isAfter(now);
boolean isStalenessAllowed(Domain domain) {
return domain.getCreationTime().plus(maxStaleness).isAfter(clock.nowUtc());
}
/** Returns unique labels across all block lists in the download specified by {@code jobName}. */
@@ -156,7 +156,7 @@ public final class DomainsRefresher {
.collect(toImmutableSet());
ImmutableSet<String> currRegistered =
ImmutableSet.copyOf(
ForeignKeyUtils.load(Domain.class, nameToEntity.keySet(), now).keySet());
ForeignKeyUtils.loadKeys(Domain.class, nameToEntity.keySet(), now).keySet());
SetView<String> noLongerRegistered = Sets.difference(prevRegistered, currRegistered);
SetView<String> newlyRegistered = Sets.difference(currRegistered, prevRegistered);
@@ -145,11 +145,10 @@ public final class LabelDiffUpdates {
ImmutableSet<String> validDomainNames =
labels.stream()
.map(label -> validDomainNamesForLabel(label, idnChecker))
.flatMap(x -> x)
.flatMap(label -> validDomainNamesForLabel(label, idnChecker))
.collect(toImmutableSet());
ImmutableSet<String> registeredDomainNames =
ImmutableSet.copyOf(ForeignKeyUtils.load(Domain.class, validDomainNames, now).keySet());
ForeignKeyUtils.loadKeys(Domain.class, validDomainNames, now).keySet();
for (String domain : registeredDomainNames) {
nonBlockedDomains.add(new UnblockableDomain(domain, Reason.REGISTERED));
tm().put(BsaUnblockableDomain.of(domain, BsaUnblockableDomain.Reason.REGISTERED));
@@ -976,17 +976,6 @@ public final class RegistryConfig {
return config.misc.transientFailureRetries;
}
/**
* Amount of time public HTTP proxies are permitted to cache our WHOIS responses.
*
* @see google.registry.whois.WhoisHttpAction
*/
@Provides
@Config("whoisHttpExpires")
public static Duration provideWhoisHttpExpires() {
return Duration.standardDays(1);
}
/**
* Maximum number of results to return for an RDAP search query
*
@@ -998,39 +987,6 @@ public final class RegistryConfig {
return 100;
}
/**
* Redaction text for email address in WHOIS
*
* @see google.registry.whois.WhoisResponse
*/
@Provides
@Config("whoisRedactedEmailText")
public static String provideWhoisRedactedEmailText(RegistryConfigSettings config) {
return config.registryPolicy.whoisRedactedEmailText;
}
/**
* Disclaimer displayed at the end of WHOIS query results.
*
* @see google.registry.whois.WhoisResponse
*/
@Provides
@Config("whoisDisclaimer")
public static String provideWhoisDisclaimer(RegistryConfigSettings config) {
return config.registryPolicy.whoisDisclaimer;
}
/**
* Message template for whois response when queried domain is blocked by BSA.
*
* @see google.registry.whois.WhoisResponse
*/
@Provides
@Config("domainBlockedByBsaTemplate")
public static String provideDomainBlockedByBsaTemplate(RegistryConfigSettings config) {
return config.registryPolicy.domainBlockedByBsaTemplate;
}
/**
* Maximum QPS for the Google Cloud Monitoring V3 (aka Stackdriver) API. The QPS limit can be
* adjusted by contacting Cloud Support.
@@ -1105,12 +1061,6 @@ public final class RegistryConfig {
return config.registryPolicy.customLogicFactoryClass;
}
@Provides
@Config("whoisCommandFactoryClass")
public static String provideWhoisCommandFactoryClass(RegistryConfigSettings config) {
return config.registryPolicy.whoisCommandFactoryClass;
}
@Provides
@Config("dnsCountQueryCoordinatorClass")
public static String dnsCountQueryCoordinatorClass(RegistryConfigSettings config) {
@@ -90,7 +90,6 @@ public class RegistryConfigSettings {
public String contactAndHostRoidSuffix;
public String productName;
public String customLogicFactoryClass;
public String whoisCommandFactoryClass;
public String dnsCountQueryCoordinatorClass;
public int contactAutomaticTransferDays;
public String greetingServerId;
@@ -102,9 +101,6 @@ public class RegistryConfigSettings {
public String registryAdminClientId;
public String premiumTermsExportDisclaimer;
public String reservedTermsExportDisclaimer;
public String whoisRedactedEmailText;
public String whoisDisclaimer;
public String domainBlockedByBsaTemplate;
public String rdapTos;
public String rdapTosStaticUrl;
public String registryName;
@@ -0,0 +1,13 @@
# Nomulus Environment Configuration
The configuration files for the different Nomulus environments are not included in this repository. To configure and run a specific environment, you will need to create the corresponding YAML configuration file in this directory.
The following is a list of the environment configuration files that you may need to create:
* `nomulus-config-alpha.yaml`
* `nomulus-config-crash.yaml`
* `nomulus-config-qa.yaml`
* `nomulus-config-sandbox.yaml`
* `nomulus-config-production.yaml`
Please create the relevant file for the environment you intend to use and populate it with the necessary configuration details.
@@ -65,10 +65,6 @@ registryPolicy:
# See flows/custom/CustomLogicFactory.java
customLogicFactoryClass: google.registry.flows.custom.CustomLogicFactory
# WHOIS command factory fully-qualified class name.
# See whois/WhoisCommandFactory.java
whoisCommandFactoryClass: google.registry.whois.WhoisCommandFactory
# Custom logic class for handling DNS query count reporting for ICANN.
# See reporting/icann/DnsCountQueryCoordinator.java
dnsCountQueryCoordinatorClass: google.registry.reporting.icann.DummyDnsCountQueryCoordinator
@@ -114,31 +110,6 @@ registryPolicy:
to publish. This list is subject to change. The most up-to-date source
is always the registry itself, by sending domain check EPP commands.
# Redaction text for email address in WHOIS
whoisRedactedEmailText: |
Please query the WHOIS server of the owning registrar identified in this
output for information on how to contact the Registrant, Admin, or Tech
contact of the queried domain name.
# Disclaimer at the top of WHOIS results.
whoisDisclaimer: |
WHOIS information is provided by the registry solely for query-based,
informational purposes. Any information provided is "as is" without any
guarantee of accuracy. You may not use such information to (a) allow,
enable, or otherwise support the transmission of mass unsolicited,
commercial advertising or solicitations; (b) enable high volume, automated,
electronic processes that access the registry's systems or any
ICANN-Accredited Registrar, except as reasonably necessary to register
domain names or modify existing registrations; or (c) engage in or support
unlawful behavior. We reserve the right to restrict or deny your access to
the WHOIS database, and may modify these terms at any time.
# BSA blocked domain name template.
domainBlockedByBsaTemplate: |
Domain Name: %s
>>> This name is not available for registration.
>>> This name has been blocked by a GlobalBlock service.
# RDAP Terms of Service text displayed at the /rdap/help/tos endpoint.
rdapTos: >
By querying our Domain Database as part of the RDAP pilot program (RDAP
@@ -1 +0,0 @@
# Add environment-specific configuration here.
@@ -1 +0,0 @@
# Add environment-specific configuration here.
@@ -1 +0,0 @@
# Add environment-specific configuration here.
@@ -1 +0,0 @@
# Add environment-specific configuration here.
@@ -1 +0,0 @@
# Add environment-specific configuration here.
@@ -5,7 +5,6 @@
addGracePeriodLength: "PT432000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT3888000S"
automaticTransferLength: "PT432000S"
@@ -25,7 +25,6 @@ import static google.registry.dns.DnsModule.PARAM_REFRESH_REQUEST_TIME;
import static google.registry.dns.DnsUtils.DNS_PUBLISH_PUSH_QUEUE_NAME;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.dns.DnsUtils.requestHostDnsRefresh;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.PARAM_TLD;
@@ -46,6 +45,7 @@ import google.registry.dns.DnsMetrics.CommitStatus;
import google.registry.dns.DnsMetrics.PublishStatus;
import google.registry.dns.writer.DnsWriter;
import google.registry.groups.GmailClient;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
@@ -237,7 +237,8 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
.findFirst()
.ifPresent(
dn -> {
Optional<Domain> domain = loadByForeignKey(Domain.class, dn, clock.nowUtc());
Optional<Domain> domain =
ForeignKeyUtils.loadResource(Domain.class, dn, clock.nowUtc());
if (domain.isPresent()) {
notifyWithEmailAboutDnsUpdateFailure(
domain.get().getCurrentSponsorRegistrarId(), dn, false);
@@ -250,7 +251,8 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
.findFirst()
.ifPresent(
hn -> {
Optional<Host> host = loadByForeignKey(Host.class, hn, clock.nowUtc());
Optional<Host> host =
ForeignKeyUtils.loadResource(Host.class, hn, clock.nowUtc());
if (host.isPresent()) {
notifyWithEmailAboutDnsUpdateFailure(
host.get().getPersistedCurrentSponsorRegistrarId(), hn, true);
@@ -16,12 +16,12 @@ package google.registry.dns;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.dns.DnsUtils.requestHostDnsRefresh;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import google.registry.dns.DnsUtils.TargetType;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
@@ -79,7 +79,7 @@ public final class RefreshDnsAction implements Runnable {
private <T extends EppResource & ForeignKeyedEppResource>
T loadAndVerifyExistence(Class<T> clazz, String foreignKey) {
return loadByForeignKey(clazz, foreignKey, clock.nowUtc())
return ForeignKeyUtils.loadResource(clazz, foreignKey, clock.nowUtc())
.orElseThrow(
() ->
new NotFoundException(
@@ -17,7 +17,6 @@ package google.registry.dns.writer.clouddns;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.dns.DnsUtils.getDnsAPlusAAAATtlForHost;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.util.DomainNameUtils.getSecondLevelDomain;
import com.google.api.client.googleapis.json.GoogleJsonError;
@@ -37,6 +36,7 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.dns.writer.BaseDnsWriter;
import google.registry.dns.writer.DnsWriter;
import google.registry.dns.writer.DnsWriterZone;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.domain.secdns.DomainDsData;
import google.registry.model.host.Host;
@@ -123,7 +123,8 @@ public class CloudDnsWriter extends BaseDnsWriter {
String absoluteDomainName = getAbsoluteHostName(domainName);
// Load the target domain. Note that it can be absent if this domain was just deleted.
Optional<Domain> domain = loadByForeignKey(Domain.class, domainName, clock.nowUtc());
Optional<Domain> domain =
ForeignKeyUtils.loadResource(Domain.class, domainName, clock.nowUtc());
// Return early if no DNS records should be published.
// desiredRecordsBuilder is populated with an empty set to indicate that all existing records
@@ -189,7 +190,7 @@ public class CloudDnsWriter extends BaseDnsWriter {
// Load the target host. Note that it can be absent if this host was just deleted.
// desiredRecords is populated with an empty set to indicate that all existing records
// should be deleted.
Optional<Host> host = loadByForeignKey(Host.class, hostName, clock.nowUtc());
Optional<Host> host = ForeignKeyUtils.loadResource(Host.class, hostName, clock.nowUtc());
// Return early if the host is deleted.
if (host.isEmpty()) {
@@ -19,7 +19,6 @@ import static com.google.common.base.Verify.verify;
import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.union;
import static google.registry.dns.DnsUtils.getDnsAPlusAAAATtlForHost;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
@@ -28,6 +27,7 @@ import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig.Config;
import google.registry.dns.writer.BaseDnsWriter;
import google.registry.dns.writer.DnsWriterZone;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.domain.secdns.DomainDsData;
import google.registry.model.host.Host;
@@ -129,7 +129,8 @@ public class DnsUpdateWriter extends BaseDnsWriter {
* this domain refresh request
*/
private void publishDomain(String domainName, String requestingHostName) {
Optional<Domain> domainOptional = loadByForeignKey(Domain.class, domainName, clock.nowUtc());
Optional<Domain> domainOptional =
ForeignKeyUtils.loadResource(Domain.class, domainName, clock.nowUtc());
update.delete(toAbsoluteName(domainName), Type.ANY);
// If the domain is now deleted, then don't update DNS for it.
if (domainOptional.isPresent()) {
@@ -218,7 +219,7 @@ public class DnsUpdateWriter extends BaseDnsWriter {
private void addInBailiwickNameServerSet(Domain domain, Update update) {
for (String hostName :
intersection(domain.loadNameserverHostNames(), domain.getSubordinateHosts())) {
Optional<Host> host = loadByForeignKey(Host.class, hostName, clock.nowUtc());
Optional<Host> host = ForeignKeyUtils.loadResource(Host.class, hostName, clock.nowUtc());
checkState(host.isPresent(), "Host %s cannot be loaded", hostName);
update.add(makeAddressSet(host.get()));
update.add(makeV6AddressSet(host.get()));
@@ -57,11 +57,6 @@
<!-- Verbatim JavaScript sources (only visible to admins for debugging). -->
<url-pattern>/assets/sources/*</url-pattern>
<!-- TODO(b/26776367): Move these files to /assets/sources. -->
<url-pattern>/assets/js/registrar_bin.js.map</url-pattern>
<url-pattern>/assets/js/registrar_dbg.js</url-pattern>
<url-pattern>/assets/css/registrar_dbg.css</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
@@ -38,7 +38,6 @@ import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static org.json.simple.JSONValue.toJSONString;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
@@ -185,8 +184,7 @@ public class CheckApiAction implements Runnable {
}
private boolean checkExists(String domainString, DateTime now) {
return !ForeignKeyUtils.loadByCache(Domain.class, ImmutableList.of(domainString), now)
.isEmpty();
return ForeignKeyUtils.loadKeyByCache(Domain.class, domainString, now).isPresent();
}
private Optional<String> checkReserved(InternetDomainName domainName) {
@@ -56,9 +56,10 @@ public final class FlowUtils {
}
}
/** Persists the saves and deletes in an {@link EntityChanges} to the DB. */
/** Persists the inserts, updates, and deletes in an {@link EntityChanges} to the DB. */
public static void persistEntityChanges(EntityChanges entityChanges) {
tm().putAll(entityChanges.getSaves());
tm().insertAll(entityChanges.getInserts());
tm().updateAll(entityChanges.getUpdates());
tm().delete(entityChanges.getDeletes());
}
@@ -16,7 +16,6 @@ package google.registry.flows;
import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -72,10 +71,9 @@ public final class ResourceFlowUtils {
*/
public static <R extends EppResource> void checkLinkedDomains(
final String targetId, final DateTime now, final Class<R> resourceClass) throws EppException {
VKey<R> key = ForeignKeyUtils.load(resourceClass, targetId, now);
if (key == null) {
throw new ResourceDoesNotExistException(resourceClass, targetId);
}
VKey<R> key =
ForeignKeyUtils.loadKey(resourceClass, targetId, now)
.orElseThrow(() -> new ResourceDoesNotExistException(resourceClass, targetId));
if (isLinked(key, now)) {
throw new ResourceToDeleteIsReferencedException();
}
@@ -97,7 +95,7 @@ public final class ResourceFlowUtils {
public static <R extends EppResource & ForeignKeyedEppResource> R loadAndVerifyExistence(
Class<R> clazz, String targetId, DateTime now) throws ResourceDoesNotExistException {
return verifyExistence(clazz, targetId, loadByForeignKey(clazz, targetId, now));
return verifyExistence(clazz, targetId, ForeignKeyUtils.loadResource(clazz, targetId, now));
}
public static <R extends EppResource> R verifyExistence(
@@ -107,11 +105,10 @@ public final class ResourceFlowUtils {
public static <R extends EppResource> void verifyResourceDoesNotExist(
Class<R> clazz, String targetId, DateTime now, String registrarId) throws EppException {
VKey<R> key = ForeignKeyUtils.load(clazz, targetId, now);
if (key != null) {
R resource = tm().loadByKey(key);
Optional<R> resource = ForeignKeyUtils.loadResource(clazz, targetId, now);
if (resource.isPresent()) {
// These are similar exceptions, but we can track them internally as log-based metrics.
if (Objects.equals(registrarId, resource.getPersistedCurrentSponsorRegistrarId())) {
if (Objects.equals(registrarId, resource.get().getPersistedCurrentSponsorRegistrarId())) {
throw new ResourceAlreadyExistsForThisClientException(targetId);
} else {
throw new ResourceCreateContentionException(targetId);
@@ -16,7 +16,6 @@ package google.registry.flows.contact;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount;
import static google.registry.model.EppResourceUtils.checkResourcesExist;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -26,6 +25,7 @@ import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Check;
import google.registry.model.eppinput.ResourceCommand;
@@ -62,7 +62,7 @@ public final class ContactCheckFlow implements TransactionalFlow {
ImmutableList<String> targetIds = ((Check) resourceCommand).getTargetIds();
verifyTargetIdCount(targetIds, maxChecks);
ImmutableSet<String> existingIds =
checkResourcesExist(Contact.class, targetIds, clock.nowUtc());
ForeignKeyUtils.loadKeys(Contact.class, targetIds, clock.nowUtc()).keySet();
ImmutableList.Builder<ContactCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
boolean unused = !existingIds.contains(id);
@@ -19,25 +19,30 @@ import com.google.common.collect.ImmutableSet;
import google.registry.model.ImmutableObject;
import google.registry.persistence.VKey;
/** A record that encapsulates database entities to both save and delete. */
/** A record that encapsulates database entities to insert, update, and delete. */
public record EntityChanges(
ImmutableSet<ImmutableObject> saves, ImmutableSet<VKey<ImmutableObject>> deletes) {
ImmutableSet<ImmutableObject> inserts,
ImmutableSet<ImmutableObject> updates,
ImmutableSet<VKey<ImmutableObject>> deletes) {
public ImmutableSet<ImmutableObject> getSaves() {
return saves;
public ImmutableSet<ImmutableObject> getInserts() {
return inserts;
}
public ImmutableSet<ImmutableObject> getUpdates() {
return updates;
}
;
public ImmutableSet<VKey<ImmutableObject>> getDeletes() {
return deletes;
}
;
public static Builder newBuilder() {
// Default both entities to save and entities to delete to empty sets, so that the build()
// method won't subsequently throw an exception if one doesn't end up being applicable.
// Default inserts, updates, and deletes to empty sets, so that the build() method won't
// subsequently throw an exception if one doesn't end up being applicable.
return new AutoBuilder_EntityChanges_Builder()
.setSaves(ImmutableSet.of())
.setInserts(ImmutableSet.of())
.setUpdates(ImmutableSet.of())
.setDeletes(ImmutableSet.of());
}
@@ -45,12 +50,21 @@ public record EntityChanges(
@AutoBuilder
public interface Builder {
Builder setSaves(ImmutableSet<ImmutableObject> entitiesToSave);
Builder setInserts(ImmutableSet<ImmutableObject> entitiesToInsert);
ImmutableSet.Builder<ImmutableObject> savesBuilder();
ImmutableSet.Builder<ImmutableObject> insertsBuilder();
default Builder addSave(ImmutableObject entityToSave) {
savesBuilder().add(entityToSave);
default Builder addInsert(ImmutableObject entityToInsert) {
insertsBuilder().add(entityToInsert);
return this;
}
Builder setUpdates(ImmutableSet<ImmutableObject> entitiesToUpdate);
ImmutableSet.Builder<ImmutableObject> updatesBuilder();
default Builder addUpdate(ImmutableObject entityToUpdate) {
updatesBuilder().add(entityToUpdate);
return this;
}
@@ -181,7 +181,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
.setAsOfDate(now)
.build());
ImmutableMap<String, VKey<Domain>> existingDomains =
ForeignKeyUtils.load(Domain.class, domainNames, now);
ForeignKeyUtils.loadKeys(Domain.class, domainNames, now);
// Check block labels only when there are unregistered domains, since "In use" goes before
// "Blocked by BSA".
ImmutableSet<InternetDomainName> bsaBlockedDomainNames =
@@ -18,7 +18,6 @@ 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;
@@ -196,7 +195,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
* @error {@link RegistrantProhibitedException}
* @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException}
* @error {@link DomainFlowUtils.TldDoesNotExistException}
@@ -224,6 +222,7 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject DomainCreateFlowCustomLogic flowCustomLogic;
@Inject DomainFlowTmchUtils tmchUtils;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainDeletionTimeCache domainDeletionTimeCache;
@Inject DomainCreateFlow() {}
@@ -239,13 +238,13 @@ public final class DomainCreateFlow implements MutatingFlow {
validateRegistrarIsLoggedIn(registrarId);
verifyRegistrarIsActive(registrarId);
extensionManager.validate();
verifyDomainDoesNotExist();
DateTime now = tm().getTransactionTime();
DomainCommand.Create command = cloneAndLinkReferences((Create) resourceCommand, now);
Period period = command.getPeriod();
verifyUnitIsYears(period);
int years = period.getValue();
validateRegistrationPeriod(years);
verifyResourceDoesNotExist(Domain.class, targetId, now, registrarId);
// Validate that this is actually a legal domain name on a TLD that the registrar has access to.
InternetDomainName domainName = validateDomainName(command.getDomainName());
String domainLabel = domainName.parts().getFirst();
@@ -357,11 +356,11 @@ public final class DomainCreateFlow implements MutatingFlow {
domainHistoryId, registrationExpirationTime, isAnchorTenant, allocationToken);
PollMessage.Autorenew autorenewPollMessage =
createAutorenewPollMessage(domainHistoryId, registrationExpirationTime);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add(createBillingEvent, autorenewBillingEvent, autorenewPollMessage);
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
entitiesToInsert.add(createBillingEvent, autorenewBillingEvent, autorenewPollMessage);
// Bill for EAP cost, if any.
if (!feesAndCredits.getEapCost().isZero()) {
entitiesToSave.add(createEapBillingEvent(feesAndCredits, createBillingEvent));
entitiesToInsert.add(createEapBillingEvent(feesAndCredits, createBillingEvent));
}
ImmutableSet<ReservationType> reservationTypes = getReservationTypes(domainName);
@@ -404,12 +403,13 @@ public final class DomainCreateFlow implements MutatingFlow {
DomainHistory domainHistory =
buildDomainHistory(domain, tld, now, period, tld.getAddGracePeriodLength());
if (reservationTypes.contains(NAME_COLLISION)) {
entitiesToSave.add(
entitiesToInsert.add(
createNameCollisionOneTimePollMessage(targetId, domainHistory, registrarId, now));
}
entitiesToSave.add(domain, domainHistory);
entitiesToInsert.add(domain, domainHistory);
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
entitiesToUpdate.add(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
@@ -422,7 +422,10 @@ public final class DomainCreateFlow implements MutatingFlow {
.setNewDomain(domain)
.setHistoryEntry(domainHistory)
.setEntityChanges(
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
EntityChanges.newBuilder()
.setInserts(entitiesToInsert.build())
.setUpdates(entitiesToUpdate.build())
.build())
.setYears(years)
.build());
persistEntityChanges(entityChanges);
@@ -649,6 +652,15 @@ public final class DomainCreateFlow implements MutatingFlow {
.build();
}
private void verifyDomainDoesNotExist() throws ResourceCreateContentionException {
Optional<DateTime> previousDeletionTime =
domainDeletionTimeCache.getDeletionTimeForDomain(targetId);
if (previousDeletionTime.isPresent()
&& !tm().getTransactionTime().isAfter(previousDeletionTime.get())) {
throw new ResourceCreateContentionException(targetId);
}
}
private static BillingEvent createEapBillingEvent(
FeesAndCredits feesAndCredits, BillingEvent createBillingEvent) {
return new BillingEvent.Builder()
@@ -77,6 +77,7 @@ import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.fee06.FeeDeleteResponseExtensionV06;
import google.registry.model.domain.fee11.FeeDeleteResponseExtensionV11;
import google.registry.model.domain.fee12.FeeDeleteResponseExtensionV12;
import google.registry.model.domain.feestdv1.FeeDeleteResponseExtensionStdV1;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.SecDnsCreateExtension;
@@ -151,7 +152,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
verifyDeleteAllowed(existingDomain, tld, now);
flowCustomLogic.afterValidation(
AfterValidationParameters.newBuilder().setExistingDomain(existingDomain).build());
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
Domain.Builder builder;
if (existingDomain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
builder =
@@ -221,7 +222,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
} else {
PollMessage.OneTime deletePollMessage =
createDeletePollMessage(existingDomain, domainHistoryId, deletionTime);
entitiesToSave.add(deletePollMessage);
entitiesToInsert.add(deletePollMessage);
builder.setDeletePollMessage(deletePollMessage.createVKey());
}
}
@@ -230,7 +231,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
// registrar other than the sponsoring registrar (which will necessarily be a superuser).
if (durationUntilDelete.isLongerThan(Duration.ZERO)
&& !registrarId.equals(existingDomain.getPersistedCurrentSponsorRegistrarId())) {
entitiesToSave.add(
entitiesToInsert.add(
createImmediateDeletePollMessage(existingDomain, domainHistoryId, now, deletionTime));
}
@@ -239,7 +240,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
// No cancellation is written if the grace period was not for a billable event.
if (gracePeriod.hasBillingEvent()) {
entitiesToSave.add(
entitiesToInsert.add(
BillingCancellation.forGracePeriod(gracePeriod, now, domainHistoryId, targetId));
if (gracePeriod.getBillingEvent() != null) {
// Take the amount of registration time being refunded off the expiration time.
@@ -271,7 +272,7 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
// ResourceDeleteFlow since it's listed in serverApproveEntities.
requestDomainDnsRefresh(existingDomain.getDomainName());
entitiesToSave.add(newDomain, domainHistory);
entitiesToInsert.add(domainHistory);
EntityChanges entityChanges =
flowCustomLogic.beforeSave(
BeforeSaveParameters.newBuilder()
@@ -279,7 +280,10 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
.setNewDomain(newDomain)
.setHistoryEntry(domainHistory)
.setEntityChanges(
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
EntityChanges.newBuilder()
.setInserts(entitiesToInsert.build())
.addUpdate(newDomain)
.build())
.build());
BeforeResponseReturnData responseData =
flowCustomLogic.beforeResponse(
@@ -425,6 +429,9 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging
@Nullable
private FeeTransformResponseExtension.Builder getDeleteResponseBuilder() {
Set<String> uris = nullToEmpty(sessionMetadata.getServiceExtensionUris());
if (uris.contains(ServiceExtension.FEE_1_00.getUri())) {
return new FeeDeleteResponseExtensionStdV1.Builder();
}
if (uris.contains(ServiceExtension.FEE_0_12.getUri())) {
return new FeeDeleteResponseExtensionV12.Builder();
}
@@ -0,0 +1,133 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.domain;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Ticker;
import com.google.common.collect.ImmutableSet;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.util.DateTimeUtils;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* Functionally-static loading cache that keeps track of deletion (AKA drop) times for domains.
*
* <p>Some domain names may have many create requests issued shortly before (and directly after) the
* name is released due to a previous registrant deleting it. In those cases, caching the deletion
* time of the existing domain allows us to short-circuit the request and avoid any load on the
* database checking the existing domain (at least, in cases where the request hits a particular
* node more than once).
*
* <p>The cache is fairly short-lived (as we're concerned about many requests at basically the same
* time), and entries also expire when the drop actually happens. If the domain is re-created after
* a drop, the next load attempt will populate the cache with a deletion time of END_OF_TIME, which
* will be read from the cache by subsequent attempts.
*
* <p>We take advantage of the fact that Caffeine caches don't store nulls returned from the
* CacheLoader, so a null result (meaning the domain doesn't exist) won't affect future calls (this
* avoids a stale-cache situation where the cache "thinks" the domain doesn't exist, but it does).
* Put another way, if a domain really doesn't exist, we'll re-attempt the database load every time.
*
* <p>We don't explicitly set the cache inside domain create/delete flows, in case the transaction
* fails at commit time. It's better to have stale data, or to require an additional database load,
* than to have incorrect data.
*
* <p>Note: this should be injected as a singleton -- it's essentially static, but we have it as a
* non-static object for concurrent testing purposes.
*/
public class DomainDeletionTimeCache {
// Max expiry time is ten minutes
private static final long NANOS_IN_ONE_MILLISECOND = 100000L;
private static final long MAX_EXPIRY_NANOS = 10L * 60L * 1000L * NANOS_IN_ONE_MILLISECOND;
private static final int MAX_ENTRIES = 500;
/**
* Expire after the max duration, or after the domain is set to drop (whichever comes first).
*
* <p>If the domain has already been deleted (the deletion time is <= now), the entry will
* immediately be expired/removed.
*
* <p>NB: the Expiry class requires the return value in <b>nanoseconds</b>, not milliseconds
*/
private static final Expiry<String, DateTime> EXPIRY_POLICY =
new Expiry<>() {
@Override
public long expireAfterCreate(String key, DateTime value, long currentTime) {
// Watch out for Long overflow
long deletionTimeNanos =
value.equals(DateTimeUtils.END_OF_TIME)
? Long.MAX_VALUE
: value.getMillis() * NANOS_IN_ONE_MILLISECOND;
long nanosUntilDeletion = deletionTimeNanos - currentTime;
return Math.max(0L, Math.min(MAX_EXPIRY_NANOS, nanosUntilDeletion));
}
/** Reset the time entirely on update, as if we were creating the entry anew. */
@Override
public long expireAfterUpdate(
String key, DateTime value, long currentTime, long currentDuration) {
return expireAfterCreate(key, value, currentTime);
}
/** Reads do not change the expiry duration. */
@Override
public long expireAfterRead(
String key, DateTime value, long currentTime, long currentDuration) {
return currentDuration;
}
};
/** Attempt to load the domain's deletion time if the domain exists. */
private static final CacheLoader<String, DateTime> CACHE_LOADER =
(domainName) -> {
ForeignKeyUtils.MostRecentResource mostRecentResource =
ForeignKeyUtils.loadMostRecentResources(
Domain.class, ImmutableSet.of(domainName), false)
.get(domainName);
return mostRecentResource == null ? null : mostRecentResource.deletionTime();
};
// Unfortunately, maintenance tasks aren't necessarily already in a transaction
private static final Ticker TRANSACTION_TIME_TICKER =
() -> tm().reTransact(() -> tm().getTransactionTime().getMillis() * NANOS_IN_ONE_MILLISECOND);
public static DomainDeletionTimeCache create() {
return new DomainDeletionTimeCache(
Caffeine.newBuilder()
.ticker(TRANSACTION_TIME_TICKER)
.expireAfter(EXPIRY_POLICY)
.maximumSize(MAX_ENTRIES)
.build(CACHE_LOADER));
}
private final LoadingCache<String, DateTime> cache;
private DomainDeletionTimeCache(LoadingCache<String, DateTime> cache) {
this.cache = cache;
}
/** Returns the domain's deletion time, or null if it doesn't currently exist. */
public Optional<DateTime> getDeletionTimeForDomain(String domainName) {
return Optional.ofNullable(cache.get(domainName));
}
}
@@ -1,4 +1,4 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
// Copyright 2025 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.
@@ -12,13 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Test existing solely to run the :check BUILD rule.
*/
package google.registry.flows.domain;
goog.setTestOnly();
import dagger.Module;
import dagger.Provides;
import jakarta.inject.Singleton;
goog.require('goog.testing.jsunit');
/** Dagger module to provide the {@link DomainDeletionTimeCache}. */
@Module
public class DomainDeletionTimeCacheModule {
function testNothing() {}
@Provides
@Singleton
public static DomainDeletionTimeCache provideDomainDeletionTimeCache() {
return DomainDeletionTimeCache.create();
}
}
@@ -553,17 +553,6 @@ public class DomainFlowUtils {
}
}
static void validateRegistrantAllowedOnTld(String tld, Optional<String> registrantContactId)
throws RegistrantNotAllowedException {
ImmutableSet<String> allowedRegistrants = Tld.get(tld).getAllowedRegistrantContactIds();
// Empty allow list or null registrantContactId are ignored.
if (registrantContactId.isPresent()
&& !allowedRegistrants.isEmpty()
&& !allowedRegistrants.contains(registrantContactId.get())) {
throw new RegistrantNotAllowedException(registrantContactId.get());
}
}
static void validateNameserversAllowedOnTld(String tld, Set<String> fullyQualifiedHostNames)
throws EppException {
ImmutableSet<String> allowedHostNames = Tld.get(tld).getAllowedFullyQualifiedHostNames();
@@ -711,7 +700,7 @@ public class DomainFlowUtils {
BillingRecurrence newBillingRecurrence =
existingBillingRecurrence.asBuilder().setRecurrenceEndTime(newEndTime).build();
tm().put(newBillingRecurrence);
tm().update(newBillingRecurrence);
return newBillingRecurrence;
}
@@ -1091,7 +1080,6 @@ public class DomainFlowUtils {
command.getContacts(), command.getRegistrant(), command.getNameservers());
validateContactsHaveTypes(command.getContacts());
String tldStr = tld.getTldStr();
validateRegistrantAllowedOnTld(tldStr, command.getRegistrantContactId());
validateNoDuplicateContacts(command.getContacts());
validateCreateContactData(command.getRegistrant(), command.getContacts());
ImmutableSet<String> hostNames = command.getNameserverHostNames();
@@ -1630,13 +1618,6 @@ public class DomainFlowUtils {
}
}
/** Registrant is not allow-listed for this TLD. */
public static class RegistrantNotAllowedException extends StatusProhibitsOperationException {
public RegistrantNotAllowedException(String contactId) {
super(String.format("Registrant with id %s is not allow-listed for this TLD", contactId));
}
}
/** Nameservers are not allow-listed for this TLD. */
public static class NameserversNotAllowedForTldException
extends StatusProhibitsOperationException {
@@ -15,12 +15,11 @@
package google.registry.flows.domain;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyExistence;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.domain.DomainFlowUtils.addSecDnsExtensionIfPresent;
import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
import static google.registry.flows.domain.DomainFlowUtils.loadForeignKeyedDesignatedContacts;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
@@ -108,8 +107,7 @@ public final class DomainInfoFlow implements MutatingFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = clock.nowUtc();
Domain domain =
verifyExistence(Domain.class, targetId, loadByForeignKey(Domain.class, targetId, now));
Domain domain = loadAndVerifyExistence(Domain.class, targetId, now);
verifyOptionalAuthInfo(authInfo, domain);
flowCustomLogic.afterValidation(
AfterValidationParameters.newBuilder().setDomain(domain).build());
@@ -245,11 +245,13 @@ public final class DomainRenewFlow implements MutatingFlow {
.build();
DomainHistory domainHistory =
buildDomainHistory(newDomain, now, command.getPeriod(), tld.getRenewGracePeriodLength());
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add(
newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
entitiesToInsert.add(
domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
entitiesToUpdate.add(newDomain);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
entitiesToUpdate.add(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
@@ -262,7 +264,10 @@ public final class DomainRenewFlow implements MutatingFlow {
.setYears(years)
.setHistoryEntry(domainHistory)
.setEntityChanges(
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
EntityChanges.newBuilder()
.setInserts(entitiesToInsert.build())
.setUpdates(entitiesToUpdate.build())
.build())
.build());
BeforeResponseReturnData responseData =
flowCustomLogic.beforeResponse(
@@ -146,18 +146,18 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
verifyRestoreAllowed(command, existingDomain, feeUpdate, feesAndCredits, now);
HistoryEntryId domainHistoryId = createHistoryEntryId(existingDomain);
historyBuilder.setRevisionId(domainHistoryId.getRevisionId());
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
DateTime newExpirationTime =
existingDomain.getRegistrationExpirationTime().plusYears(isExpired ? 1 : 0);
// Restore the expiration time on the deleted domain, except if that's already passed, then add
// a year and bill for it immediately, with no grace period.
if (isExpired) {
entitiesToSave.add(
entitiesToInsert.add(
createRenewBillingEvent(domainHistoryId, feesAndCredits.getRenewCost(), now));
}
// Always bill for the restore itself.
entitiesToSave.add(
entitiesToInsert.add(
createRestoreBillingEvent(domainHistoryId, feesAndCredits.getRestoreCost(), now));
BillingRecurrence autorenewEvent =
@@ -166,12 +166,14 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistoryId(domainHistoryId)
.build();
entitiesToInsert.add(autorenewEvent);
PollMessage.Autorenew autorenewPollMessage =
newAutorenewPollMessage(existingDomain)
.setEventTime(newExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setDomainHistoryId(domainHistoryId)
.build();
entitiesToInsert.add(autorenewPollMessage);
Domain newDomain =
performRestore(
existingDomain,
@@ -181,8 +183,9 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
now,
registrarId);
DomainHistory domainHistory = buildDomainHistory(newDomain, now);
entitiesToSave.add(newDomain, domainHistory, autorenewEvent, autorenewPollMessage);
tm().putAll(entitiesToSave.build());
entitiesToInsert.add(domainHistory);
tm().update(newDomain);
tm().insertAll(entitiesToInsert.build());
if (existingDomain.getDeletePollMessage() != null) {
tm().delete(existingDomain.getDeletePollMessage());
}
@@ -172,7 +172,7 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
.setDomainHistoryId(domainHistoryId)
.build());
ImmutableList.Builder<ImmutableObject> entitiesToSave = new ImmutableList.Builder<>();
ImmutableList.Builder<ImmutableObject> entitiesToInsert = new ImmutableList.Builder<>();
// If we are within an autorenew grace period, cancel the autorenew billing event and don't
// increase the registration time, since the transfer subsumes the autorenew's extra year.
GracePeriod autorenewGrace =
@@ -184,7 +184,7 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
// then the gaining registrar is not charged for the one-year renewal and the losing registrar
// still needs to be charged for the auto-renew.
if (billingEvent.isPresent()) {
entitiesToSave.add(
entitiesToInsert.add(
BillingCancellation.forGracePeriod(autorenewGrace, now, domainHistoryId, targetId));
}
}
@@ -259,14 +259,11 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
PollMessage gainingClientPollMessage =
createGainingTransferPollMessage(
targetId, newDomain.getTransferData(), newExpirationTime, now, domainHistoryId);
billingEvent.ifPresent(entitiesToSave::add);
entitiesToSave.add(
autorenewEvent,
gainingClientPollMessage,
gainingClientAutorenewPollMessage,
newDomain,
domainHistory);
tm().putAll(entitiesToSave.build());
billingEvent.ifPresent(entitiesToInsert::add);
entitiesToInsert.add(
autorenewEvent, gainingClientPollMessage, gainingClientAutorenewPollMessage, domainHistory);
tm().update(newDomain);
tm().insertAll(entitiesToInsert.build());
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingDomain.getTransferData().getServerApproveEntities());
@@ -110,8 +110,8 @@ public final class DomainTransferCancelFlow implements MutatingFlow {
Domain newDomain =
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_CANCELLED, now, registrarId);
DomainHistory domainHistory = buildDomainHistory(newDomain, tld, now);
tm().putAll(
newDomain,
tm().update(newDomain);
tm().insertAll(
domainHistory,
createLosingTransferPollMessage(
targetId, newDomain.getTransferData(), null, domainHistoryId));
@@ -109,8 +109,8 @@ public final class DomainTransferRejectFlow implements MutatingFlow {
Domain newDomain =
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_REJECTED, now, registrarId);
DomainHistory domainHistory = buildDomainHistory(newDomain, tld, now);
tm().putAll(
newDomain,
tm().update(newDomain);
tm().insertAll(
domainHistory,
createGainingTransferPollMessage(
targetId, newDomain.getTransferData(), null, now, domainHistoryId));
@@ -283,11 +283,9 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
asyncTaskEnqueuer.enqueueAsyncResave(
newDomain.createVKey(), now, ImmutableSortedSet.of(automaticTransferTime));
tm().putAll(
new ImmutableSet.Builder<>()
.add(newDomain, domainHistory, requestPollMessage)
.addAll(serverApproveEntities)
.build());
tm().put(newDomain);
tm().putAll(serverApproveEntities);
tm().insertAll(domainHistory, requestPollMessage);
return responseBuilder
.setResultFromCode(SUCCESS_WITH_ACTION_PENDING)
.setResData(createResponse(period, existingDomain, newDomain, now))
@@ -36,7 +36,6 @@ import static google.registry.flows.domain.DomainFlowUtils.validateFeesAckedIfPr
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts;
import static google.registry.flows.domain.DomainFlowUtils.validateRegistrantAllowedOnTld;
import static google.registry.flows.domain.DomainFlowUtils.validateUpdateContactData;
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
@@ -131,7 +130,6 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
* @error {@link RegistrantProhibitedException}
* @error {@link DomainFlowUtils.SecDnsAllUsageException}
* @error {@link DomainFlowUtils.TooManyDsRecordsException}
@@ -190,14 +188,16 @@ public final class DomainUpdateFlow implements MutatingFlow {
if (requiresDnsUpdate(existingDomain, newDomain)) {
requestDomainDnsRefresh(targetId);
}
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add(newDomain, domainHistory);
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
entitiesToUpdate.add(newDomain);
entitiesToInsert.add(domainHistory);
Optional<BillingEvent> statusUpdateBillingEvent =
createBillingEventForStatusUpdates(existingDomain, newDomain, domainHistory, now);
statusUpdateBillingEvent.ifPresent(entitiesToSave::add);
statusUpdateBillingEvent.ifPresent(entitiesToInsert::add);
Optional<PollMessage.OneTime> serverStatusUpdatePollMessage =
createPollMessageForServerStatusUpdates(existingDomain, newDomain, domainHistory, now);
serverStatusUpdatePollMessage.ifPresent(entitiesToSave::add);
serverStatusUpdatePollMessage.ifPresent(entitiesToInsert::add);
EntityChanges entityChanges =
flowCustomLogic.beforeSave(
BeforeSaveParameters.newBuilder()
@@ -205,7 +205,10 @@ public final class DomainUpdateFlow implements MutatingFlow {
.setNewDomain(newDomain)
.setExistingDomain(existingDomain)
.setEntityChanges(
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build())
EntityChanges.newBuilder()
.setInserts(entitiesToInsert.build())
.setUpdates(entitiesToUpdate.build())
.build())
.build());
persistEntityChanges(entityChanges);
return responseBuilder.build();
@@ -243,7 +246,6 @@ public final class DomainUpdateFlow implements MutatingFlow {
add.getNameservers());
validateContactsHaveTypes(add.getContacts());
validateContactsHaveTypes(remove.getContacts());
validateRegistrantAllowedOnTld(tldStr, command.getInnerChange().getRegistrantContactId());
validateNameserversAllowedOnTld(tldStr, add.getNameserverHostNames());
}
@@ -16,7 +16,6 @@ package google.registry.flows.host;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount;
import static google.registry.model.EppResourceUtils.checkResourcesExist;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -26,6 +25,7 @@ import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CheckData.HostCheck;
import google.registry.model.eppoutput.CheckData.HostCheckData;
@@ -61,7 +61,8 @@ public final class HostCheckFlow implements TransactionalFlow {
extensionManager.validate(); // There are no legal extensions for this flow.
ImmutableList<String> hostnames = ((Check) resourceCommand).getTargetIds();
verifyTargetIdCount(hostnames, maxChecks);
ImmutableSet<String> existingIds = checkResourcesExist(Host.class, hostnames, clock.nowUtc());
ImmutableSet<String> existingIds =
ForeignKeyUtils.loadKeys(Host.class, hostnames, clock.nowUtc()).keySet();
ImmutableList.Builder<HostCheck> checks = new ImmutableList.Builder<>();
for (String hostname : hostnames) {
boolean unused = !existingIds.contains(hostname);
@@ -126,7 +126,8 @@ public final class HostCreateFlow implements MutatingFlow {
.setSuperordinateDomain(superordinateDomain.map(Domain::createVKey).orElse(null))
.build();
historyBuilder.setType(HOST_CREATE).setHost(newHost);
ImmutableSet<ImmutableObject> entitiesToSave = ImmutableSet.of(newHost, historyBuilder.build());
ImmutableSet<ImmutableObject> entitiesToInsert =
ImmutableSet.of(newHost, historyBuilder.build());
if (superordinateDomain.isPresent()) {
tm().update(
superordinateDomain
@@ -138,7 +139,7 @@ public final class HostCreateFlow implements MutatingFlow {
// they are only written as NS records from the referencing domain.
requestHostDnsRefresh(targetId);
}
tm().insertAll(entitiesToSave);
tm().insertAll(entitiesToInsert);
return responseBuilder.setResData(HostCreateData.create(targetId, now)).build();
}
@@ -15,7 +15,6 @@
package google.registry.flows.host;
import static google.registry.model.EppResourceUtils.isActive;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.stream.Collectors.joining;
@@ -29,6 +28,7 @@ import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueRangeErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.util.Idn;
@@ -90,7 +90,8 @@ public class HostFlowUtils {
hostName.parts().stream()
.skip(hostName.parts().size() - (tld.get().parts().size() + 1))
.collect(joining("."));
Optional<Domain> superordinateDomain = loadByForeignKey(Domain.class, domainName, now);
Optional<Domain> superordinateDomain =
ForeignKeyUtils.loadResource(Domain.class, domainName, now);
if (superordinateDomain.isEmpty() || !isActive(superordinateDomain.get(), now)) {
throw new SuperordinateDomainDoesNotExistException(domainName);
}
@@ -52,7 +52,6 @@ import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.Domain;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.StatusValue;
@@ -155,7 +154,7 @@ public final class HostUpdateFlow implements MutatingFlow {
EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost);
verifyUpdateAllowed(
command, existingHost, newSuperordinateDomain.orElse(null), owningResource, isHostRename);
if (isHostRename && ForeignKeyUtils.load(Host.class, newHostName, now) != null) {
if (isHostRename && ForeignKeyUtils.loadKey(Host.class, newHostName, now).isPresent()) {
throw new HostAlreadyExistsException(newHostName);
}
AddRemove add = command.getInnerAdd();
@@ -198,16 +197,12 @@ public final class HostUpdateFlow implements MutatingFlow {
.setPersistedCurrentSponsorRegistrarId(newPersistedRegistrarId)
.build();
verifyHasIpsIffIsExternal(command, existingHost, newHost);
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
entitiesToUpdate.add(newHost);
if (isHostRename) {
updateSuperordinateDomains(existingHost, newHost);
}
enqueueTasks(existingHost, newHost);
entitiesToInsert.add(historyBuilder.setType(HOST_UPDATE).setHost(newHost).build());
tm().updateAll(entitiesToUpdate.build());
tm().insertAll(entitiesToInsert.build());
tm().update(newHost);
tm().insert(historyBuilder.setType(HOST_UPDATE).setHost(newHost).build());
return responseBuilder.build();
}
@@ -290,7 +285,7 @@ public final class HostUpdateFlow implements MutatingFlow {
&& newHost.isSubordinate()
&& Objects.equals(
existingHost.getSuperordinateDomain(), newHost.getSuperordinateDomain())) {
tm().put(
tm().update(
tm().loadByKey(existingHost.getSuperordinateDomain())
.asBuilder()
.removeSubordinateHost(existingHost.getHostName())
@@ -299,14 +294,14 @@ public final class HostUpdateFlow implements MutatingFlow {
return;
}
if (existingHost.isSubordinate()) {
tm().put(
tm().update(
tm().loadByKey(existingHost.getSuperordinateDomain())
.asBuilder()
.removeSubordinateHost(existingHost.getHostName())
.build());
}
if (newHost.isSubordinate()) {
tm().put(
tm().update(
tm().loadByKey(newHost.getSuperordinateDomain())
.asBuilder()
.addSubordinateHost(newHost.getHostName())
@@ -16,19 +16,14 @@ package google.registry.model;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.latestOf;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig;
import google.registry.model.EppResource.BuilderWithTransferData;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
@@ -41,13 +36,9 @@ import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionManager;
import jakarta.persistence.Query;
import java.util.Collection;
import java.util.Comparator;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.Interval;
@@ -90,123 +81,6 @@ public final class EppResourceUtils {
return (T) resource.cloneProjectedAtTime(now);
}
/**
* Loads the last created version of an {@link EppResource} from the database by foreign key.
*
* <p>Returns empty if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
*
* <p>Loading an {@link EppResource} by itself is not sufficient to know its current state since
* it may have various expirable conditions and status values that might implicitly change its
* state as time progresses even if it has not been updated in the database. Rather, the resource
* must be combined with a timestamp to view its current state. We use a global last updated
* timestamp on the resource's entity group (which is essentially free since all writes to the
* entity group must be serialized anyways) to guarantee monotonically increasing write times, and
* forward our projected time to the greater of this timestamp or "now". This guarantees that
* we're not projecting into the past.
*
* @param clazz the resource type to load
* @param foreignKey id to match
* @param now the current logical time to project resources at
*/
public static <T extends EppResource> Optional<T> loadByForeignKey(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(tm(), clazz, foreignKey, now, false);
}
/**
* Loads the last created version of an {@link EppResource} from the database by foreign key,
* using a cache, if caching is enabled in config settings.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
*
* <p>Loading an {@link EppResource} by itself is not sufficient to know its current state since
* it may have various expirable conditions and status values that might implicitly change its
* state as time progresses even if it has not been updated in the database. Rather, the resource
* must be combined with a timestamp to view its current state. We use a global last updated
* timestamp to guarantee monotonically increasing write times, and forward our projected time to
* the greater of this timestamp or "now". This guarantees that we're not projecting into the
* past.
*
* <p>Do not call this cached version for anything that needs transactional consistency. It should
* only be used when it's OK if the data is potentially being out of date, e.g. WHOIS.
*
* @param clazz the resource type to load
* @param foreignKey id to match
* @param now the current logical time to project resources at
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyByCacheIfEnabled(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(
tm(), clazz, foreignKey, now, RegistryConfig.isEppResourceCachingEnabled());
}
/**
* Loads the last created version of an {@link EppResource} from the replica database by foreign
* key, using a cache.
*
* <p>This method ignores the config setting for caching, and is reserved for use cases that can
* tolerate slightly stale data.
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyByCache(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(replicaTm(), clazz, foreignKey, now, true);
}
private static <T extends EppResource> Optional<T> loadByForeignKeyHelper(
TransactionManager txnManager,
Class<T> clazz,
String foreignKey,
DateTime now,
boolean useCache) {
checkArgument(
ForeignKeyedEppResource.class.isAssignableFrom(clazz),
"loadByForeignKey may only be called for foreign keyed EPP resources");
VKey<T> key =
useCache
? ForeignKeyUtils.loadByCache(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
: ForeignKeyUtils.load(clazz, foreignKey, now);
// The returned key is null if the resource is hard deleted or soft deleted by the given time.
if (key == null) {
return Optional.empty();
}
T resource =
useCache
? EppResource.loadByCache(key)
// This transaction is buried very deeply inside many outer nested calls, hence merits
// the use of reTransact() for now pending a substantial refactoring.
: txnManager.reTransact(() -> txnManager.loadByKeyIfPresent(key).orElse(null));
if (resource == null || isAtOrAfter(now, resource.getDeletionTime())) {
return Optional.empty();
}
// When setting status values based on a time, choose the greater of "now" and the resource's
// UpdateAutoTimestamp. For non-mutating uses (info, whois, etc.), this is equivalent to rolling
// "now" forward to at least the last update on the resource, so that a read right after a write
// doesn't appear stale. For mutating flows, if we had to roll now forward then the flow will
// fail when it tries to save anything, since "now" is needed to be > the last update time for
// writes.
return Optional.of(
cloneProjectedAtTime(
resource, latestOf(now, resource.getUpdateTimestamp().getTimestamp())));
}
/**
* Checks multiple {@link EppResource} objects from the database by unique ids.
*
* <p>There are currently no resources that support checks and do not use foreign keys. If we need
* to support that case in the future, we can loosen the type to allow any {@link EppResource} and
* add code to do the lookup by id directly.
*
* @param clazz the resource type to load
* @param uniqueIds a list of ids to match
* @param now the logical time of the check
*/
public static <T extends EppResource> ImmutableSet<String> checkResourcesExist(
Class<T> clazz, Collection<String> uniqueIds, final DateTime now) {
return ForeignKeyUtils.load(clazz, uniqueIds, now).keySet();
}
/**
* Returns a Function that transforms an EppResource to the given DateTime, suitable for use with
* Iterables.transform() over a collection of EppResources.
@@ -306,21 +180,6 @@ public final class EppResourceUtils {
: null);
}
/**
* Rewinds an {@link EppResource} object to a given point in time.
*
* <p>This method costs nothing if {@code resource} is already current. Otherwise, it returns an
* async operation that performs a single fetch operation.
*
* @return an asynchronous operation returning resource at {@code timestamp} or {@code null} if
* resource is deleted or not yet created
* @see #loadAtPointInTime(EppResource, DateTime)
*/
public static <T extends EppResource> Supplier<T> loadAtPointInTimeAsync(
final T resource, final DateTime timestamp) {
return () -> loadAtPointInTime(resource, timestamp);
}
/**
* Returns the most recent revision of a given EppResource before or at the provided timestamp,
* falling back to using the resource as-is if there are no revisions.
@@ -395,13 +254,11 @@ public final class EppResourceUtils {
/**
* Returns whether the given contact or host is linked to (that is, referenced by) a domain.
*
* <p>This is an eventually consistent query.
*
* @param key the referent key
* @param now the logical time of the check
*/
public static boolean isLinked(VKey<? extends EppResource> key, DateTime now) {
return getLinkedDomainKeys(key, now, 1).size() > 0;
return !getLinkedDomainKeys(key, now, 1).isEmpty();
}
private EppResourceUtils() {}
@@ -41,13 +41,17 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/**
* Util class to map a foreign key to the {@link VKey} to the active instance of {@link EppResource}
* whose unique repoId matches the foreign key string at a given time. The instance is never
* deleted, but it is updated if a newer entity becomes the active entity.
* Util class for mapping a foreign key to a {@link VKey} or {@link EppResource}.
*
* <p>We return the resource that matches the foreign key at a given time (this may vary over time
* e.g. if a domain expires and is re-registered). The instance is never deleted, but it is updated
* if a newer entity becomes the active entity.
*
* <p>One can retrieve either the {@link VKey} (the repo ID) in the situations where that's
* sufficient, or the resource itself, either with or without caching.
*/
public final class ForeignKeyUtils {
@@ -60,21 +64,30 @@ public final class ForeignKeyUtils {
Domain.class, "domainName",
Host.class, "hostName");
public record MostRecentResource(String repoId, DateTime deletionTime) {}
/**
* Loads a {@link VKey} to an {@link EppResource} from the database by foreign key.
* Loads an optional {@link VKey} to an {@link EppResource} from the database by foreign key.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* <p>Returns empty if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
*
* @param clazz the resource type to load
* @param foreignKey foreign key to match
* @param now the current logical time to use when checking for soft deletion of the foreign key
* index
*/
@Nullable
public static <E extends EppResource> VKey<E> load(
public static <E extends EppResource> Optional<VKey<E>> loadKey(
Class<E> clazz, String foreignKey, DateTime now) {
return load(clazz, ImmutableList.of(foreignKey), now).get(foreignKey);
return Optional.ofNullable(loadKeys(clazz, ImmutableList.of(foreignKey), now).get(foreignKey));
}
/**
* Loads an {@link EppResource} from the database by foreign key.
*
* <p>Returns null if no resource with this foreign key was ever created or if the most recently
* created resource was deleted before time "now".
*/
public static <E extends EppResource> Optional<E> loadResource(
Class<E> clazz, String foreignKey, DateTime now) {
// Note: no need to project to "now" because loadResources already does
return Optional.ofNullable(
loadResources(clazz, ImmutableList.of(foreignKey), now).get(foreignKey));
}
/**
@@ -84,13 +97,28 @@ public final class ForeignKeyUtils {
* <p>The returned map will omit any foreign keys for which the {@link EppResource} doesn't exist
* or has been soft-deleted.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> load(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return load(clazz, foreignKeys, false).entrySet().stream()
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadKeys(
Class<E> clazz, Collection<String> foreignKeys, DateTime now) {
return loadMostRecentResources(clazz, foreignKeys, false).entrySet().stream()
.filter(e -> now.isBefore(e.getValue().deletionTime()))
.collect(toImmutableMap(Entry::getKey, e -> VKey.create(clazz, e.getValue().repoId())));
}
/**
* Load a map of {@link String} foreign keys to the {@link EppResource} that are active at or
* after the specified moment in time.
*
* <p>The returned map will omit any foreign keys for which the {@link EppResource} doesn't exist
* or has been soft-deleted.
*/
@SuppressWarnings("unchecked")
public static <E extends EppResource> ImmutableMap<String, E> loadResources(
Class<E> clazz, Collection<String> foreignKeys, DateTime now) {
return loadMostRecentResourceObjects(clazz, foreignKeys, false).entrySet().stream()
.filter(e -> now.isBefore(e.getValue().getDeletionTime()))
.collect(toImmutableMap(Entry::getKey, e -> (E) e.getValue().cloneProjectedAtTime(now)));
}
/**
* Helper method to load {@link VKey}s to all the most recent {@link EppResource}s for the given
* foreign keys, regardless of whether they have been soft-deleted.
@@ -104,8 +132,9 @@ public final class ForeignKeyUtils {
* same max {@code deleteTime}, usually {@code END_OF_TIME}, lest this method throws an error due
* to duplicate keys.
*/
private static <E extends EppResource> ImmutableMap<String, MostRecentResource> load(
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaTm) {
public static <E extends EppResource>
ImmutableMap<String, MostRecentResource> loadMostRecentResources(
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaTm) {
String fkProperty = RESOURCE_TYPE_TO_FK_PROPERTY.get(clazz);
JpaTransactionManager tmToUse = useReplicaTm ? replicaTm() : tm();
return tmToUse.reTransact(
@@ -123,22 +152,49 @@ public final class ForeignKeyUtils {
.collect(
toImmutableMap(
row -> (String) row[0],
row -> MostRecentResource.create((String) row[1], (DateTime) row[2]))));
row -> new MostRecentResource((String) row[1], (DateTime) row[2]))));
}
private static final CacheLoader<VKey<? extends EppResource>, Optional<MostRecentResource>>
CACHE_LOADER =
new CacheLoader<>() {
/** Method to load the most recent {@link EppResource}s for the given foreign keys. */
private static <E extends EppResource> ImmutableMap<String, E> loadMostRecentResourceObjects(
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaTm) {
String fkProperty = RESOURCE_TYPE_TO_FK_PROPERTY.get(clazz);
JpaTransactionManager tmToUse = useReplicaTm ? replicaTm() : tm();
return tmToUse.reTransact(
() ->
tmToUse
.query(
("FROM %entity% WHERE (%fkProperty%, deletionTime) IN (SELECT %fkProperty%, "
+ "MAX(deletionTime) FROM %entity% WHERE %fkProperty% IN (:fks) "
+ "GROUP BY %fkProperty%)")
.replace("%fkProperty%", fkProperty)
.replace("%entity%", clazz.getSimpleName()),
clazz)
.setParameter("fks", foreignKeys)
.getResultStream()
.collect(toImmutableMap(EppResource::getForeignKey, e -> e)));
}
/**
* Cache loader for loading repo IDs and deletion times for the given foreign keys.
*
* <p>Note: while this is given a {@link VKey}, one cannot use that key to load directly from the
* database. That key is basically used to signify a foreign-key + resource-type pairing.
*/
private static final CacheLoader<VKey<? extends EppResource>, Optional<MostRecentResource>>
REPO_ID_CACHE_LOADER =
new CacheLoader<>() {
@Override
public Optional<MostRecentResource> load(VKey<? extends EppResource> key) {
return loadAll(ImmutableSet.of(key)).get(key);
String foreignKey = (String) key.getKey();
return Optional.ofNullable(
loadMostRecentResources(key.getKind(), ImmutableList.of(foreignKey), true)
.get(foreignKey));
}
@Override
public Map<
? extends VKey<? extends EppResource>, ? extends Optional<MostRecentResource>>
loadAll(Set<? extends VKey<? extends EppResource>> keys) {
public Map<VKey<? extends EppResource>, ? extends Optional<MostRecentResource>> loadAll(
Set<? extends VKey<? extends EppResource>> keys) {
if (keys.isEmpty()) {
return ImmutableMap.of();
}
@@ -148,7 +204,7 @@ public final class ForeignKeyUtils {
ImmutableList<String> foreignKeys =
keys.stream().map(key -> (String) key.getKey()).collect(toImmutableList());
ImmutableMap<String, MostRecentResource> existingKeys =
ForeignKeyUtils.load(clazz, foreignKeys, true);
loadMostRecentResources(clazz, foreignKeys, true);
// The above map only contains keys that exist in the database, so we re-add the
// missing ones with Optional.empty() values for caching.
return Maps.asMap(
@@ -158,11 +214,10 @@ public final class ForeignKeyUtils {
};
/**
* A limited size, limited time cache for foreign-keyed entities.
* A limited size, limited time cache for mapping foreign keys to repo IDs.
*
* <p>This is only used to cache foreign-keyed entities for the purposes of checking whether they
* exist (and if so, what entity they point to) during a few domain flows. Any other operations on
* foreign keys should not use this cache.
* <p>This is only used to cache foreign-keyed entity keys for the purposes of checking whether
* they exist (and if so, what entity they point to).
*
* <p>Note that here the key of the {@link LoadingCache} is of type {@code VKey<? extends
* EppResource>}, but they are not legal {@link VKey}s to {@link EppResource}s, whose keys are the
@@ -176,22 +231,26 @@ public final class ForeignKeyUtils {
* our system that actually exist. So we cache the fact that they *don't* exist by using
* Optional.empty(), then several layers up the EPP command will fail with an error message like
* "The contact with given IDs (blah) don't exist."
*
* <p>If one wishes to load the entity itself via cache, use the {@link
* #foreignKeyToResourceCache} instead, as that loads the entity instead. This cache is used for
* situations where the repo ID, or the existence of the repo ID, is sufficient.
*/
@NonFinalForTesting
private static LoadingCache<VKey<? extends EppResource>, Optional<MostRecentResource>>
foreignKeyCache = createForeignKeyMapCache(getEppResourceCachingDuration());
foreignKeyToRepoIdCache = createForeignKeyToRepoIdCache(getEppResourceCachingDuration());
private static LoadingCache<VKey<? extends EppResource>, Optional<MostRecentResource>>
createForeignKeyMapCache(Duration expiry) {
createForeignKeyToRepoIdCache(Duration expiry) {
return CacheUtils.newCacheBuilder(expiry)
.maximumSize(getEppResourceMaxCachedEntries())
.build(CACHE_LOADER);
.build(REPO_ID_CACHE_LOADER);
}
@VisibleForTesting
public static void setCacheForTest(Optional<Duration> expiry) {
public static void setRepoIdCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getEppResourceCachingDuration());
foreignKeyCache = createForeignKeyMapCache(effectiveExpiry);
foreignKeyToRepoIdCache = createForeignKeyToRepoIdCache(effectiveExpiry);
}
/**
@@ -204,26 +263,12 @@ public final class ForeignKeyUtils {
* <p>Don't use the cached version of this method unless you really need it for performance
* reasons, and are OK with the trade-offs in loss of transactional consistency.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCacheIfEnabled(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadKeysByCacheIfEnabled(
Class<E> clazz, Collection<String> foreignKeys, DateTime now) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return load(clazz, foreignKeys, now);
return loadKeys(clazz, foreignKeys, now);
}
return loadByCache(clazz, foreignKeys, now);
}
/**
* Load a list of {@link VKey} to {@link EppResource} instances by class and foreign key strings
* that are active at or after the specified moment in time, using the cache.
*
* <p>The returned map will omit any keys for which the {@link EppResource} doesn't exist or has
* been soft-deleted.
*
* <p>This method is reserved for use cases that can tolerate slightly stale data.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCache(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return foreignKeyCache
return foreignKeyToRepoIdCache
.getAll(foreignKeys.stream().map(fk -> VKey.create(clazz, fk)).collect(toImmutableList()))
.entrySet()
.stream()
@@ -234,10 +279,127 @@ public final class ForeignKeyUtils {
e -> VKey.create(clazz, e.getValue().get().repoId())));
}
record MostRecentResource(String repoId, DateTime deletionTime) {
/** Loads an optional {@link VKey} to an {@link EppResource} using the cache. */
public static <E extends EppResource> Optional<VKey<E>> loadKeyByCache(
Class<E> clazz, String foreignKey, DateTime now) {
return foreignKeyToRepoIdCache
.get(VKey.create(clazz, foreignKey))
.filter(mrr -> now.isBefore(mrr.deletionTime()))
.map(mrr -> VKey.create(clazz, mrr.repoId()));
}
static MostRecentResource create(String repoId, DateTime deletionTime) {
return new MostRecentResource(repoId, deletionTime);
}
/**
* Cache loader for loading {@link EppResource}s for the given foreign keys.
*
* <p>Note: while this is given a {@link VKey}, one cannot use that key to load directly from the
* database. That key is basically used to signify a foreign-key + resource-type pairing.
*/
private static final CacheLoader<VKey<? extends EppResource>, Optional<? extends EppResource>>
RESOURCE_CACHE_LOADER =
new CacheLoader<>() {
@Override
public Optional<? extends EppResource> load(VKey<? extends EppResource> key) {
String foreignKey = (String) key.getKey();
return Optional.ofNullable(
loadMostRecentResourceObjects(key.getKind(), ImmutableList.of(foreignKey), true)
.get(foreignKey));
}
@Override
public Map<VKey<? extends EppResource>, Optional<? extends EppResource>> loadAll(
Set<? extends VKey<? extends EppResource>> keys) {
if (keys.isEmpty()) {
return ImmutableMap.of();
}
// It is safe to use the resource type of first element because when this function is
// called, it is always passed with a list of VKeys with the same type.
Class<? extends EppResource> clazz = keys.iterator().next().getKind();
ImmutableList<String> foreignKeys =
keys.stream().map(key -> (String) key.getKey()).collect(toImmutableList());
ImmutableMap<String, ? extends EppResource> existingResources =
loadMostRecentResourceObjects(clazz, foreignKeys, true);
// The above map only contains resources that exist in the database, so we re-add the
// missing ones with Optional.empty() values for caching.
return Maps.asMap(
ImmutableSet.copyOf(keys),
key -> Optional.ofNullable(existingResources.get((String) key.getKey())));
}
};
/**
* An additional limited size, limited time cache for foreign-keyed entities.
*
* <p>Note that here the key of the {@link LoadingCache} is of type {@code VKey<? extends
* EppResource>}, but they are not legal {@link VKey}s to {@link EppResource}s, whose keys are the
* SQL primary keys, i.e., the {@code repoId}s. Instead, their keys are the foreign keys used to
* query the database. We use {@link VKey} here because it is a convenient composite class that
* contains both the resource type and the foreign key, which are needed to for the query and
* caching.
*
* <p>Also note that the value type of this cache is {@link Optional} because the foreign keys in
* question are coming from external commands, and thus don't necessarily represent entities in
* our system that actually exist. So we cache the fact that they *don't* exist by using
* Optional.empty(), then several layers up the EPP command will fail with an error message like
* "The contact with given IDs (blah) don't exist."
*
* <p>This cache bypasses the foreign-key-to-repo-ID lookup and maps directly from the foreign key
* to the entity itself (at least, at this point in time).
*/
private static LoadingCache<VKey<? extends EppResource>, Optional<? extends EppResource>>
foreignKeyToResourceCache = createForeignKeyToResourceCache(getEppResourceCachingDuration());
private static LoadingCache<VKey<? extends EppResource>, Optional<? extends EppResource>>
createForeignKeyToResourceCache(Duration expiry) {
return CacheUtils.newCacheBuilder(expiry)
.maximumSize(getEppResourceMaxCachedEntries())
.build(RESOURCE_CACHE_LOADER);
}
@VisibleForTesting
public static void setResourceCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getEppResourceCachingDuration());
foreignKeyToResourceCache = createForeignKeyToResourceCache(effectiveExpiry);
}
/**
* Loads the last created version of an {@link EppResource} from the database by foreign key,
* using a cache, if caching is enabled in config settings.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
*
* <p>Loading an {@link EppResource} by itself is not sufficient to know its current state since
* it may have various expirable conditions and status values that might implicitly change its
* state as time progresses even if it has not been updated in the database. Rather, the resource
* must be combined with a timestamp to view its current state. We use a global last updated
* timestamp to guarantee monotonically increasing write times, and forward our projected time to
* the greater of this timestamp or "now". This guarantees that we're not projecting into the
* past.
*
* <p>Do not call this cached version for anything that needs transactional consistency. It should
* only be used when it's OK if the data is potentially being out of date, e.g. RDAP.
*/
public static <E extends EppResource> Optional<E> loadResourceByCacheIfEnabled(
Class<E> clazz, String foreignKey, DateTime now) {
return RegistryConfig.isEppResourceCachingEnabled()
? loadResourceByCache(clazz, foreignKey, now)
: loadResource(clazz, foreignKey, now);
}
/**
* Loads the last created version of an {@link EppResource} from the replica database by foreign
* key, 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> Optional<E> loadResourceByCache(
Class<E> clazz, String foreignKey, DateTime now) {
return (Optional<E>)
foreignKeyToResourceCache
.get(VKey.create(clazz, foreignKey))
.filter(e -> now.isBefore(e.getDeletionTime()))
.map(e -> e.cloneProjectedAtTime(now));
}
}
@@ -0,0 +1,49 @@
// Copyright 2025 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.model;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* A delegate getter method to be used when getting the value of an {@link ImmutableObject} field.
*
* <p>This is useful because Hibernate has limitations on what kinds of types can be used to
* represent a field value, the most relevant being that it must be mutable. Since we use Guava's
* ImmutableCollections widely, this means that a frequent pattern is to e.g. have a field be
* declared as a Set (with a HashSet implementation), but then implement a getter method for that
* field that returns the desired ImmutableSet or ImmutableSortedSet. For purposes where it matters
* that the field be represented using the appropriate type, such as for outputting in sorted order
* via toString, then declare a getter delegate as follows:
*
* <pre>{@code
* @GetterDelegate(methodName = "getAllowedTlds")
* Set<String> allowedTlds;
*
* public ImmutableSortedSet<String> getAllowedTlds() {
* return nullToEmptyImmutableSortedCopy(allowedTlds);
* }
* }</pre>
*/
@Documented
@Retention(RUNTIME)
@Target(FIELD)
public @interface GetterDelegate {
String methodName();
}
@@ -76,11 +76,20 @@ public class ModelUtils {
return ALL_FIELDS_CACHE.get(clazz);
}
/** Retrieves a field value via reflection. */
/**
* Retrieves a field value via reflection, using the field's {@link GetterDelegate} if present.
*/
static Object getFieldValue(Object instance, Field field) {
try {
return field.get(instance);
} catch (IllegalAccessException e) {
if (field.isAnnotationPresent(GetterDelegate.class)) {
return instance
.getClass()
.getMethod(field.getAnnotation(GetterDelegate.class).methodName())
.invoke(instance);
} else {
return field.get(instance);
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
@@ -442,7 +442,7 @@ public class DomainCommand {
final Set<String> foreignKeys, final Class<T> clazz, final DateTime now)
throws InvalidReferencesException {
ImmutableMap<String, VKey<T>> fks =
ForeignKeyUtils.loadByCacheIfEnabled(clazz, foreignKeys, now);
ForeignKeyUtils.loadKeysByCacheIfEnabled(clazz, foreignKeys, now);
if (!fks.keySet().equals(foreignKeys)) {
throw new InvalidReferencesException(
clazz, ImmutableSet.copyOf(difference(foreignKeys, fks.keySet())));
@@ -64,6 +64,7 @@ public class Fee extends BaseFee {
public static final ImmutableSet<String> FEE_EXTENSION_URIS =
ImmutableSet.of(
ServiceExtension.FEE_1_00.getUri(),
ServiceExtension.FEE_0_12.getUri(),
ServiceExtension.FEE_0_11.getUri(),
ServiceExtension.FEE_0_6.getUri());
@@ -0,0 +1,112 @@
// Copyright 2025 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.model.domain.feestdv1;
import com.google.common.base.Ascii;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.FeeCheckCommandExtensionItem;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.Locale;
import java.util.Optional;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
/**
* An individual price check item in version 1.0 of the fee extension on domain check commands.
* Items look like:
*
* <pre>{@code
* <fee:command name="renew" phase="sunrise" subphase="hello">
* <fee:period unit="y">1</fee:period>
* <fee:class>premium</fee:class>
* <fee:date>2017-05-17T13:22:21.0Z</fee:date>
* </fee:command>
* }</pre>
*/
@XmlType(propOrder = {"period", "feeClass", "feeDate"})
public class FeeCheckCommandExtensionItemStdV1 extends FeeCheckCommandExtensionItem {
/** The default validity period (if not specified) is 1 year for all operations. */
static final Period DEFAULT_PERIOD = Period.create(1, Period.Unit.YEARS);
@XmlAttribute(name = "name")
String commandName;
@XmlAttribute String phase;
@XmlAttribute String subphase;
@XmlElement(name = "class")
String feeClass;
@XmlElement(name = "date")
DateTime feeDate;
/** Version 1.0 does not support domain name or currency in fee extension items. */
@Override
public boolean isDomainNameSupported() {
return false;
}
@Override
public String getDomainName() {
throw new UnsupportedOperationException("Domain not supported");
}
@Override
public CurrencyUnit getCurrency() {
return null; // This version of the fee extension doesn't specify currency per-item.
}
@Override
public String getUnparsedCommandName() {
return commandName;
}
@Override
public CommandName getCommandName() {
// Require the xml string to be lowercase.
if (commandName != null && commandName.toLowerCase(Locale.ENGLISH).equals(commandName)) {
try {
return CommandName.valueOf(Ascii.toUpperCase(commandName));
} catch (IllegalArgumentException e) {
// Swallow this and return UNKNOWN below because there's no matching CommandName.
}
}
return CommandName.UNKNOWN;
}
@Override
public String getPhase() {
return phase;
}
@Override
public String getSubphase() {
return subphase;
}
@Override
public FeeCheckResponseExtensionItemStdV1.Builder createResponseBuilder() {
return new FeeCheckResponseExtensionItemStdV1.Builder();
}
@Override
public Optional<DateTime> getEffectiveDate() {
return Optional.ofNullable(feeDate);
}
}
@@ -0,0 +1,63 @@
// Copyright 2025 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.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.fee.FeeCheckCommandExtension;
import google.registry.model.domain.fee.FeeCheckResponseExtensionItem;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.List;
import org.joda.money.CurrencyUnit;
/** Version 1.0 of the fee extension that may be present on domain check commands. */
@XmlRootElement(name = "check")
@XmlType(propOrder = {"currency", "items"})
public class FeeCheckCommandExtensionStdV1 extends ImmutableObject
implements FeeCheckCommandExtension<
FeeCheckCommandExtensionItemStdV1, FeeCheckResponseExtensionStdV1> {
CurrencyUnit currency;
@Override
public CurrencyUnit getCurrency() {
return currency;
}
@XmlElement(name = "command")
List<FeeCheckCommandExtensionItemStdV1> items;
@Override
public ImmutableList<FeeCheckCommandExtensionItemStdV1> getItems() {
return nullToEmptyImmutableCopy(items);
}
@Override
public FeeCheckResponseExtensionStdV1 createResponse(
ImmutableList<? extends FeeCheckResponseExtensionItem> items) {
ImmutableList.Builder<FeeCheckResponseExtensionItemStdV1> builder =
new ImmutableList.Builder<>();
for (FeeCheckResponseExtensionItem item : items) {
if (item instanceof FeeCheckResponseExtensionItemStdV1) {
builder.add((FeeCheckResponseExtensionItemStdV1) item);
}
}
return FeeCheckResponseExtensionStdV1.create(currency, builder.build());
}
}
@@ -0,0 +1,119 @@
// Copyright 2025 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.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.List;
import org.joda.time.DateTime;
/** The version 1.0 response command entity for a domain check on a single resource. */
@XmlType(propOrder = {"period", "fee", "feeClass", "effectiveDate", "notAfterDate"})
public class FeeCheckResponseExtensionItemCommandStdV1 extends ImmutableObject {
/** The command that was checked. */
@XmlAttribute(name = "name")
String commandName;
/** The phase that was checked. */
@XmlAttribute String phase;
/** The subphase that was checked. */
@XmlAttribute String subphase;
/** The period that was checked. */
Period period;
/**
* The magnitude of the fee, in the specified units, with an optional description.
*
* <p>This is a list because a single operation can involve multiple fees.
*/
List<Fee> fee;
/**
* The type of the fee.
*
* <p>We will use "premium" for fees on premium names, and omit the field otherwise.
*/
@XmlElement(name = "class")
String feeClass;
/** The effective date that the check is to be performed on (if specified in the query). */
@XmlElement(name = "date")
DateTime effectiveDate;
/** The date after which the quoted fee is no longer valid (if applicable). */
@XmlElement(name = "notAfter")
DateTime notAfterDate;
public String getFeeClass() {
return feeClass;
}
/** Builder for {@link FeeCheckResponseExtensionItemCommandStdV1}. */
public static class Builder extends Buildable.Builder<FeeCheckResponseExtensionItemCommandStdV1> {
public Builder setCommandName(CommandName commandName) {
getInstance().commandName = Ascii.toLowerCase(commandName.name());
return this;
}
public Builder setPhase(String phase) {
getInstance().phase = phase;
return this;
}
public Builder setSubphase(String subphase) {
getInstance().subphase = subphase;
return this;
}
public Builder setPeriod(Period period) {
getInstance().period = period;
return this;
}
public Builder setEffectiveDate(DateTime effectiveDate) {
getInstance().effectiveDate = effectiveDate;
return this;
}
public Builder setNotAfterDate(DateTime notAfterDate) {
getInstance().notAfterDate = notAfterDate;
return this;
}
public Builder setFee(List<Fee> fees) {
getInstance().fee = forceEmptyToNull(ImmutableList.copyOf(fees));
return this;
}
public Builder setClass(String feeClass) {
getInstance().feeClass = feeClass;
return this;
}
}
}
@@ -0,0 +1,122 @@
// Copyright 2025 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.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.common.collect.ImmutableList;
import google.registry.model.domain.DomainObjectSpec;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeCheckResponseExtensionItem;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import jakarta.xml.bind.annotation.XmlType;
import org.joda.time.DateTime;
/** The version 1.0 response for a domain check on a single resource. */
@XmlType(propOrder = {"object", "command"})
public class FeeCheckResponseExtensionItemStdV1 extends FeeCheckResponseExtensionItem {
/** The domain that was checked. */
DomainObjectSpec object;
/** The command that was checked. */
FeeCheckResponseExtensionItemCommandStdV1 command;
/**
* This method is overridden and not annotated for JAXB because this version of the extension
* doesn't support "period".
*/
@Override
public Period getPeriod() {
return super.getPeriod();
}
/**
* This method is overridden and not annotated for JAXB because this version of the extension
* doesn't support "fee".
*/
@Override
public ImmutableList<Fee> getFees() {
return super.getFees();
}
/**
* This method is not annotated for JAXB because this version of the extension doesn't support
* "feeClass" and because the data comes off of the command object rather than a field.
*/
@Override
public String getFeeClass() {
return command.getFeeClass();
}
/** Builder for {@link FeeCheckResponseExtensionItemStdV1}. */
public static class Builder
extends FeeCheckResponseExtensionItem.Builder<FeeCheckResponseExtensionItemStdV1> {
final FeeCheckResponseExtensionItemCommandStdV1.Builder commandBuilder =
new FeeCheckResponseExtensionItemCommandStdV1.Builder();
@Override
public Builder setCommand(CommandName commandName, String phase, String subphase) {
commandBuilder.setCommandName(commandName);
commandBuilder.setPhase(phase);
commandBuilder.setSubphase(subphase);
return this;
}
@Override
public Builder setPeriod(Period period) {
commandBuilder.setPeriod(period);
return this;
}
@Override
public Builder setFees(ImmutableList<Fee> fees) {
commandBuilder.setFee(forceEmptyToNull(ImmutableList.copyOf(fees)));
return this;
}
@Override
public Builder setClass(String feeClass) {
commandBuilder.setClass(feeClass);
return this;
}
@Override
public Builder setDomainNameIfSupported(String name) {
getInstance().object = new DomainObjectSpec(name);
return this;
}
@Override
public FeeCheckResponseExtensionItemStdV1 build() {
getInstance().command = commandBuilder.build();
return super.build();
}
@Override
public Builder setEffectiveDateIfSupported(DateTime effectiveDate) {
commandBuilder.setEffectiveDate(effectiveDate);
return this;
}
@Override
public Builder setNotAfterDateIfSupported(DateTime notAfterDate) {
commandBuilder.setNotAfterDate(notAfterDate);
return this;
}
}
}
@@ -0,0 +1,59 @@
// Copyright 2025 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.model.domain.feestdv1;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.fee.FeeCheckResponseExtension;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import org.joda.money.CurrencyUnit;
/**
* An XML data object that represents version 1.0 of the fee extension that may be present on the
* response to EPP domain check commands.
*/
@XmlRootElement(name = "chkData")
@XmlType(propOrder = {"currency", "items"})
public class FeeCheckResponseExtensionStdV1 extends ImmutableObject
implements FeeCheckResponseExtension<FeeCheckResponseExtensionItemStdV1> {
CurrencyUnit currency;
/** Check responses. */
@XmlElement(name = "cd")
ImmutableList<FeeCheckResponseExtensionItemStdV1> items;
@Override
public void setCurrencyIfSupported(CurrencyUnit currency) {
this.currency = currency;
}
@VisibleForTesting
@Override
public ImmutableList<FeeCheckResponseExtensionItemStdV1> getItems() {
return items;
}
static FeeCheckResponseExtensionStdV1 create(
CurrencyUnit currency, ImmutableList<FeeCheckResponseExtensionItemStdV1> items) {
FeeCheckResponseExtensionStdV1 instance = new FeeCheckResponseExtensionStdV1();
instance.currency = currency;
instance.items = items;
return instance;
}
}
@@ -0,0 +1,45 @@
// Copyright 2025 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.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.FeeCreateCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.List;
/** A fee extension that may be present on domain create commands. */
@XmlRootElement(name = "create")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeCreateCommandExtensionStdV1 extends FeeCreateCommandExtension {
@XmlElement(name = "credit")
List<Credit> credits;
@Override
public ImmutableList<Credit> getCredits() {
return nullToEmptyImmutableCopy(credits);
}
@Override
public FeeTransformResponseExtension.Builder createResponseBuilder() {
return new FeeTransformResponseExtension.Builder(new FeeCreateResponseExtensionStdV1());
}
}
@@ -0,0 +1,27 @@
// Copyright 2025 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.model.domain.feestdv1;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
/**
* An XML data object that represents a fee extension that may be present on the response to EPP
* domain create commands.
*/
@XmlRootElement(name = "creData")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeCreateResponseExtensionStdV1 extends FeeTransformResponseExtension {}
@@ -0,0 +1,35 @@
// Copyright 2025 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.model.domain.feestdv1;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
/**
* An XML data object that represents a fee extension that may be present on the response to EPP
* domain create commands.
*/
@XmlRootElement(name = "delData")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeDeleteResponseExtensionStdV1 extends FeeTransformResponseExtension {
/** Builder for {@link FeeDeleteResponseExtensionStdV1}. */
public static class Builder extends FeeTransformResponseExtension.Builder {
public Builder() {
super(new FeeDeleteResponseExtensionStdV1());
}
}
}
@@ -0,0 +1,45 @@
// Copyright 2025 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.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.FeeRenewCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.List;
/** A fee extension that may be present on domain renew commands. */
@XmlRootElement(name = "renew")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeRenewCommandExtensionStdV1 extends FeeRenewCommandExtension {
@XmlElement(name = "credit")
List<Credit> credits;
@Override
public ImmutableList<Credit> getCredits() {
return nullToEmptyImmutableCopy(credits);
}
@Override
public FeeTransformResponseExtension.Builder createResponseBuilder() {
return new FeeTransformResponseExtension.Builder(new FeeRenewResponseExtensionStdV1());
}
}
@@ -0,0 +1,27 @@
// Copyright 2025 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.model.domain.feestdv1;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
/**
* An XML data object that represents a fee extension that may be present on the response to EPP
* domain renew commands.
*/
@XmlRootElement(name = "renData")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeRenewResponseExtensionStdV1 extends FeeTransformResponseExtension {}
@@ -0,0 +1,45 @@
// Copyright 2025 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.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.FeeTransferCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.List;
/** A fee extension that may be present on domain transfer requests. */
@XmlRootElement(name = "transfer")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeTransferCommandExtensionStdV1 extends FeeTransferCommandExtension {
@XmlElement(name = "credit")
List<Credit> credits;
@Override
public ImmutableList<Credit> getCredits() {
return nullToEmptyImmutableCopy(credits);
}
@Override
public FeeTransformResponseExtension.Builder createResponseBuilder() {
return new FeeTransformResponseExtension.Builder(new FeeTransferResponseExtensionStdV1());
}
}
@@ -0,0 +1,27 @@
// Copyright 2025 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.model.domain.feestdv1;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
/**
* An XML data object that represents a fee extension that may be present on the response to EPP
* domain transfer requests.
*/
@XmlRootElement(name = "trnData")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeTransferResponseExtensionStdV1 extends FeeTransformResponseExtension {}
@@ -0,0 +1,45 @@
// Copyright 2025 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.model.domain.feestdv1;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.fee.FeeUpdateCommandExtension;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.List;
/** A fee extension that may be present on domain update commands. */
@XmlRootElement(name = "update")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeUpdateCommandExtensionStdV1 extends FeeUpdateCommandExtension {
@XmlElement(name = "credit")
List<Credit> credits;
@Override
public ImmutableList<Credit> getCredits() {
return nullToEmptyImmutableCopy(credits);
}
@Override
public FeeTransformResponseExtension.Builder createResponseBuilder() {
return new FeeTransformResponseExtension.Builder(new FeeUpdateResponseExtensionStdV1());
}
}
@@ -0,0 +1,27 @@
// Copyright 2025 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.model.domain.feestdv1;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
/**
* An XML data object that represents a fee extension that may be present on the response to EPP
* domain update commands.
*/
@XmlRootElement(name = "updData")
@XmlType(propOrder = {"currency", "fees", "credits"})
public class FeeUpdateResponseExtensionStdV1 extends FeeTransformResponseExtension {}
@@ -0,0 +1,34 @@
// Copyright 2025 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.
@XmlSchema(
namespace = "urn:ietf:params:xml:ns:epp:fee-1.0",
xmlns = @XmlNs(prefix = "fee_1_00", namespaceURI = "urn:ietf:params:xml:ns:epp:fee-1.0"),
elementFormDefault = XmlNsForm.QUALIFIED)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlJavaTypeAdapters({
@XmlJavaTypeAdapter(CurrencyUnitAdapter.class),
@XmlJavaTypeAdapter(UtcDateTimeAdapter.class)
})
package google.registry.model.domain.feestdv1;
import google.registry.model.adapters.CurrencyUnitAdapter;
import google.registry.xml.UtcDateTimeAdapter;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlNs;
import jakarta.xml.bind.annotation.XmlNsForm;
import jakarta.xml.bind.annotation.XmlSchema;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
@@ -43,6 +43,7 @@ public class EppXmlTransformer {
"fee06.xsd",
"fee11.xsd",
"fee12.xsd",
"fee-std-v1.xsd",
"metadata.xsd",
"mark.xsd",
"dsig.xsd",
@@ -25,6 +25,8 @@ import google.registry.model.domain.fee11.FeeCheckCommandExtensionV11;
import google.registry.model.domain.fee11.FeeCheckResponseExtensionV11;
import google.registry.model.domain.fee12.FeeCheckCommandExtensionV12;
import google.registry.model.domain.fee12.FeeCheckResponseExtensionV12;
import google.registry.model.domain.feestdv1.FeeCheckCommandExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeCheckResponseExtensionStdV1;
import google.registry.model.domain.launch.LaunchCreateExtension;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.RgpUpdateExtension;
@@ -54,6 +56,7 @@ public class ProtocolDefinition {
FEE_0_6(FeeCheckCommandExtensionV06.class, FeeCheckResponseExtensionV06.class, true),
FEE_0_11(FeeCheckCommandExtensionV11.class, FeeCheckResponseExtensionV11.class, true),
FEE_0_12(FeeCheckCommandExtensionV12.class, FeeCheckResponseExtensionV12.class, true),
FEE_1_00(FeeCheckCommandExtensionStdV1.class, FeeCheckResponseExtensionStdV1.class, true),
METADATA_1_0(MetadataExtension.class, null, false);
private final Class<? extends CommandExtension> commandExtensionClass;
@@ -40,6 +40,11 @@ import google.registry.model.domain.fee12.FeeCreateCommandExtensionV12;
import google.registry.model.domain.fee12.FeeRenewCommandExtensionV12;
import google.registry.model.domain.fee12.FeeTransferCommandExtensionV12;
import google.registry.model.domain.fee12.FeeUpdateCommandExtensionV12;
import google.registry.model.domain.feestdv1.FeeCheckCommandExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeCreateCommandExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeRenewCommandExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeTransferCommandExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeUpdateCommandExtensionStdV1;
import google.registry.model.domain.launch.LaunchCheckExtension;
import google.registry.model.domain.launch.LaunchCreateExtension;
import google.registry.model.domain.launch.LaunchDeleteExtension;
@@ -348,6 +353,13 @@ public class EppInput extends ImmutableObject {
@XmlElementRef(type = FeeTransferCommandExtensionV12.class),
@XmlElementRef(type = FeeUpdateCommandExtensionV12.class),
// Fee extension standard version 1.0 (RFC 8748)
@XmlElementRef(type = FeeCheckCommandExtensionStdV1.class),
@XmlElementRef(type = FeeCreateCommandExtensionStdV1.class),
@XmlElementRef(type = FeeRenewCommandExtensionStdV1.class),
@XmlElementRef(type = FeeTransferCommandExtensionStdV1.class),
@XmlElementRef(type = FeeUpdateCommandExtensionStdV1.class),
// Launch phase extensions
@XmlElementRef(type = LaunchCheckExtension.class),
@XmlElementRef(type = LaunchCreateExtension.class),
@@ -43,6 +43,12 @@ import google.registry.model.domain.fee12.FeeDeleteResponseExtensionV12;
import google.registry.model.domain.fee12.FeeRenewResponseExtensionV12;
import google.registry.model.domain.fee12.FeeTransferResponseExtensionV12;
import google.registry.model.domain.fee12.FeeUpdateResponseExtensionV12;
import google.registry.model.domain.feestdv1.FeeCheckResponseExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeCreateResponseExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeDeleteResponseExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeRenewResponseExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeTransferResponseExtensionStdV1;
import google.registry.model.domain.feestdv1.FeeUpdateResponseExtensionStdV1;
import google.registry.model.domain.launch.LaunchCheckResponseExtension;
import google.registry.model.domain.rgp.RgpInfoExtension;
import google.registry.model.domain.secdns.SecDnsInfoExtension;
@@ -142,6 +148,12 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
@XmlElementRef(type = FeeRenewResponseExtensionV12.class),
@XmlElementRef(type = FeeTransferResponseExtensionV12.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV12.class),
@XmlElementRef(type = FeeCheckResponseExtensionStdV1.class),
@XmlElementRef(type = FeeCreateResponseExtensionStdV1.class),
@XmlElementRef(type = FeeDeleteResponseExtensionStdV1.class),
@XmlElementRef(type = FeeRenewResponseExtensionStdV1.class),
@XmlElementRef(type = FeeTransferResponseExtensionStdV1.class),
@XmlElementRef(type = FeeUpdateResponseExtensionStdV1.class),
@XmlElementRef(type = LaunchCheckResponseExtension.class),
@XmlElementRef(type = RgpInfoExtension.class),
@XmlElementRef(type = SecDnsInfoExtension.class)
@@ -54,6 +54,7 @@ import com.google.gson.annotations.Expose;
import com.google.re2j.Pattern;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.GetterDelegate;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UpdateAutoTimestamp;
@@ -245,12 +246,15 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
State state;
/** The set of TLDs which this registrar is allowed to access. */
@Expose Set<String> allowedTlds;
@GetterDelegate(methodName = "getAllowedTlds")
@Expose
Set<String> allowedTlds;
/** Host name of WHOIS server. */
@Expose String whoisServer;
/** Base URLs for the registrar's RDAP servers. */
@GetterDelegate(methodName = "getRdapBaseUrls")
Set<String> rdapBaseUrls;
/**
@@ -492,7 +496,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return LIVE_STATES.contains(state);
}
/** Returns {@code true} if registrar should be visible in WHOIS results. */
/** Returns {@code true} if registrar should be visible in RDAP results. */
public boolean isLiveAndPubliclyVisible() {
return LIVE_STATES.contains(state) && PUBLICLY_VISIBLE_TYPES.contains(type);
}
@@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.GetterDelegate;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
@@ -110,6 +111,7 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
* data is internal to the registry.
*/
@Enumerated(EnumType.STRING)
@GetterDelegate(methodName = "getTypes")
@Expose
Set<Type> types;
@@ -521,11 +521,6 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
@Column(nullable = false)
DateTime claimsPeriodEnd = END_OF_TIME;
/** An allowlist of clients allowed to be used on domains on this TLD (ignored if empty). */
@Nullable
@JsonSerialize(using = SortedSetSerializer.class)
Set<String> allowedRegistrantContactIds;
/** An allowlist of hosts allowed to be used on domains on this TLD (ignored if empty). */
@Nullable
@JsonSerialize(using = SortedSetSerializer.class)
@@ -786,10 +781,6 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return tldUnicode;
}
public ImmutableSet<String> getAllowedRegistrantContactIds() {
return nullToEmptyImmutableCopy(allowedRegistrantContactIds);
}
public ImmutableSet<String> getAllowedFullyQualifiedHostNames() {
return nullToEmptyImmutableCopy(allowedFullyQualifiedHostNames);
}
@@ -1077,12 +1068,6 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
public Builder setAllowedRegistrantContactIds(
ImmutableSet<String> allowedRegistrantContactIds) {
getInstance().allowedRegistrantContactIds = allowedRegistrantContactIds;
return this;
}
public Builder setAllowedFullyQualifiedHostNames(
ImmutableSet<String> allowedFullyQualifiedHostNames) {
getInstance().allowedFullyQualifiedHostNames = allowedFullyQualifiedHostNames;
@@ -15,7 +15,6 @@
package google.registry.model.tmch;
import static google.registry.config.RegistryConfig.getClaimsListCacheDuration;
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -79,14 +78,11 @@ public class ClaimsListDao {
*/
private static ClaimsList getUncached() {
return tm().reTransact(
() -> {
Long revisionId =
tm().query("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
.getSingleResult();
return tm().createQueryComposer(ClaimsList.class)
.where("revisionId", EQ, revisionId)
.first();
})
() ->
tm().query("FROM ClaimsList ORDER BY revisionId DESC", ClaimsList.class)
.setMaxResults(1)
.getResultStream()
.findFirst())
.orElse(ClaimsList.create(START_OF_TIME, ImmutableMap.of()));
}
@@ -30,6 +30,7 @@ import google.registry.export.DriveModule;
import google.registry.export.sheet.SheetsServiceModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
@@ -66,6 +67,7 @@ import jakarta.inject.Singleton;
CredentialModule.class,
CustomLogicFactoryModule.class,
DirectoryModule.class,
DomainDeletionTimeCacheModule.class,
DriveModule.class,
GmailModule.class,
GroupsModule.class,
@@ -131,9 +131,6 @@ import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.RdapRegistrarFieldsAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.whois.WhoisAction;
import google.registry.whois.WhoisHttpAction;
import google.registry.whois.WhoisModule;
/** Dagger component with per-request lifetime. */
@RequestScope
@@ -161,8 +158,7 @@ import google.registry.whois.WhoisModule;
Spec11Module.class,
TmchModule.class,
ToolsServerModule.class,
WhiteboxModule.class,
WhoisModule.class,
WhiteboxModule.class
})
interface RequestComponent {
FlowComponent.Builder flowComponentBuilder();
@@ -343,10 +339,6 @@ interface RequestComponent {
VerifyOteAction verifyOteAction();
WhoisAction whoisAction();
WhoisHttpAction whoisHttpAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
@Subcomponent.Builder
@@ -26,6 +26,7 @@ import google.registry.export.DriveModule;
import google.registry.export.sheet.SheetsServiceModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
@@ -56,6 +57,7 @@ import jakarta.inject.Singleton;
CloudTasksUtilsModule.class,
CredentialModule.class,
CustomLogicFactoryModule.class,
DomainDeletionTimeCacheModule.class,
DirectoryModule.class,
DriveModule.class,
GmailModule.class,
@@ -22,6 +22,7 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
@@ -50,6 +51,7 @@ import jakarta.inject.Singleton;
CustomLogicFactoryModule.class,
CloudTasksUtilsModule.class,
DirectoryModule.class,
DomainDeletionTimeCacheModule.class,
FrontendRequestComponentModule.class,
GmailModule.class,
GroupsModule.class,
@@ -36,9 +36,6 @@ import google.registry.rdap.RdapNameserverSearchAction;
import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
import google.registry.whois.WhoisAction;
import google.registry.whois.WhoisHttpAction;
import google.registry.whois.WhoisModule;
/** Dagger component with per-request lifetime for "pubapi" App Engine module. */
@RequestScope
@@ -49,8 +46,7 @@ import google.registry.whois.WhoisModule;
EppTlsModule.class,
RdapModule.class,
RequestModule.class,
WhiteboxModule.class,
WhoisModule.class,
WhiteboxModule.class
})
public interface PubApiRequestComponent {
CheckApiAction checkApiAction();
@@ -67,9 +63,6 @@ public interface PubApiRequestComponent {
ReadinessProbeActionPubApi readinessProbeActionPubApi();
WhoisHttpAction whoisHttpAction();
WhoisAction whoisAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<PubApiRequestComponent> {
@Override public abstract Builder requestModule(RequestModule requestModule);
@@ -23,6 +23,7 @@ import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.export.DriveModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
@@ -47,6 +48,7 @@ import jakarta.inject.Singleton;
CustomLogicFactoryModule.class,
CloudTasksUtilsModule.class,
DirectoryModule.class,
DomainDeletionTimeCacheModule.class,
DriveModule.class,
GroupsModule.class,
GroupssettingsModule.class,
@@ -15,7 +15,6 @@
package google.registry.rdap;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -23,6 +22,7 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
@@ -68,7 +68,7 @@ public class RdapDomainAction extends RdapActionBase {
}
// The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com.
Optional<Domain> domain =
loadByForeignKeyByCache(
ForeignKeyUtils.loadResourceByCache(
Domain.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());
@@ -15,7 +15,6 @@
package google.registry.rdap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -59,6 +58,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.hibernate.Hibernate;
import org.joda.time.DateTime;
/**
* RDAP action for domain search requests.
@@ -183,7 +183,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
private DomainSearchResponse searchByDomainNameWithoutWildcard(
final RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyByCache(
ForeignKeyUtils.loadResourceByCache(
Domain.class, partialStringQuery.getInitialString(), getRequestTime());
return makeSearchResults(
shouldBeVisible(domain) ? ImmutableList.of(domain.get()) : ImmutableList.of());
@@ -333,40 +333,35 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
/** Assembles a list of {@link Host} keys by name when the pattern has no wildcard. */
private ImmutableList<VKey<Host>> getNameserverRefsByLdhNameWithoutWildcard(
final RdapSearchPattern partialStringQuery) {
DateTime timeToQuery = shouldIncludeDeleted() ? START_OF_TIME : getRequestTime();
// If we need to check the sponsoring registrar, we need to load the resource rather than just
// the key.
Optional<String> desiredRegistrar = getDesiredRegistrar();
String queryString = partialStringQuery.getInitialString();
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyByCache(
Host.class,
partialStringQuery.getInitialString(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
ForeignKeyUtils.loadResourceByCache(Host.class, queryString, timeToQuery);
return (host.isEmpty()
|| !desiredRegistrar.get().equals(host.get().getPersistedCurrentSponsorRegistrarId()))
? ImmutableList.of()
: ImmutableList.of(host.get().createVKey());
} else {
VKey<Host> hostKey =
ForeignKeyUtils.load(
Host.class,
partialStringQuery.getInitialString(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
return (hostKey == null) ? ImmutableList.of() : ImmutableList.of(hostKey);
Optional<VKey<Host>> hostKey =
ForeignKeyUtils.loadKeyByCache(Host.class, queryString, timeToQuery);
return hostKey.map(ImmutableList::of).orElseGet(ImmutableList::of);
}
}
/** Assembles a list of {@link Host} keys by name using a superordinate domain suffix. */
private ImmutableList<VKey<Host>> getNameserverRefsByLdhNameWithSuffix(
final RdapSearchPattern partialStringQuery) {
RdapSearchPattern partialStringQuery) {
DateTime timeToQuery = shouldIncludeDeleted() ? START_OF_TIME : getRequestTime();
// The suffix must be a domain that we manage. That way, we can look up the domain and search
// through the subordinate hosts. This is more efficient, and lets us permit wildcard searches
// with no initial string.
Domain domain =
loadByForeignKeyByCache(
Domain.class,
partialStringQuery.getSuffix(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime())
ForeignKeyUtils.loadResourceByCache(
Domain.class, partialStringQuery.getSuffix(), timeToQuery)
.orElseThrow(
() ->
new UnprocessableEntityException(
@@ -379,9 +374,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// then the query ns.exam*.example.com would match against nameserver ns.example.com.
if (partialStringQuery.matches(fqhn)) {
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyByCache(
Host.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
Optional<Host> host = ForeignKeyUtils.loadResourceByCache(Host.class, fqhn, timeToQuery);
if (host.isPresent()
&& desiredRegistrar
.get()
@@ -389,14 +382,10 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
builder.add(host.get().createVKey());
}
} else {
VKey<Host> hostKey =
ForeignKeyUtils.load(
Host.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
if (hostKey != null) {
builder.add(hostKey);
} else {
logger.atWarning().log("Host key unexpectedly null.");
}
Optional<VKey<Host>> hostKey =
ForeignKeyUtils.loadKeyByCache(Host.class, fqhn, timeToQuery);
hostKey.ifPresentOrElse(
builder::add, () -> logger.atWarning().log("Host key unexpectedly null."));
}
}
}
@@ -15,12 +15,12 @@
package google.registry.rdap;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import google.registry.flows.EppException;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.host.Host;
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
import google.registry.rdap.RdapMetrics.EndpointType;
@@ -63,7 +63,7 @@ public class RdapNameserverAction extends RdapActionBase {
// If there are no undeleted nameservers with the given name, the foreign key should point to
// the most recently deleted one.
Optional<Host> host =
loadByForeignKeyByCache(
ForeignKeyUtils.loadResourceByCache(
Host.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
@@ -14,7 +14,6 @@
package google.registry.rdap;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -25,6 +24,7 @@ import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.net.InetAddresses;
import com.google.common.primitives.Booleans;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
@@ -159,7 +159,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
.setIncompletenessWarningType(IncompletenessWarningType.COMPLETE);
Optional<Host> host =
loadByForeignKeyByCache(
ForeignKeyUtils.loadResourceByCache(
Host.class, partialStringQuery.getInitialString(), getRequestTime());
metricInformationBuilder.setNumHostsRetrieved(host.isPresent() ? 1 : 0);
@@ -176,7 +176,8 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
private NameserverSearchResponse searchByNameUsingSuperordinateDomain(
RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyByCache(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
ForeignKeyUtils.loadResourceByCache(
Domain.class, partialStringQuery.getSuffix(), getRequestTime());
if (domain.isEmpty()) {
// Don't allow wildcards with suffixes which are not domains we manage. That would risk a
// table scan in many easily foreseeable cases. The user might ask for ns*.zombo.com,
@@ -194,7 +195,8 @@ 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 = loadByForeignKeyByCache(Host.class, fqhn, getRequestTime());
Optional<Host> host =
ForeignKeyUtils.loadResourceByCache(Host.class, fqhn, getRequestTime());
if (shouldBeVisible(host)) {
hostList.add(host.get());
if (hostList.size() > rdapResultSetMaxSize) {
@@ -1,103 +0,0 @@
// Copyright 2021 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.rde;
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableMap;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.rde.RdeMode;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import org.joda.time.DateTime;
/** Loading cache that turns a resource into XML for the various points in time and modes. */
public class RdeFragmenter {
private final Map<WatermarkModePair, Optional<DepositFragment>> cache = new HashMap<>();
private final ImmutableMap<DateTime, Supplier<EppResource>> resourceAtTimes;
private final RdeMarshaller marshaller;
long cacheHits = 0;
long resourcesNotFound = 0;
long resourcesFound = 0;
public RdeFragmenter(
ImmutableMap<DateTime, Supplier<EppResource>> resourceAtTimes, RdeMarshaller marshaller) {
this.resourceAtTimes = resourceAtTimes;
this.marshaller = marshaller;
}
public Optional<DepositFragment> marshal(DateTime watermark, RdeMode mode) {
Optional<DepositFragment> result = cache.get(WatermarkModePair.create(watermark, mode));
if (result != null) {
cacheHits++;
return result;
}
EppResource resource = resourceAtTimes.get(watermark).get();
if (resource == null) {
result = Optional.empty();
cache.put(WatermarkModePair.create(watermark, RdeMode.FULL), result);
cache.put(WatermarkModePair.create(watermark, RdeMode.THIN), result);
resourcesNotFound++;
return result;
}
resourcesFound++;
if (resource instanceof Domain) {
result = Optional.of(marshaller.marshalDomain((Domain) resource, mode));
cache.put(WatermarkModePair.create(watermark, mode), result);
return result;
} else if (resource instanceof Contact) {
result = Optional.of(marshaller.marshalContact((Contact) resource));
cache.put(WatermarkModePair.create(watermark, RdeMode.FULL), result);
cache.put(WatermarkModePair.create(watermark, RdeMode.THIN), result);
return result;
} else if (resource instanceof Host host) {
result =
Optional.of(
host.isSubordinate()
? marshaller.marshalSubordinateHost(
host,
// Note that loadAtPointInTime() does cloneProjectedAtTime(watermark) for
// us.
Objects.requireNonNull(
loadAtPointInTime(
tm().loadByKey(host.getSuperordinateDomain()), watermark)))
: marshaller.marshalExternalHost(host));
cache.put(WatermarkModePair.create(watermark, RdeMode.FULL), result);
cache.put(WatermarkModePair.create(watermark, RdeMode.THIN), result);
return result;
} else {
throw new IllegalStateException(
String.format(
"Resource %s of type %s cannot be converted to XML.",
resource, resource.getClass().getSimpleName()));
}
}
/** Map key for {@link RdeFragmenter} cache. */
record WatermarkModePair(DateTime watermark, RdeMode mode) {
static WatermarkModePair create(DateTime watermark, RdeMode mode) {
return new WatermarkModePair(watermark, mode);
}
}
}
@@ -36,7 +36,7 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
static final String DNS_COUNTS = "dns_counts";
static final String MONTHLY_LOGS = "monthly_logs";
static final String EPP_METRICS = "epp_metrics";
static final String WHOIS_COUNTS = "whois_counts";
static final String RDAP_COUNTS = "rdap_counts";
static final String ACTIVITY_REPORT_AGGREGATION = "activity_report_aggregation";
private final String projectId;
@@ -98,13 +98,13 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
.build();
queriesBuilder.put(getTableName(EPP_METRICS, yearMonth), eppQuery);
String whoisQuery =
SqlTemplate.create(getQueryFromFile(WHOIS_COUNTS + ".sql"))
String rdapQuery =
SqlTemplate.create(getQueryFromFile(RDAP_COUNTS + ".sql"))
.put("PROJECT_ID", projectId)
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("MONTHLY_LOGS_TABLE", getTableName(MONTHLY_LOGS, yearMonth))
.build();
queriesBuilder.put(getTableName(WHOIS_COUNTS, yearMonth), whoisQuery);
queriesBuilder.put(getTableName(RDAP_COUNTS, yearMonth), rdapQuery);
SqlTemplate aggregateQuery =
SqlTemplate.create(getQueryFromFile(ACTIVITY_REPORT_AGGREGATION + ".sql"))
@@ -115,7 +115,7 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("DNS_COUNTS_TABLE", getTableName(DNS_COUNTS, yearMonth))
.put("EPP_METRICS_TABLE", getTableName(EPP_METRICS, yearMonth))
.put("WHOIS_COUNTS_TABLE", getTableName(WHOIS_COUNTS, yearMonth));
.put("RDAP_COUNTS_TABLE", getTableName(RDAP_COUNTS, yearMonth));
queriesBuilder.put(
getTableName(ACTIVITY_REPORT_AGGREGATION, yearMonth), aggregateQuery.build());
@@ -67,15 +67,13 @@ class RequestMetrics {
private static String truncatePath(String path) {
// We want to bucket RDAP requests by type to use less metric space,
// e.g. "/rdap/domains" rather than "/rdap/domains/foo.tld"
if (path.startsWith("/rdap")) {
List<String> splitPath = Splitter.on("/").omitEmptyStrings().splitToList(path);
return Streams.stream(Iterables.limit(splitPath, 2))
.collect(Collectors.joining("/", "/", "/"));
// Similarly, we put all web WHOIS requests under the same path because otherwise its
// cardinality is unbound, and it is possible to generate a huge amount of metrics with all
// the different paths.
} else if (path.startsWith("/whois")) {
// Though we don't serve WHOIS requests anymore, bucket any non-served WHOIS requests together
// to avoid an unbound cardinality
return "/whois";
}
return path;
@@ -23,7 +23,7 @@ public enum Auth {
/**
* Allows anyone to access.
*
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
* <p>This is used for public HTML endpoints like RDAP and the check API.
*/
AUTH_PUBLIC(AuthLevel.NONE, UserPolicy.PUBLIC),
@@ -41,7 +41,10 @@ class CommandUtilities {
}
public VKey<? extends EppResource> getKey(String uniqueId, DateTime now) {
return ForeignKeyUtils.load(clazz, uniqueId, now);
return ForeignKeyUtils.loadKey(clazz, uniqueId, now)
.orElseThrow(
() ->
new IllegalArgumentException(String.format("Invalid resource ID %s", uniqueId)));
}
}
@@ -47,6 +47,11 @@ final class CreateCdnsTld extends ConfirmingCommand {
)
String name;
@Parameter(
names = "--skip_sandbox_tld_check",
description = "In Sandbox, skip the dns_name format check.")
boolean skipSandboxTldCheck;
@Inject
@Config("projectId")
String projectId;
@@ -61,10 +66,15 @@ final class CreateCdnsTld extends ConfirmingCommand {
protected void init() {
// Sandbox talks to production Cloud DNS. As a result, we can't configure any domains with a
// suffix that might be used by customers on the same nameserver set. Limit the user to setting
// up *.test TLDs.
if (RegistryToolEnvironment.get() == RegistryToolEnvironment.SANDBOX
&& !dnsName.endsWith(".test.")) {
throw new IllegalArgumentException("Sandbox TLDs must be of the form \"*.test.\"");
// up *.test TLDs unless the user declares that the name is approved.
//
// The name format check simply provides a user-friendly error message. If the user wrongly
// declares name approval, the request to the Cloud DNS API will still fail.
if (RegistryToolEnvironment.get() == RegistryToolEnvironment.SANDBOX) {
if (!skipSandboxTldCheck && !dnsName.endsWith(".test.")) {
throw new IllegalArgumentException(
"Sandbox TLDs must be approved or in the form \"*.test.\"");
}
}
managedZone =
@@ -16,7 +16,6 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCacheIfEnabled;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
@@ -26,6 +25,7 @@ import com.google.common.collect.Sets;
import google.registry.batch.CloudTasksUtils;
import google.registry.batch.RelockDomainAction;
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.domain.Domain;
@@ -327,7 +327,7 @@ public final class DomainLockUtils {
private Domain getDomain(String domainName, String registrarId, DateTime now) {
Domain domain =
loadByForeignKeyByCacheIfEnabled(Domain.class, domainName, now)
ForeignKeyUtils.loadResource(Domain.class, domainName, now)
.orElseThrow(() -> new IllegalArgumentException("Domain doesn't exist"));
// The user must have specified either the correct registrar ID or the admin registrar ID
checkArgument(
@@ -15,7 +15,6 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.reporting.HistoryEntry.Type.SYNTHETIC;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
@@ -25,6 +24,7 @@ import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.ResourceFlowUtils;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.poll.PollMessage;
@@ -33,7 +33,6 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.util.DomainNameUtils;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Optional;
/**
* Tool to enqueue a poll message for a registrar.
@@ -85,11 +84,9 @@ class EnqueuePollMessageCommand extends MutatingCommand {
!sendToAll || isNullOrEmpty(clientIds), "Cannot specify both --all and --clients");
tm().transact(
() -> {
Optional<Domain> domainOpt =
loadByForeignKey(Domain.class, domainName, tm().getTransactionTime());
checkArgument(
domainOpt.isPresent(), "Domain %s doesn't exist or isn't active", domainName);
Domain domain = domainOpt.get();
Domain domain =
ResourceFlowUtils.loadAndVerifyExistence(
Domain.class, domainName, tm().getTransactionTime());
ImmutableList<String> registrarIds;
if (sendToAll) {
registrarIds =
@@ -15,6 +15,7 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
@@ -46,6 +47,9 @@ import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tlds;
import google.registry.persistence.VKey;
import google.registry.tools.params.MoneyParameter;
import google.registry.tools.params.TransitionListParameter.TokenStatusTransitions;
@@ -291,10 +295,12 @@ class GenerateAllocationTokensCommand implements Command {
!ImmutableList.of("").equals(allowedClientIds),
"Either omit --allowed_client_ids if all registrars are allowed, or include a"
+ " comma-separated list");
verifyAllRegistrarIdsExist(allowedClientIds);
checkArgument(
!ImmutableList.of("").equals(allowedTlds),
"Either omit --allowed_tlds if all TLDs are allowed, or include a comma-separated list");
verifyAllTldsExist(allowedTlds);
if (ImmutableList.of("").equals(allowedEppActions)) {
allowedEppActions = ImmutableList.of();
@@ -326,6 +332,34 @@ class GenerateAllocationTokensCommand implements Command {
}
}
static void verifyAllRegistrarIdsExist(@Nullable List<String> allowedClientIds) {
// a null/empty list means that all registrars are allowed
if (isNullOrEmpty(allowedClientIds)) {
return;
}
ImmutableSet<String> allRegistrarIds =
Registrar.loadAllKeysCached().stream()
.map(VKey::getKey)
.map(Object::toString)
.collect(toImmutableSet());
ImmutableList<String> badRegistrarIds =
allowedClientIds.stream()
.filter(id -> !allRegistrarIds.contains(id))
.collect(toImmutableList());
checkArgument(badRegistrarIds.isEmpty(), "Unknown registrar ID(s) %s", badRegistrarIds);
}
static void verifyAllTldsExist(@Nullable List<String> allowedTlds) {
// a null/empty list means that all TLDs are allowed
if (isNullOrEmpty(allowedTlds)) {
return;
}
ImmutableSet<String> allTlds = Tlds.getTldsOfType(Tld.TldType.REAL);
ImmutableList<String> badTlds =
allowedTlds.stream().filter(tld -> !allTlds.contains(tld)).collect(toImmutableList());
checkArgument(badTlds.isEmpty(), "Unknown REAL TLD(s) %s", badTlds);
}
private void verifyTokenStringsDoNotExist() {
ImmutableSet<String> existingTokenStrings =
getExistingTokenStrings(ImmutableSet.copyOf(tokenStrings));
@@ -14,10 +14,9 @@
package google.registry.tools;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.Contact;
import java.util.List;
@@ -34,7 +33,9 @@ final class GetContactCommand extends GetEppResourceCommand {
public void runAndPrint() {
for (String contactId : mainParameters) {
printResource(
"Contact", contactId, loadByForeignKey(Contact.class, contactId, readTimestamp));
"Contact",
contactId,
ForeignKeyUtils.loadResource(Contact.class, contactId, readTimestamp));
}
}
}
@@ -14,11 +14,11 @@
package google.registry.tools;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.util.DomainNameUtils;
@@ -57,7 +57,7 @@ final class GetDomainCommand extends GetEppResourceCommand {
printResource(
"Domain",
canonicalDomain,
loadByForeignKey(Domain.class, canonicalDomain, readTimestamp));
ForeignKeyUtils.loadResource(Domain.class, canonicalDomain, readTimestamp));
}
}
}

Some files were not shown because too many files have changed in this diff Show More