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

Compare commits

...

11 Commits

Author SHA1 Message Date
sarahcaseybot 342ae7a5de Add defaultPromoTokens to Registry (#1850)
* Add defaultPromoTokens to Registry

* Remove flyway files from this PR

* Fix merge conflicts

* Add back flyway file

* Add more info to error messages

* Change to a list

* Fix javadoc

* Change error message

* Add note to field declaration
2022-12-06 12:22:43 -05:00
gbrodman 9bf1bf47dd Take anchor tenant tokens into account in domain check flows (#1868)
These were always properly reflected in the actual creations, but when
running a check flow it would still show a non-zero cost even when using
an ANCHOR_TENANT allocation token. This changes it so that we accurately
show the $0.00 cost.
2022-12-05 16:14:53 -05:00
gbrodman 6dc1ca0279 Remove usage of the AppEngine remote API (#1858)
This is only used for contacting Datastore. With the removal of:
1. All standard usages of Datastore
2. Usage of Datastore for allocation of object IDs
3. Usage of Datastore for GAE user IDs

we can remove the remote API without affecting functionality.

This also allows us to just use SQL for every command since it's lazily
supplied. This simplifies the SQL setup and means that we remove a
possible situation where we forget the SQL setup.
2022-12-05 13:23:18 -05:00
Lai Jiang 1d7dfe4e07 Remove Ofy (#1863)
So long, farewell, adios, ciao, sayonara, 再见!

TESTED=deployed to alpha and used `nomulus list_tlds` to confirm that the web app can receive and serve requests.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1863)
<!-- Reviewable:end -->
2022-12-02 22:28:33 -05:00
Lai Jiang 601aed388c Fix javadoc build (#1866)
With newer versions of Java 11, javadoc fails to build due to unknown
tags in package-info.java files. These files are not important so we
exclude them.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1866)
<!-- Reviewable:end -->
2022-12-02 13:37:56 -05:00
Weimin Yu 46a7956f77 Fix nomulus GetEppResourceCommand (#1865)
* Fix nomulus GetEppResourceCommand

Fixes a bug in read_timestamp validation.

Fixes string representation of Collection fields in Epp Resources.
2022-12-01 18:35:15 -05:00
Lai Jiang 63d3453848 Re-add parenthesis (#1862)
Apparently IntelliJ doesn't like the extra parens, but our own
ErrorProne checks want it for clarity.
2022-11-30 10:45:12 -05:00
Lai Jiang 85272a30a2 Use login email instead of GAE user ID for RegistrarPoc (#1852)
Switch to using the login email address instead of GAE user ID to
identify console users. The primary use cases are:

1) When the user logged in the registrar console, need to figure out
   which registrars they have access to (in
   AuthenticatedReigstrarAccess).

2) When a user tries to apply a registry lock, needs to know if they
   can (in RegistryLockGetAction).

Both cases are tested in alpha with a personal email address to ensure
it does not get the permission due to being a GAE admin account.

Also verified that the soy templates includes the hidden login email
form field instead of GAE user ID when registrars are displayed on the
console; and consequently when a contact update is posted to the server,
the login email is part of the JSON payload. Even though it does not
look like it is used in any way by RegistrarSettingsAction, which
receives the POST request. Like GAE user ID, the field is hidden, so
cannot be changed by the user from the console, it is also not used to
identify the RegistryPoc entity, whose composite keys are the contact
email and the registrar ID associated with it.

The login email address is backfilled for all RegistrarPocs that have a
non-null GAE user ID. The backfilled addresses converted to the same ID
as stored in the database.
2022-11-29 17:16:19 -05:00
gbrodman e3944d5d52 Rename AppEngineConnection to ServiceConnection (#1857)
It doesn't actually use any App Engine libraries or code -- it's just a
generic connection with authentication to a service. This also involves
changing that block of config to be "gcpProject" instead of "appEngine"
since it's more generic.

Note: this will require an internal PR as well to change the
corresponding private config block
2022-11-28 15:46:51 -05:00
sarahcaseybot 124a3d83ba Remove package token on manual transfer approval (#1819)
* Remove package token on manual transfer approval

* remove extra variables

* Add back white space

* Don't overwrite existingDomain

* Format fixes, use available helper variables

* Use PACKAGE allocation tokens in tests

* Refactor

* Fix merge conflicts

* Dont overwrite existingRecurring
2022-11-28 15:30:55 -05:00
Pavlo Tkach 99cbb862dc remove jpaTransactionManagerType rde pipeline param (#1860) 2022-11-28 12:13:45 -05:00
169 changed files with 908 additions and 3165 deletions
-1
View File
@@ -6,5 +6,4 @@ node_modules/
repos/**
**/.idea/
*.jar
!third_party/**/*.jar
!/gradle/wrapper/**/*.jar
-1
View File
@@ -14,7 +14,6 @@ gjf.out
*.jar
*.war
*.ear
!/third_party/**/*.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
+4 -2
View File
@@ -355,8 +355,6 @@ subprojects {
}
}
if (project.name == 'third_party') return
project.tasks.test.dependsOn runPresubmits
def commonlyExcludedResources = ['**/*.java', '**/BUILD']
@@ -528,6 +526,10 @@ task javaIncrementalFormatApply {
task javadoc(type: Javadoc) {
source javadocSource
// Java 11.0.17 has the following bug that affects annotation handling on
// package-info.java:
// https://bugs.openjdk.org/browse/JDK-8222091
exclude "**/package-info.java"
classpath = files(javadocClasspath)
destinationDir = file("${buildDir}/docs/javadoc")
options.encoding = "UTF-8"
+1 -7
View File
@@ -43,12 +43,6 @@ by Joshua Bloch in his book Effective Java -->
<property name="message" value='Your Javadocs appear to use invalid &lt;a&gt; tag syntax in @see tags. Please use the correct syntax: @see &lt;a href="http(s)://your_url"&gt;url_description&lt;/a&gt;'/>
</module>
<!-- Checks that our Ofy wrapper is used instead of the "real" ofy(). -->
<module name="RegexpSingleline">
<property name="format" value="com\.googlecode\.objectify\.ObjectifyService\.ofy"/>
<property name="message" value="Use google.registry.model.ofy.ObjectifyService.ofy(). Do not use com.googlecode.objectify.v4.ObjectifyService.ofy()."/>
</module>
<!-- Checks that java.util.Optional is used instead of Guava's Optional. -->
<module name="RegexpSingleline">
<property name="format" value="com\.google\.common\.base\.Optional"/>
@@ -58,7 +52,7 @@ by Joshua Bloch in his book Effective Java -->
<!-- Checks that the deprecated ExpectedException is not used. -->
<module name="RegexpSingleline">
<property name="format" value="org\.junit\.rules\.ExpectedException"/>
<property name="message" value="Use assertThrows and expectThrows from JUnitBackports instead of the deprecated methods on ExpectedException."/>
<property name="message" value="Use assertThrows and expectThrows instead of the deprecated methods on ExpectedException."/>
</module>
<module name="LineLength">
-5
View File
@@ -163,11 +163,6 @@ configurations {
dependencies {
def deps = rootProject.dependencyMap
// Custom-built objectify jar at commit ecd5165, included in Nomulus
// release.
implementation files(
"${rootDir}/third_party/objectify/v4_1/objectify-4.1.3.jar")
testRuntimeOnly files(sourceSets.test.resources.srcDirs)
implementation deps['com.beust:jcommander']
@@ -21,7 +21,6 @@ import com.google.common.flogger.FluentLogger;
import dagger.Lazy;
import google.registry.config.RegistryEnvironment;
import google.registry.config.SystemPropertySetter;
import google.registry.model.AppEngineEnvironment;
import google.registry.model.IdService;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
@@ -63,10 +62,6 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
transactionManagerLazy = registryPipelineComponent.getJpaTransactionManager();
}
TransactionManagerFactory.setJpaTmOnBeamWorker(transactionManagerLazy::get);
// Masquerade all threads as App Engine threads, so we can create Ofy keys in the pipeline. Also
// loads all ofy entities.
new AppEngineEnvironment("s~" + registryPipelineComponent.getProjectId())
.setEnvironmentForAllThreads();
SystemPropertySetter.PRODUCTION_IMPL.setProperty(PROPERTY, "true");
// Use self-allocated IDs if requested. Note that this inevitably results in duplicate IDs from
// multiple workers, which can also collide with existing IDs in the database. So they cannot be
@@ -103,13 +103,13 @@ public final class RegistryConfig {
@Provides
@Config("projectId")
public static String provideProjectId(RegistryConfigSettings config) {
return config.appEngine.projectId;
return config.gcpProject.projectId;
}
@Provides
@Config("locationId")
public static String provideLocationId(RegistryConfigSettings config) {
return config.appEngine.locationId;
return config.gcpProject.locationId;
}
/**
@@ -1325,7 +1325,7 @@ public final class RegistryConfig {
/** Returns the App Engine project ID, which is based off the environment name. */
public static String getProjectId() {
return CONFIG_SETTINGS.get().appEngine.projectId;
return CONFIG_SETTINGS.get().gcpProject.projectId;
}
/**
@@ -1338,7 +1338,7 @@ public final class RegistryConfig {
}
public static boolean areServersLocal() {
return CONFIG_SETTINGS.get().appEngine.isLocal;
return CONFIG_SETTINGS.get().gcpProject.isLocal;
}
/**
@@ -1347,7 +1347,7 @@ public final class RegistryConfig {
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getDefaultServer() {
return makeUrl(CONFIG_SETTINGS.get().appEngine.defaultServiceUrl);
return makeUrl(CONFIG_SETTINGS.get().gcpProject.defaultServiceUrl);
}
/**
@@ -1356,7 +1356,7 @@ public final class RegistryConfig {
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getBackendServer() {
return makeUrl(CONFIG_SETTINGS.get().appEngine.backendServiceUrl);
return makeUrl(CONFIG_SETTINGS.get().gcpProject.backendServiceUrl);
}
/**
@@ -1365,7 +1365,7 @@ public final class RegistryConfig {
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getToolsServer() {
return makeUrl(CONFIG_SETTINGS.get().appEngine.toolsServiceUrl);
return makeUrl(CONFIG_SETTINGS.get().gcpProject.toolsServiceUrl);
}
/**
@@ -1374,7 +1374,7 @@ public final class RegistryConfig {
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getPubapiServer() {
return makeUrl(CONFIG_SETTINGS.get().appEngine.pubapiServiceUrl);
return makeUrl(CONFIG_SETTINGS.get().gcpProject.pubapiServiceUrl);
}
/** Returns the amount of time a singleton should be cached, before expiring. */
@@ -21,7 +21,7 @@ import java.util.Set;
/** The POJO that YAML config files are deserialized into. */
public class RegistryConfigSettings {
public AppEngine appEngine;
public GcpProject gcpProject;
public GSuite gSuite;
public OAuth oAuth;
public CredentialOAuth credentialOAuth;
@@ -45,8 +45,8 @@ public class RegistryConfigSettings {
public DnsUpdate dnsUpdate;
public PackageMonitoring packageMonitoring;
/** Configuration options that apply to the entire App Engine project. */
public static class AppEngine {
/** Configuration options that apply to the entire GCP project. */
public static class GcpProject {
public String projectId;
public String locationId;
public boolean isLocal;
@@ -5,12 +5,12 @@
# to override some of these values to configure and enable some services used in
# production environments.
appEngine:
# Globally unique App Engine project ID
gcpProject:
# Globally unique GCP project ID
projectId: registry-project-id
# Location of the App engine project, note that us-central1 and europe-west1 are special in that
# they are used without the trailing number in App Engine commands and Google Cloud Console.
# See: https://cloud.google.com/appengine/docs/locations
# Location of the GCP project, note that us-central1 and europe-west1 are special in that
# they are used without the trailing number in GCP commands and Google Cloud Console.
# See: https://cloud.google.com/appengine/docs/locations as an example
locationId: registry-location-id
# whether to use local/test credentials when connecting to the servers
@@ -2,7 +2,7 @@
# This is the same as what Google Registry runs in production, except with
# placeholders for Google-specific settings.
appEngine:
gcpProject:
projectId: placeholder
# Set to true if running against local servers (localhost)
isLocal: false
@@ -341,24 +341,4 @@ have been in the database for a certain period of time. -->
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
@@ -139,24 +139,4 @@
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
@@ -105,24 +105,4 @@
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
@@ -135,24 +135,4 @@
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
@@ -682,7 +682,13 @@ public class DomainFlowUtils {
builder.setAvailIfSupported(true);
fees =
pricingLogic
.getCreatePrice(registry, domainNameString, now, years, false, allocationToken)
.getCreatePrice(
registry,
domainNameString,
now,
years,
isAnchorTenant(domainName, allocationToken, Optional.empty()),
allocationToken)
.getFees();
}
break;
@@ -49,6 +49,7 @@ import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
@@ -67,6 +68,7 @@ import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferStatus;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
/**
@@ -147,6 +149,8 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
HistoryEntryId domainHistoryId = createHistoryEntryId(existingDomain);
historyBuilder.setRevisionId(domainHistoryId.getRevisionId());
boolean hasPackageToken = existingDomain.getCurrentPackageToken().isPresent();
Money renewalPrice = hasPackageToken ? null : existingRecurring.getRenewalPrice().orElse(null);
Optional<BillingEvent.OneTime> billingEvent =
transferData.getTransferPeriod().getValue() == 0
? Optional.empty()
@@ -162,12 +166,16 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
Registry.get(tld),
targetId,
transferData.getTransferRequestTime(),
existingRecurring)
// When removing a domain from a package it should return to the
// default recurring billing behavior so the existing recurring
// billing event should not be passed in.
hasPackageToken ? null : existingRecurring)
.getRenewCost())
.setEventTime(now)
.setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength()))
.setDomainHistoryId(domainHistoryId)
.build());
ImmutableList.Builder<ImmutableObject> entitiesToSave = 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.
@@ -198,8 +206,11 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.setTargetId(targetId)
.setRegistrarId(gainingRegistrarId)
.setEventTime(newExpirationTime)
.setRenewalPriceBehavior(existingRecurring.getRenewalPriceBehavior())
.setRenewalPrice(existingRecurring.getRenewalPrice().orElse(null))
.setRenewalPriceBehavior(
hasPackageToken
? RenewalPriceBehavior.DEFAULT
: existingRecurring.getRenewalPriceBehavior())
.setRenewalPrice(renewalPrice)
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistoryId(domainHistoryId)
.build();
@@ -243,7 +254,11 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.orElseGet(ImmutableSet::of))
.setLastEppUpdateTime(now)
.setLastEppUpdateRegistrarId(registrarId)
// Even if the existing domain had a package token, that package token should be removed
// on transfer
.setCurrentPackageToken(null)
.build();
Registry registry = Registry.get(existingDomain.getTld());
DomainHistory domainHistory = buildDomainHistory(newDomain, registry, now, gainingRegistrarId);
// Create a poll message for the gaining client.
@@ -1,124 +0,0 @@
// Copyright 2020 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 com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.common.collect.ImmutableMap;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.ofy.ObjectifyService;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Sets up a fake {@link Environment} so that the following operations can be performed without the
* Datastore service:
*
* <ul>
* <li>Create Objectify {@code Keys}.
* <li>Instantiate Objectify objects.
* <li>Convert Datastore {@code Entities} to their corresponding Objectify objects.
* </ul>
*
* <p>User has the option to specify their desired {@code appId} string, which forms part of an
* Objectify {@code Key} and is included in the equality check. This feature makes it easy to
* compare a migrated object in SQL with the original in Objectify.
*
* <p>Note that conversion from Objectify objects to Datastore {@code Entities} still requires the
* Datastore service.
*/
@DeleteAfterMigration
public class AppEngineEnvironment {
private Environment environment;
/**
* Constructor for use by tests.
*
* <p>All test suites must use the same appId for environments, since when tearing down we do not
* clear cached environments in spawned threads. See {@link #unsetEnvironmentForAllThreads} for
* more information.
*/
public AppEngineEnvironment() {
/**
* Use AppEngineExtension's appId here so that ofy and sql entities can be compared with {@code
* Objects#equals()}. The choice of this value does not impact functional correctness.
*/
this("test");
}
/** Constructor for use by applications, e.g., BEAM pipelines. */
public AppEngineEnvironment(String appId) {
environment = createAppEngineEnvironment(appId);
}
public void setEnvironmentForCurrentThread() {
ApiProxy.setEnvironmentForCurrentThread(environment);
ObjectifyService.initOfy();
}
public void setEnvironmentForAllThreads() {
setEnvironmentForCurrentThread();
ApiProxy.setEnvironmentFactory(() -> environment);
}
public void unsetEnvironmentForCurrentThread() {
ApiProxy.clearEnvironmentForCurrentThread();
}
/**
* Unsets the test environment in all threads with best effort.
*
* <p>This method unsets the environment factory and clears the cached environment in the current
* thread (the main test runner thread). We do not clear the cache in spawned threads, even though
* they may be reused. This is not a problem as long as the appId stays the same: those threads
* are used only in AppEngine or BEAM tests, and expect the presence of an environment.
*/
public void unsetEnvironmentForAllThreads() {
unsetEnvironmentForCurrentThread();
try {
Method method = ApiProxy.class.getDeclaredMethod("clearEnvironmentFactory");
method.setAccessible(true);
method.invoke(null);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/** Returns a placeholder {@link Environment} that can return hardcoded AppId and Attributes. */
private static Environment createAppEngineEnvironment(String appId) {
return (Environment)
Proxy.newProxyInstance(
Environment.class.getClassLoader(),
new Class[] {Environment.class},
(Object proxy, Method method, Object[] args) -> {
switch (method.getName()) {
case "getAppId":
return appId;
case "getAttributes":
return ImmutableMap.<String, Object>of();
default:
throw new UnsupportedOperationException(method.getName());
}
});
}
/** Returns true if the current thread is in an App Engine Environment. */
public static boolean isInAppEngineEnvironment() {
return ApiProxy.getCurrentEnvironment() != null;
}
}
@@ -18,13 +18,9 @@ import static com.google.common.base.Suppliers.memoizeWithExpiration;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Supplier;
import google.registry.model.annotations.DeleteAfterMigration;
import java.time.Duration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Utility methods related to caching Datastore entities. */
public class CacheUtils {
@@ -77,29 +73,4 @@ public class CacheUtils {
}
return caffeine;
}
/**
* A {@link CacheLoader} that automatically masquerade the background thread where the refresh
* action runs in to be an GAE thread.
*/
@DeleteAfterMigration
public abstract static class AppEngineEnvironmentCacheLoader<K, V> implements CacheLoader<K, V> {
private static final AppEngineEnvironment environment = new AppEngineEnvironment();
@Override
public @Nullable V reload(@NonNull K key, @NonNull V oldValue) throws Exception {
V value;
boolean isMasqueraded = false;
if (!AppEngineEnvironment.isInAppEngineEnvironment()) {
environment.setEnvironmentForCurrentThread();
isMasqueraded = true;
}
value = load(key);
if (isMasqueraded) {
environment.unsetEnvironmentForCurrentThread();
}
return value;
}
}
}
@@ -32,7 +32,6 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.annotations.IdAllocation;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.transfer.TransferData;
@@ -354,7 +353,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
}
static final CacheLoader<VKey<? extends EppResource>, EppResource> CACHE_LOADER =
new AppEngineEnvironmentCacheLoader<VKey<? extends EppResource>, EppResource>() {
new CacheLoader<VKey<? extends EppResource>, EppResource>() {
@Override
public EppResource load(VKey<? extends EppResource> key) {
@@ -22,7 +22,6 @@ import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.common.flogger.FluentLogger;
import google.registry.beam.common.RegistryPipelineWorkerInitializer;
import google.registry.config.RegistryEnvironment;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import java.math.BigInteger;
@@ -33,7 +32,6 @@ import org.joda.time.DateTime;
/**
* Allocates a {@link long} to use as a {@code @Id}, (part) of the primary SQL key for an entity.
*/
@DeleteAfterMigration
public final class IdService {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -14,6 +14,7 @@
package google.registry.model;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Maps.transformValues;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -178,7 +179,7 @@ public abstract class ImmutableObject implements Cloneable {
return transformValues((Map<?, ?>) value, ImmutableObject::hydrate);
}
if (value instanceof Collection) {
return ((Collection<?>) value).stream().map(ImmutableObject::hydrate);
return transform((Collection<?>) value, ImmutableObject::hydrate);
}
if (value instanceof ImmutableObject) {
return ((ImmutableObject) value).toHydratedString();
@@ -15,7 +15,6 @@
package google.registry.model;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.tld.Registry.TldState.GENERAL_AVAILABILITY;
@@ -30,7 +29,6 @@ import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import google.registry.config.RegistryEnvironment;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
@@ -77,14 +75,14 @@ public final class OteAccountBuilder {
* Validation regex for registrar base client IDs (3-14 lowercase alphanumeric characters).
*
* <p>The base client ID is appended with numbers to create four different test registrar accounts
* (e.g. reg-1, reg-3, reg-4, reg-5). Registrar client IDs are of type clIDType in eppcom.xsd
* (e.g. reg-1, reg-3, reg-4, reg-5). Registrar client IDs are of type clIDType in eppcom.xsd
* which is limited to 16 characters, hence the limit of 14 here to account for the dash and
* numbers.
*
* <p>The base client ID is also used to generate the OT&E TLDs, hence the restriction to
* lowercase alphanumeric characters.
*/
private static final Pattern REGISTRAR_PATTERN = Pattern.compile("^[a-z0-9]{3,14}$");
private static final Pattern REGISTRAR_PATTERN = Pattern.compile("^[a-z\\d]{3,14}$");
// Durations are short so that registrars can test with quick transfer (etc.) turnaround.
private static final Duration SHORT_ADD_GRACE_PERIOD = Duration.standardMinutes(60);
@@ -179,17 +177,11 @@ public final class OteAccountBuilder {
* <p>NOTE: can be called more than once, adding multiple contacts. Each contact will have access
* to all OT&amp;E Registrars.
*
* @param email the contact email that will have web-console access to all the Registrars. Must be
* from "our G Suite domain" (we have to be able to get its GaeUserId)
* @param email the contact/login email that will have web-console access to all the Registrars.
* Must be from "our G Suite domain".
*/
public OteAccountBuilder addContact(String email) {
String gaeUserId =
checkNotNull(
GaeUserIdConverter.convertEmailAddressToGaeUserId(email),
"Email address %s is not associated with any GAE ID",
email);
registrars.forEach(
registrar -> contactsBuilder.add(createRegistrarContact(email, gaeUserId, registrar)));
registrars.forEach(registrar -> contactsBuilder.add(createRegistrarContact(email, registrar)));
return this;
}
@@ -304,7 +296,7 @@ public final class OteAccountBuilder {
TldState initialTldState,
boolean isEarlyAccess,
int roidSuffix) {
String tldNameAlphaNumerical = tldName.replaceAll("[^a-z0-9]", "");
String tldNameAlphaNumerical = tldName.replaceAll("[^a-z\\d]", "");
Optional<PremiumList> premiumList = PremiumListDao.getLatestRevision(DEFAULT_PREMIUM_LIST);
checkState(premiumList.isPresent(), "Couldn't find premium list %s.", DEFAULT_PREMIUM_LIST);
Registry.Builder builder =
@@ -348,13 +340,12 @@ public final class OteAccountBuilder {
.build();
}
private static RegistrarPoc createRegistrarContact(
String email, String gaeUserId, Registrar registrar) {
private static RegistrarPoc createRegistrarContact(String email, Registrar registrar) {
return new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName(email)
.setEmailAddress(email)
.setGaeUserId(gaeUserId)
.setLoginEmailAddress(email)
.build();
}
@@ -1,47 +0,0 @@
// Copyright 2017 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.annotations;
import com.googlecode.objectify.annotation.Entity;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for an Objectify {@link Entity} to indicate that it should not be backed up by the
* default Datastore backup configuration (it may be backed up by something else).
*/
@DeleteAfterMigration
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface NotBackedUp {
Reason reason();
/** Reasons why a given entity does not need to be be backed up. */
enum Reason {
/** This entity is transient by design and has only a short-term useful lifetime. */
TRANSIENT,
/** This entity's data is already regularly pulled down from an external source. */
EXTERNALLY_SOURCED,
/** This entity is generated automatically by the app and will be recreated if need be. */
AUTO_GENERATED,
/** Commit log entities are exported separately from the regular backups, by design. */
COMMIT_LOGS
}
}
@@ -1,32 +0,0 @@
// Copyright 2017 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.annotations;
import com.googlecode.objectify.annotation.Entity;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for an Objectify {@link Entity} to indicate that it is a "virtual entity".
*
* <p>A virtual entity type exists only to define part of the parentage key hierarchy for its child
* entities, and is never actually persisted and thus has no fields besides its ID field.
*/
@DeleteAfterMigration
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface VirtualEntity {}
@@ -1,73 +0,0 @@
// Copyright 2017 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.common;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.IdService.allocateId;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import com.google.appengine.api.users.User;
import com.google.common.base.Splitter;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import java.util.List;
/**
* A helper class to convert email addresses to GAE user ids. It does so by persisting a User
* object with the email address to Datastore, and then immediately reading it back.
*/
@Entity
@NotBackedUp(reason = Reason.TRANSIENT)
public class GaeUserIdConverter extends ImmutableObject {
@Id
public long id;
User user;
/**
* Converts an email address to a GAE user id.
*
* @return Numeric GAE user id (in String form), or null if email address has no GAE id
*/
public static String convertEmailAddressToGaeUserId(String emailAddress) {
final GaeUserIdConverter gaeUserIdConverter = new GaeUserIdConverter();
gaeUserIdConverter.id = allocateId();
List<String> emailParts = Splitter.on('@').splitToList(emailAddress);
checkState(emailParts.size() == 2, "'%s' is not a valid email address", emailAddress);
gaeUserIdConverter.user = new User(emailAddress, emailParts.get(1));
try {
// Perform these operations in a transactionless context to avoid enlisting in some outer
// transaction (if any).
auditedOfy()
.doTransactionless(
() -> {
auditedOfy().saveWithoutBackup().entity(gaeUserIdConverter).now();
return null;
});
// The read must be done in its own transaction to avoid reading from the session cache.
return auditedOfy()
.transactNew(() -> auditedOfy().load().entity(gaeUserIdConverter).now().user.getUserId());
} finally {
auditedOfy()
.doTransactionless(
() -> auditedOfy().deleteWithoutBackup().entity(gaeUserIdConverter).now());
}
}
}
@@ -1,90 +0,0 @@
// Copyright 2017 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.ofy;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.googlecode.objectify.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.cmd.DeleteType;
import com.googlecode.objectify.cmd.Deleter;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* A Deleter that forwards to {@code auditedOfy().delete()}, but can be augmented via subclassing to
* do custom processing on the keys to be deleted prior to their deletion.
*/
@DeleteAfterMigration
abstract class AugmentedDeleter implements Deleter {
private final Deleter delegate = ofy().delete();
/** Extension method to allow this Deleter to do extra work prior to the actual delete. */
protected abstract void handleDeletion(Iterable<Key<?>> keys);
private void handleDeletionStream(Stream<?> entityStream) {
handleDeletion(entityStream.map(Key::create).collect(toImmutableList()));
}
@Override
public Result<Void> entities(Iterable<?> entities) {
handleDeletionStream(Streams.stream(entities));
return delegate.entities(entities);
}
@Override
public Result<Void> entities(Object... entities) {
handleDeletionStream(Arrays.stream(entities));
return delegate.entities(entities);
}
@Override
public Result<Void> entity(Object entity) {
handleDeletionStream(Stream.of(entity));
return delegate.entity(entity);
}
@Override
public Result<Void> key(Key<?> key) {
handleDeletion(ImmutableList.of(key));
return delegate.keys(key);
}
@Override
public Result<Void> keys(Iterable<? extends Key<?>> keys) {
// Magic to convert the type Iterable<? extends Key<?>> (a family of types which allows for
// homogeneous iterables of a fixed Key<T> type, e.g. List<Key<Lock>>, and is convenient for
// callers) into the type Iterable<Key<?>> (a concrete type of heterogeneous keys, which is
// convenient for users).
handleDeletion(ImmutableList.copyOf(keys));
return delegate.keys(keys);
}
@Override
public Result<Void> keys(Key<?>... keys) {
handleDeletion(Arrays.asList(keys));
return delegate.keys(keys);
}
/** Augmenting this gets ugly; you can always just use keys(Key.create(...)) instead. */
@Override
public DeleteType type(Class<?> clazz) {
throw new UnsupportedOperationException();
}
}
@@ -1,63 +0,0 @@
// Copyright 2017 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.ofy;
import static com.googlecode.objectify.ObjectifyService.ofy;
import com.google.appengine.api.datastore.Entity;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.cmd.Saver;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.Arrays;
import java.util.Map;
/**
* A Saver that forwards to {@code ofy().save()}, but can be augmented via subclassing to do custom
* processing on the entities to be saved prior to their saving.
*/
@DeleteAfterMigration
abstract class AugmentedSaver implements Saver {
private final Saver delegate = ofy().save();
/** Extension method to allow this Saver to do extra work prior to the actual save. */
protected abstract void handleSave(Iterable<?> entities);
@Override
public <E> Result<Map<Key<E>, E>> entities(Iterable<E> entities) {
handleSave(entities);
return delegate.entities(entities);
}
@Override
@SafeVarargs
public final <E> Result<Map<Key<E>, E>> entities(E... entities) {
handleSave(Arrays.asList(entities));
return delegate.entities(entities);
}
@Override
public <E> Result<Key<E>> entity(E entity) {
handleSave(ImmutableList.of(entity));
return delegate.entity(entity);
}
@Override
public Entity toEntity(Object pojo) {
// No call to the extension method, since toEntity() doesn't do any actual saving.
return delegate.toEntity(pojo);
}
}
@@ -1,84 +0,0 @@
// Copyright 2017 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.ofy;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableSet;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.util.Clock;
import java.util.function.Supplier;
/** Wrapper for {@link Supplier} that associates a time with each attempt. */
@DeleteAfterMigration
public class CommitLoggedWork<R> implements Runnable {
private final Supplier<R> work;
private final Clock clock;
/**
* Temporary place to store the result of a non-void work.
*
* <p>We don't want to return the result directly because we are going to try to recover from a
* {@link com.google.appengine.api.datastore.DatastoreTimeoutException} deep inside Objectify when
* it tries to commit the transaction. When an exception is thrown the return value would be lost,
* but sometimes we will be able to determine that we actually succeeded despite the timeout, and
* we'll want to get the result.
*/
private R result;
/**
* Temporary place to store the mutations belonging to the commit log manifest.
*
* <p>These are used along with the manifest to determine whether a transaction succeeded.
*/
protected ImmutableSet<ImmutableObject> mutations = ImmutableSet.of();
/** Lifecycle marker to track whether {@link #run} has been called. */
private boolean runCalled;
CommitLoggedWork(Supplier<R> work, Clock clock) {
this.work = work;
this.clock = clock;
}
protected TransactionInfo createNewTransactionInfo() {
return new TransactionInfo(clock.nowUtc());
}
boolean hasRun() {
return runCalled;
}
R getResult() {
checkState(runCalled, "Cannot call getResult() before run()");
return result;
}
@Override
public void run() {
// The previous time will generally be null, except when using transactNew.
TransactionInfo previous = Ofy.TRANSACTION_INFO.get();
// Set the time to be used for "now" within the transaction.
try {
Ofy.TRANSACTION_INFO.set(createNewTransactionInfo());
result = work.get();
} finally {
Ofy.TRANSACTION_INFO.set(previous);
}
runCalled = true;
}
}
@@ -1,140 +0,0 @@
// Copyright 2017 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.ofy;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.googlecode.objectify.ObjectifyService.factory;
import static google.registry.util.TypeUtils.hasAnnotation;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceConfig;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.EntitySubclass;
import google.registry.config.RegistryEnvironment;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.GaeUserIdConverter;
/**
* An instance of Ofy, obtained via {@code #auditedOfy()}, should be used to access all persistable
* objects. The class contains a static initializer to call factory().register(...) on all
* persistable objects in this package.
*/
@DeleteAfterMigration
public class ObjectifyService {
/** A singleton instance of our Ofy wrapper. */
private static final Ofy OFY = new Ofy(null);
/**
* Returns the singleton {@link Ofy} instance, signifying that the caller has been audited for the
* Registry 3.0 conversion.
*/
public static Ofy auditedOfy() {
return OFY;
}
static {
initOfyOnce();
}
/** Ensures that Objectify has been fully initialized. */
public static void initOfy() {
// This method doesn't actually do anything; it's here so that callers have something to call
// to ensure that the static initialization of ObjectifyService has been performed (which Java
// guarantees will happen exactly once, before any static methods are invoked).
//
// See JLS section 12.4: http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4
}
/**
* Performs static initialization for Objectify to register types and do other setup.
*
* <p>This method is non-idempotent, so it should only be called exactly once, which is achieved
* by calling it from this class's static initializer block.
*/
private static void initOfyOnce() {
// Set an ObjectifyFactory that uses our extended ObjectifyImpl.
// The "false" argument means that we are not using the v5-style Objectify embedded entities.
com.googlecode.objectify.ObjectifyService.setFactory(
new ObjectifyFactory(false) {
@Override
protected AsyncDatastoreService createRawAsyncDatastoreService(
DatastoreServiceConfig cfg) {
// In the unit test environment, wrap the Datastore service in a proxy that can be used
// to examine the number of requests sent to Datastore.
AsyncDatastoreService service = super.createRawAsyncDatastoreService(cfg);
return RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
? new RequestCapturingAsyncDatastoreService(service)
: service;
}
});
registerEntityClasses(ImmutableSet.of(GaeUserIdConverter.class));
}
/** Register classes that can be persisted via Objectify as Datastore entities. */
private static void registerEntityClasses(
ImmutableSet<Class<? extends ImmutableObject>> entityClasses) {
// Register all the @Entity classes before any @EntitySubclass classes so that we can check
// that every @Entity registration is a new kind and every @EntitySubclass registration is not.
// This is future-proofing for Objectify 5.x where the registration logic gets less lenient.
for (Class<?> clazz :
Streams.concat(
entityClasses.stream().filter(hasAnnotation(Entity.class)),
entityClasses.stream().filter(hasAnnotation(Entity.class).negate()))
.collect(toImmutableSet())) {
String kind = Key.getKind(clazz);
boolean registered = factory().getMetadata(kind) != null;
if (clazz.isAnnotationPresent(Entity.class)) {
// Objectify silently replaces current registration for a given kind string when a different
// class is registered again for this kind. For simplicity's sake, throw an exception on any
// re-registration.
checkState(
!registered,
"Kind '%s' already registered, cannot register new @Entity %s",
kind,
clazz.getCanonicalName());
} else if (clazz.isAnnotationPresent(EntitySubclass.class)) {
// Ensure that any @EntitySubclass classes have also had their parent @Entity registered,
// which Objectify nominally requires but doesn't enforce in 4.x (though it may in 5.x).
checkState(
registered,
"No base entity for kind '%s' registered yet, cannot register new @EntitySubclass %s",
kind,
clazz.getCanonicalName());
}
com.googlecode.objectify.ObjectifyService.register(clazz);
// Autogenerated ids make the commit log code very difficult since we won't always be able
// to create a key for an entity immediately when requesting a save. So, we require such
// entities to implement google.registry.model.Buildable as its build() function allocates the
// id to the entity.
if (factory().getMetadata(clazz).getKeyMetadata().isIdGeneratable()) {
checkState(
Buildable.class.isAssignableFrom(clazz),
"Can't register %s: Entity with autogenerated ids (@Id on a Long) must implement"
+ " google.registry.model.Buildable.",
kind);
}
}
}
}
@@ -1,374 +0,0 @@
// Copyright 2017 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.ofy;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.uniqueIndex;
import static com.googlecode.objectify.ObjectifyService.ofy;
import static google.registry.config.RegistryConfig.getBaseOfyRetryDuration;
import com.google.appengine.api.datastore.DatastoreFailureException;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.taskqueue.TransientFailureException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.cmd.Deleter;
import com.googlecode.objectify.cmd.Loader;
import com.googlecode.objectify.cmd.Saver;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.VirtualEntity;
import google.registry.model.ofy.ReadOnlyWork.KillTransactionException;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import google.registry.util.Sleeper;
import google.registry.util.SystemClock;
import google.registry.util.SystemSleeper;
import java.lang.annotation.Annotation;
import java.util.Objects;
import java.util.function.Supplier;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* A wrapper around ofy().
*
* <p>The primary purpose of this class is to add functionality to support commit logs. It is
* simpler to wrap {@link Objectify} rather than extend it because this way we can remove some
* methods that we don't really want exposed and add some shortcuts.
*/
@DeleteAfterMigration
public class Ofy {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Default clock for transactions that don't provide one. */
@NonFinalForTesting
static Clock clock = new SystemClock();
/** Default sleeper for transactions that don't provide one. */
@NonFinalForTesting
static Sleeper sleeper = new SystemSleeper();
/**
* An injected clock that overrides the static clock.
*
* <p>Eventually the static clock should go away when we are 100% injected, but for now we need to
* preserve the old way of overriding the clock in tests by changing the static field.
*/
private final Clock injectedClock;
/** Retry for 8^2 * 100ms = ~25 seconds. */
private static final int NUM_RETRIES = 8;
@Inject
public Ofy(Clock injectedClock) {
this.injectedClock = injectedClock;
}
/**
* Thread local transaction info. There can only be one active transaction on a thread at a given
* time, and this will hold metadata for it.
*/
static final ThreadLocal<TransactionInfo> TRANSACTION_INFO = new ThreadLocal<>();
/** Returns the wrapped Objectify's ObjectifyFactory. */
public ObjectifyFactory factory() {
return ofy().factory();
}
/** Clears the session cache. */
public void clearSessionCache() {
ofy().clear();
}
boolean inTransaction() {
return ofy().getTransaction() != null;
}
public void assertInTransaction() {
checkState(inTransaction(), "Must be called in a transaction");
}
/** Load from Datastore. */
public Loader load() {
return ofy().load();
}
/**
* Delete, augmented to enroll the deleted entities in a commit log.
*
* <p>We only allow this in transactions so commit logs can be written in tandem with the delete.
*/
public Deleter delete() {
return deleteIgnoringReadOnlyWithBackup();
}
/**
* Delete, without any augmentations except to check that we're not saving any virtual entities.
*
* <p>No backups get written.
*/
public Deleter deleteWithoutBackup() {
return deleteIgnoringReadOnlyWithoutBackup();
}
/**
* Save, augmented to enroll the saved entities in a commit log and to check that we're not saving
* virtual entities.
*
* <p>We only allow this in transactions so commit logs can be written in tandem with the save.
*/
public Saver save() {
return saveIgnoringReadOnlyWithBackup();
}
/**
* Save, without any augmentations except to check that we're not saving any virtual entities.
*
* <p>No backups get written.
*/
public Saver saveWithoutBackup() {
return saveIgnoringReadOnlyWithoutBackup();
}
/** Save, ignoring any backups or any read-only settings. */
public Saver saveIgnoringReadOnlyWithoutBackup() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable<?> entities) {
checkProhibitedAnnotations(entities, VirtualEntity.class);
}
};
}
/** Delete, ignoring any backups or any read-only settings. */
public Deleter deleteIgnoringReadOnlyWithoutBackup() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable<Key<?>> keys) {
checkProhibitedAnnotations(keys, VirtualEntity.class);
}
};
}
/** Save, ignoring any read-only settings (but still write commit logs). */
public Saver saveIgnoringReadOnlyWithBackup() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable<?> entities) {
assertInTransaction();
checkState(
Streams.stream(entities).allMatch(Objects::nonNull), "Can't save a null entity.");
checkProhibitedAnnotations(entities, NotBackedUp.class, VirtualEntity.class);
ImmutableMap<Key<?>, ?> keysToEntities = uniqueIndex(entities, Key::create);
TRANSACTION_INFO.get().putSaves(keysToEntities);
}
};
}
/** Delete, ignoring any read-only settings (but still write commit logs). */
public Deleter deleteIgnoringReadOnlyWithBackup() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable<Key<?>> keys) {
assertInTransaction();
checkState(Streams.stream(keys).allMatch(Objects::nonNull), "Can't delete a null key.");
checkProhibitedAnnotations(keys, NotBackedUp.class, VirtualEntity.class);
TRANSACTION_INFO.get().putDeletes(keys);
}
};
}
private Clock getClock() {
return injectedClock == null ? clock : injectedClock;
}
/** Execute a transaction. */
<R> R transact(Supplier<R> work) {
// If we are already in a transaction, don't wrap in a CommitLoggedWork.
return inTransaction() ? work.get() : transactNew(work);
}
/**
* Execute a transaction.
*
* <p>This overload is used for transactions that don't return a value, formerly implemented using
* VoidWork.
*/
void transact(Runnable work) {
transact(
() -> {
work.run();
return null;
});
}
/** Pause the current transaction (if any) and complete this one before returning to it. */
public <R> R transactNew(Supplier<R> work) {
// Wrap the Work in a CommitLoggedWork so that we can give transactions a frozen view of time.
return transactCommitLoggedWork(new CommitLoggedWork<>(work, getClock()));
}
/**
* Pause the current transaction (if any) and complete this one before returning to it.
*
* <p>This overload is used for transactions that don't return a value, formerly implemented using
* VoidWork.
*/
void transactNew(Runnable work) {
transactNew(
() -> {
work.run();
return null;
});
}
/**
* Transact with commit logs and retry with exponential backoff.
*
* <p>This method is broken out from {@link #transactNew(Supplier)} for testing purposes.
*/
@VisibleForTesting
<R> R transactCommitLoggedWork(CommitLoggedWork<R> work) {
long baseRetryMillis = getBaseOfyRetryDuration().getMillis();
for (long attempt = 0, sleepMillis = baseRetryMillis;
true;
attempt++, sleepMillis *= 2) {
try {
ofy().transactNew(() -> {
work.run();
return null;
});
return work.getResult();
} catch (TransientFailureException
| DatastoreTimeoutException
| DatastoreFailureException e) {
// TransientFailureExceptions come from task queues and always mean nothing committed.
// TimestampInversionExceptions are thrown by our code and are always retryable as well.
// However, Datastore exceptions might get thrown even if the transaction succeeded.
if ((e instanceof DatastoreTimeoutException || e instanceof DatastoreFailureException)
&& work.hasRun()) {
return work.getResult();
}
if (attempt == NUM_RETRIES) {
throw e; // Give up.
}
sleeper.sleepUninterruptibly(Duration.millis(sleepMillis));
logger.atInfo().withCause(e).log(
"Retrying %s, attempt %d.", e.getClass().getSimpleName(), attempt);
}
}
}
/** A read-only transaction is useful to get strongly consistent reads at a shared timestamp. */
<R> R transactNewReadOnly(Supplier<R> work) {
ReadOnlyWork<R> readOnlyWork = new ReadOnlyWork<>(work, getClock());
try {
ofy().transactNew(() -> {
readOnlyWork.run();
return null;
});
} catch (TransientFailureException | DatastoreTimeoutException | DatastoreFailureException e) {
// These are always retryable for a read-only operation.
return transactNewReadOnly(work);
} catch (KillTransactionException e) {
// Expected; we killed the transaction as a safety measure, and now we can return the result.
return readOnlyWork.getResult();
}
throw new AssertionError(); // How on earth did we get here?
}
void transactNewReadOnly(Runnable work) {
transactNewReadOnly(
() -> {
work.run();
return null;
});
}
/** Execute some work in a transactionless context. */
public <R> R doTransactionless(Supplier<R> work) {
try {
com.googlecode.objectify.ObjectifyService.push(
com.googlecode.objectify.ObjectifyService.ofy().transactionless());
return work.get();
} finally {
com.googlecode.objectify.ObjectifyService.pop();
}
}
/**
* Execute some work with a fresh session cache.
*
* <p>This is useful in cases where we want to load the latest possible data from Datastore but
* don't need point-in-time consistency across loads and consequently don't need a transaction.
* Note that unlike a transaction's fresh session cache, the contents of this cache will be
* discarded once the work completes, rather than being propagated into the enclosing session.
*/
public <R> R doWithFreshSessionCache(Supplier<R> work) {
try {
com.googlecode.objectify.ObjectifyService.push(
com.googlecode.objectify.ObjectifyService.factory().begin());
return work.get();
} finally {
com.googlecode.objectify.ObjectifyService.pop();
}
}
/** Get the time associated with the start of this particular transaction attempt. */
DateTime getTransactionTime() {
assertInTransaction();
return TRANSACTION_INFO.get().transactionTime;
}
/**
* Returns the @Entity-annotated base class for an object that is either an {@code Key<?>} or an
* object of an entity class registered with Objectify.
*/
@VisibleForTesting
static Class<?> getBaseEntityClassFromEntityOrKey(Object entityOrKey) {
// Convert both keys and entities into keys, so that we get consistent behavior in either case.
Key<?> key = (entityOrKey instanceof Key<?> ? (Key<?>) entityOrKey : Key.create(entityOrKey));
// Get the entity class associated with this key's kind, which should be the base @Entity class
// from which the kind name is derived. Don't be tempted to use getMetadata(String kind) or
// getMetadataForEntity(T pojo) instead; the former won't throw an exception for an unknown
// kind (it just returns null) and the latter will return the @EntitySubclass if there is one.
return ofy().factory().getMetadata(key).getEntityClass();
}
/**
* Checks that the base @Entity classes for the provided entities or keys don't have any of the
* specified forbidden annotations.
*/
@SafeVarargs
private static void checkProhibitedAnnotations(
Iterable<?> entitiesOrKeys, Class<? extends Annotation>... annotations) {
for (Object entityOrKey : entitiesOrKeys) {
Class<?> entityClass = getBaseEntityClassFromEntityOrKey(entityOrKey);
for (Class<? extends Annotation> annotation : annotations) {
checkArgument(!entityClass.isAnnotationPresent(annotation),
"Can't save/delete a @%s entity: %s", annotation.getSimpleName(), entityClass);
}
}
}
}
@@ -1,44 +0,0 @@
// Copyright 2017 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.ofy;
import google.registry.model.annotations.DeleteAfterMigration;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/** A filter that statically registers types with Objectify. */
@DeleteAfterMigration
public class OfyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
filterChain.doFilter(request, response);
}
@Override
public void init(FilterConfig config) {
// Make sure that we've registered all types before we do anything else with Objectify.
ObjectifyService.initOfy();
}
@Override
public void destroy() {}
}
@@ -1,42 +0,0 @@
// Copyright 2017 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.ofy;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.util.Clock;
import java.util.function.Supplier;
/** Wrapper for {@link Supplier} that disallows mutations and fails the transaction at the end. */
@DeleteAfterMigration
class ReadOnlyWork<R> extends CommitLoggedWork<R> {
ReadOnlyWork(Supplier<R> work, Clock clock) {
super(work, clock);
}
@Override
protected TransactionInfo createNewTransactionInfo() {
return super.createNewTransactionInfo().setReadOnly();
}
@Override
public void run() {
super.run();
throw new KillTransactionException();
}
/** Exception used to exit a transaction. */
static class KillTransactionException extends RuntimeException {}
}
@@ -1,194 +0,0 @@
// Copyright 2017 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.ofy;
import static java.util.Collections.synchronizedList;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreAttributes;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Index;
import com.google.appengine.api.datastore.Index.IndexState;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyRange;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.datastore.TransactionOptions;
import com.google.common.collect.ImmutableList;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
/** A proxy for {@link AsyncDatastoreService} that exposes call counts. */
@DeleteAfterMigration
public class RequestCapturingAsyncDatastoreService implements AsyncDatastoreService {
private final AsyncDatastoreService delegate;
// Each outer lists represents Datastore operations, with inner lists representing the keys or
// entities involved in that operation. We use static lists because we care about overall calls to
// Datastore, not calls via a specific instance of the service.
private static List<List<Key>> reads = synchronizedList(new ArrayList<List<Key>>());
private static List<List<Key>> deletes = synchronizedList(new ArrayList<List<Key>>());
private static List<List<Entity>> puts = synchronizedList(new ArrayList<List<Entity>>());
RequestCapturingAsyncDatastoreService(AsyncDatastoreService delegate) {
this.delegate = delegate;
}
public static List<List<Key>> getReads() {
return reads;
}
public static List<List<Key>> getDeletes() {
return deletes;
}
public static List<List<Entity>> getPuts() {
return puts;
}
@Override
public Collection<Transaction> getActiveTransactions() {
return delegate.getActiveTransactions();
}
@Override
public Transaction getCurrentTransaction() {
return delegate.getCurrentTransaction();
}
@Override
public Transaction getCurrentTransaction(Transaction transaction) {
return delegate.getCurrentTransaction(transaction);
}
@Override
public PreparedQuery prepare(Query query) {
return delegate.prepare(query);
}
@Override
public PreparedQuery prepare(Transaction transaction, Query query) {
return delegate.prepare(transaction, query);
}
@Override
public Future<KeyRange> allocateIds(String kind, long num) {
return delegate.allocateIds(kind, num);
}
@Override
public Future<KeyRange> allocateIds(Key parent, String kind, long num) {
return delegate.allocateIds(parent, kind, num);
}
@Override
public Future<Transaction> beginTransaction() {
return delegate.beginTransaction();
}
@Override
public Future<Transaction> beginTransaction(TransactionOptions transaction) {
return delegate.beginTransaction(transaction);
}
@Override
public Future<Void> delete(Key... keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(keys);
}
@Override
public Future<Void> delete(Iterable<Key> keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(keys);
}
@Override
public Future<Void> delete(Transaction transaction, Key... keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(transaction, keys);
}
@Override
public Future<Void> delete(Transaction transaction, Iterable<Key> keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(transaction, keys);
}
@Override
public Future<Entity> get(Key key) {
reads.add(ImmutableList.of(key));
return delegate.get(key);
}
@Override
public Future<Map<Key, Entity>> get(Iterable<Key> keys) {
reads.add(ImmutableList.copyOf(keys));
return delegate.get(keys);
}
@Override
public Future<Entity> get(Transaction transaction, Key key) {
reads.add(ImmutableList.of(key));
return delegate.get(transaction, key);
}
@Override
public Future<Map<Key, Entity>> get(Transaction transaction, Iterable<Key> keys) {
reads.add(ImmutableList.copyOf(keys));
return delegate.get(transaction, keys);
}
@Override
public Future<DatastoreAttributes> getDatastoreAttributes() {
return delegate.getDatastoreAttributes();
}
@Override
public Future<Map<Index, IndexState>> getIndexes() {
return delegate.getIndexes();
}
@Override
public Future<Key> put(Entity entity) {
puts.add(ImmutableList.of(entity));
return delegate.put(entity);
}
@Override
public Future<List<Key>> put(Iterable<Entity> entities) {
puts.add(ImmutableList.copyOf(entities));
return delegate.put(entities);
}
@Override
public Future<Key> put(Transaction transaction, Entity entity) {
puts.add(ImmutableList.of(entity));
return delegate.put(transaction, entity);
}
@Override
public Future<List<Key>> put(Transaction transaction, Iterable<Entity> entities) {
puts.add(ImmutableList.copyOf(entities));
return delegate.put(transaction, entities);
}
}
@@ -1,73 +0,0 @@
// Copyright 2017 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.ofy;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.toMap;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.Map;
import org.joda.time.DateTime;
/** Metadata for an {@link Ofy} transaction that saves commit logs. */
@DeleteAfterMigration
public class TransactionInfo {
@VisibleForTesting
public enum Delete {
SENTINEL
}
/** Logical "now" of the transaction. */
DateTime transactionTime;
/** Whether this is a read-only transaction. */
private boolean readOnly;
/**
* Accumulator of save/delete operations performed in transaction.
*
* <p>The {@link ImmutableMap} builder provides us the benefit of not permitting duplicates.
* This allows us to avoid potential race conditions where the same key is mutated twice in a
* transaction.
*/
private final ImmutableMap.Builder<Key<?>, Object> changesBuilder = new ImmutableMap.Builder<>();
TransactionInfo(DateTime now) {
this.transactionTime = now;
}
TransactionInfo setReadOnly() {
this.readOnly = true;
return this;
}
void assertNotReadOnly() {
checkState(!readOnly, "This is a read only transaction.");
}
void putSaves(Map<Key<?>, ?> keysToEntities) {
assertNotReadOnly();
changesBuilder.putAll(keysToEntities);
}
void putDeletes(Iterable<Key<?>> keys) {
assertNotReadOnly();
changesBuilder.putAll(toMap(keys, k -> Delete.SENTINEL));
}
}
@@ -57,7 +57,7 @@ import javax.persistence.Table;
* set to true.
*/
@Entity
@Table(indexes = {@Index(columnList = "gaeUserId", name = "registrarpoc_gae_user_id_idx")})
@Table(indexes = @Index(columnList = "loginEmailAddress", name = "registrarpoc_login_email_idx"))
@IdClass(RegistrarPocId.class)
public class RegistrarPoc extends ImmutableObject implements Jsonifiable, UnsafeSerializable {
@@ -89,7 +89,7 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
}
Type(String display, boolean required) {
this.displayName = display;
displayName = display;
this.required = required;
}
}
@@ -97,7 +97,12 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
/** The name of the contact. */
String name;
/** The email address of the contact. */
/**
* The contact email address of the contact.
*
* <p>This is different from the login email which is assgined to the regstrar and cannot be
* changed.
*/
@Id String emailAddress;
@Id String registrarId;
@@ -118,13 +123,21 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
Set<Type> types;
/**
* A GAE user ID allowed to act as this registrar contact.
* A GAIA email address that was assigned to the registrar for console login purpose.
*
* <p>This can be derived from a known email address using http://email-to-gae-id.appspot.com.
* <p>We used to store the GAE user ID directly to identify the logged-in user in the registrar
* console, and relied on a hacky trick with datastore to get the ID from the email address when
* creating a {@link RegistrarPoc}. We switched to using the login email directly as each
* registrar is assigned a unique email address that is immutable (to them at least), so it is as
* good as an identifier as the ID itself, and it allows us to get rid of the datastore
* dependency.
*
* @see com.google.appengine.api.users.User#getUserId()
* <p>We backfilled all login email addresses for existing {@link RegistrarPoc}s that have a
* non-null GAE user ID. The backfill is done by first trying the {@link #emailAddress} field,
* then trying {@link #registrarId}+"@known-dasher_domain" and picking the ones that converted to
* the existing ID stored in the database.
*/
String gaeUserId;
String loginEmailAddress;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact.
@@ -220,8 +233,8 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
return visibleInDomainWhoisAsAbuse;
}
public String getGaeUserId() {
return gaeUserId;
public String getLoginEmailAddress() {
return loginEmailAddress;
}
public Builder asBuilder() {
@@ -258,8 +271,8 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
* Types: [ADMIN, WHOIS]
* Visible in WHOIS as Admin contact: Yes
* Visible in WHOIS as Technical contact: No
* GAE-UserID: 1234567890
* Registrar-Console access: Yes
* Login Email Address: person@registry.example
* }</pre>
*/
public String toStringMultilinePlainText() {
@@ -289,10 +302,10 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
.append('\n');
result
.append("Registrar-Console access: ")
.append(getGaeUserId() != null ? "Yes" : "No")
.append(getLoginEmailAddress() != null ? "Yes" : "No")
.append('\n');
if (getGaeUserId() != null) {
result.append("GAE-UserID: ").append(getGaeUserId()).append('\n');
if (getLoginEmailAddress() != null) {
result.append("Login Email Address: ").append(getLoginEmailAddress()).append('\n');
}
return result.toString();
}
@@ -311,7 +324,7 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.put("gaeUserId", gaeUserId)
.put("loginEmailAddress", loginEmailAddress)
.build();
}
@@ -418,8 +431,8 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
return this;
}
public Builder setGaeUserId(String gaeUserId) {
getInstance().gaeUserId = gaeUserId;
public Builder setLoginEmailAddress(String loginEmailAddress) {
getInstance().loginEmailAddress = loginEmailAddress;
return this;
}
@@ -30,6 +30,7 @@ import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
@@ -45,11 +46,14 @@ import google.registry.model.UnsafeSerializable;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.ReservedList;
import google.registry.persistence.VKey;
import google.registry.persistence.converter.JodaMoneyType;
import google.registry.util.Idn;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -451,6 +455,18 @@ public class Registry extends ImmutableObject implements Buildable, UnsafeSerial
/** An allowlist of hosts allowed to be used on domains on this TLD (ignored if empty). */
@Nullable Set<String> allowedFullyQualifiedHostNames;
/**
* References to allocation tokens that can be used on the TLD if no other token is passed in on a
* domain create.
*
* <p>Ordering is important for this field as it will determine which token is used if multiple
* tokens in the list are valid for a specific registration. It is crucial that modifications to
* this field only modify the entire list contents. Modifications to a single token in the list
* (ex: add a token to the list or remove a token from the list) should not be allowed without
* resetting the entire list contents.
*/
List<VKey<AllocationToken>> defaultPromoTokens;
public String getTldStr() {
return tldStr;
}
@@ -639,6 +655,10 @@ public class Registry extends ImmutableObject implements Buildable, UnsafeSerial
return nullToEmptyImmutableCopy(allowedFullyQualifiedHostNames);
}
public ImmutableList<VKey<AllocationToken>> getDefaultPromoTokens() {
return nullToEmptyImmutableCopy(defaultPromoTokens);
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -900,6 +920,28 @@ public class Registry extends ImmutableObject implements Buildable, UnsafeSerial
return this;
}
public Builder setDefaultPromoTokens(ImmutableList<VKey<AllocationToken>> promoTokens) {
tm().transact(
() -> {
for (VKey<AllocationToken> tokenKey : promoTokens) {
AllocationToken token = tm().loadByKey(tokenKey);
checkArgument(
token.getTokenType().equals(TokenType.DEFAULT_PROMO),
String.format(
"Token %s has an invalid token type of %s. DefaultPromoTokens must be of"
+ " the type DEFAULT_PROMO",
token.getToken(), token.getTokenType()));
checkArgument(
token.getAllowedTlds().contains(getInstance().tldStr),
String.format(
"The token %s is not valid for this TLD. The valid TLDs for it are %s",
token.getToken(), token.getAllowedTlds()));
}
getInstance().defaultPromoTokens = promoTokens;
});
return this;
}
@Override
public Registry build() {
final Registry instance = getInstance();
@@ -75,7 +75,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
* Constructs a {@link VKey} for an {@link EppResource } from the string representation.
*
* <p>The string representation is obtained from the {@link #stringify()} function and like this:
* {@code kind:TestObject@sql:rO0ABXQAA2Zvbw}
* {@code kind:SomeEntity@sql:rO0ABXQAA2Zvbw}
*/
public static <T extends EppResource> VKey<T> createEppVKeyFromString(String keyString) {
ImmutableMap<String, String> kvs =
@@ -0,0 +1,33 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.converter;
import google.registry.model.domain.token.AllocationToken;
import google.registry.persistence.VKey;
import javax.persistence.Converter;
@Converter(autoApply = true)
public class AllocationTokenListConverter extends StringListConverterBase<VKey<AllocationToken>> {
@Override
String toString(VKey<AllocationToken> element) {
return element.getKey().toString();
}
@Override
VKey<AllocationToken> fromString(String value) {
return VKey.create(AllocationToken.class, value);
}
}
@@ -149,14 +149,6 @@ public abstract class QueryComposer<T> {
/**
* Enum used to specify comparison operations, e.g. {@code where("fieldName", Comparator.NE,
* "someval")'}.
*
* <p>These contain values that specify the comparison behavior for both objectify and criteria
* queries. For objectify, we provide a string to be appended to the field name in a {@code
* filter()} expression. For criteria queries we provide a function that knows how to obtain a
* {@link WhereOperator} from a {@link CriteriaBuilder}.
*
* <p>Note that the objectify strings for comparators other than equality are preceded by a space
* because {@code filter()} expects the fieldname to be separated from the operator by a space.
*/
public enum Comparator {
/**
@@ -21,6 +21,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import com.google.appengine.api.users.User;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.flogger.FluentLogger;
@@ -307,7 +308,7 @@ public class AuthenticatedRegistrarAccessor {
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
if (userAuthInfo.appEngineUser().isPresent()) {
User user = userAuthInfo.appEngineUser().get();
logger.atInfo().log("Checking registrar contacts for user ID %s.", user.getUserId());
logger.atInfo().log("Checking registrar contacts for user ID %s.", user.getEmail());
// Find all registrars that have a registrar contact with this user's ID.
jpaTm()
@@ -315,11 +316,11 @@ public class AuthenticatedRegistrarAccessor {
() ->
jpaTm()
.query(
"SELECT r FROM Registrar r INNER JOIN RegistrarPoc rp ON "
+ "r.registrarId = rp.registrarId WHERE rp.gaeUserId = "
+ ":gaeUserId AND r.state != :state",
"SELECT r FROM Registrar r INNER JOIN RegistrarPoc rp ON r.registrarId ="
+ " rp.registrarId WHERE lower(rp.loginEmailAddress) = :email AND"
+ " r.state != :state",
Registrar.class)
.setParameter("gaeUserId", user.getUserId())
.setParameter("email", Ascii.toLowerCase(user.getEmail()))
.setParameter("state", State.DISABLED)
.getResultStream()
.forEach(registrar -> builder.put(registrar.getRegistrarId(), Role.OWNER)));
@@ -19,12 +19,12 @@ import static google.registry.config.RegistryConfig.ConfigModule.TmchCaMode.PROD
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
import google.registry.model.CacheUtils;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.tmch.TmchCrl;
import google.registry.util.Clock;
import google.registry.util.X509Utils;
@@ -78,7 +78,7 @@ public final class TmchCertificateAuthority {
private static final LoadingCache<TmchCaMode, X509CRL> CRL_CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new AppEngineEnvironmentCacheLoader<TmchCaMode, X509CRL>() {
new CacheLoader<TmchCaMode, X509CRL>() {
@Override
public X509CRL load(final TmchCaMode tmchCaMode) throws GeneralSecurityException {
Optional<TmchCrl> storedCrl = TmchCrl.get();
@@ -53,7 +53,7 @@ import javax.inject.Inject;
* event time is in the past), same as through EPP.
*/
@Parameters(separators = " =", commandDescription = "Acknowledge one-time poll messages.")
final class AckPollMessagesCommand implements CommandWithRemoteApi {
final class AckPollMessagesCommand implements Command {
@Parameter(
names = {"-c", "--client"},
@@ -16,5 +16,5 @@ package google.registry.tools;
/** A command that can send HTTP requests to a backend module. */
public interface CommandWithConnection extends Command {
void setConnection(AppEngineConnection connection);
void setConnection(ServiceConnection connection);
}
@@ -1,23 +0,0 @@
// Copyright 2018 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.tools;
/**
* Marker interface for commands that use the remote api.
*
* <p>Just implementing this is sufficient to use the remote api; {@link RegistryTool} will install
* it as needed.
*/
public interface CommandWithRemoteApi extends Command {}
@@ -28,7 +28,7 @@ import org.joda.time.DateTime;
/** Command to show the count of active domains on a given TLD. */
@Parameters(separators = " =", commandDescription = "Show count of domains on TLD")
final class CountDomainsCommand implements CommandWithRemoteApi {
final class CountDomainsCommand implements Command {
@Parameter(
names = {"-t", "--tld", "--tlds"},
@@ -33,8 +33,7 @@ import org.joda.time.DateTime;
/** A command to create a new domain via EPP. */
@Parameters(separators = " =", commandDescription = "Create a new domain via EPP.")
final class CreateDomainCommand extends CreateOrUpdateDomainCommand
implements CommandWithRemoteApi {
final class CreateDomainCommand extends CreateOrUpdateDomainCommand {
@Parameter(
names = "--period",
@@ -27,8 +27,7 @@ import org.joda.money.CurrencyUnit;
* Base class for specification of command line parameters common to creating and updating premium
* lists.
*/
abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
protected List<String> inputData;
@@ -26,8 +26,7 @@ import javax.annotation.Nullable;
* Base class for specification of command line parameters common to creating and updating reserved
* lists.
*/
public abstract class CreateOrUpdateReservedListCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
public abstract class CreateOrUpdateReservedListCommand extends ConfirmingCommand {
static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -40,7 +40,7 @@ import javax.annotation.Nullable;
/** Command to create a Registrar. */
@Parameters(separators = " =", commandDescription = "Create new registrar account(s)")
final class CreateRegistrarCommand extends CreateOrUpdateRegistrarCommand
implements CommandWithConnection, CommandWithRemoteApi {
implements CommandWithConnection {
private static final ImmutableSet<RegistryToolEnvironment> ENVIRONMENTS_ALLOWING_GROUP_CREATION =
ImmutableSet.of(PRODUCTION, SANDBOX, UNITTEST);
@@ -51,10 +51,10 @@ final class CreateRegistrarCommand extends CreateOrUpdateRegistrarCommand
arity = 1)
boolean createGoogleGroups = true;
private AppEngineConnection connection;
private ServiceConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -30,7 +30,7 @@ import java.util.List;
/** Command to create groups in Google Groups for all contact types for a registrar. */
@Parameters(separators = " =", commandDescription = "Create groups for a registrar.")
public class CreateRegistrarGroupsCommand extends ConfirmingCommand
implements CommandWithConnection, CommandWithRemoteApi {
implements CommandWithConnection {
@Parameter(
description = "Client identifier(s) of the registrar(s) to create groups for",
@@ -39,10 +39,10 @@ public class CreateRegistrarGroupsCommand extends ConfirmingCommand
private List<Registrar> registrars = new ArrayList<>();
private AppEngineConnection connection;
private ServiceConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -66,7 +66,7 @@ public class CreateRegistrarGroupsCommand extends ConfirmingCommand
}
/** Calls the server endpoint to create groups for the specified registrar client id. */
static void executeOnServer(AppEngineConnection connection, String clientId) throws IOException {
static void executeOnServer(ServiceConnection connection, String clientId) throws IOException {
connection.sendPostRequest(
CreateGroupsAction.PATH,
ImmutableMap.of(CreateGroupsAction.CLIENT_ID_PARAM, clientId),
@@ -31,7 +31,7 @@ import java.util.List;
@Parameters(separators = " =", commandDescription = "Send an HTTP command to the nomulus server.")
class CurlCommand implements CommandWithConnection {
private AppEngineConnection connection;
private ServiceConnection connection;
// HTTP Methods that are acceptable for use as values for --method.
public enum Method {
@@ -76,7 +76,7 @@ class CurlCommand implements CommandWithConnection {
private Service service;
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -90,7 +90,7 @@ class CurlCommand implements CommandWithConnection {
throw new IllegalArgumentException("You may not specify a body for a get method.");
}
AppEngineConnection connectionToService = connection.withService(service);
ServiceConnection connectionToService = connection.withService(service);
String response =
(method == Method.GET)
? connectionToService.sendGetRequest(path, ImmutableMap.<String, String>of())
@@ -29,7 +29,7 @@ import javax.annotation.Nullable;
* in use on a tld.
*/
@Parameters(separators = " =", commandDescription = "Delete a PremiumList.")
final class DeletePremiumListCommand extends ConfirmingCommand implements CommandWithRemoteApi {
final class DeletePremiumListCommand extends ConfirmingCommand {
@Nullable PremiumList premiumList;
@@ -28,7 +28,7 @@ import google.registry.model.tld.label.ReservedListDao;
* reserved list is currently in use on a tld.
*/
@Parameters(separators = " =", commandDescription = "Deletes a ReservedList from the database.")
final class DeleteReservedListCommand extends ConfirmingCommand implements CommandWithRemoteApi {
final class DeleteReservedListCommand extends ConfirmingCommand {
@Parameter(
names = {"-n", "--name"},
@@ -31,7 +31,7 @@ import google.registry.persistence.transaction.QueryComposer.Comparator;
* <p>This command will fail if any domains are currently registered on the TLD.
*/
@Parameters(separators = " =", commandDescription = "Delete a TLD from Datastore.")
final class DeleteTldCommand extends ConfirmingCommand implements CommandWithRemoteApi {
final class DeleteTldCommand extends ConfirmingCommand {
private Registry registry;
@@ -26,7 +26,7 @@ import javax.inject.Inject;
/** Command to encrypt an escrow deposit. */
@Parameters(separators = " =", commandDescription = "Encrypt an escrow deposit")
class EncryptEscrowDepositCommand implements CommandWithRemoteApi {
class EncryptEscrowDepositCommand implements Command {
@Parameter(
names = {"-t", "--tld"},
@@ -1,80 +0,0 @@
// Copyright 2017 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.tools;
import com.google.appengine.api.datastore.Entity;
import com.google.auto.value.AutoValue;
import com.google.common.base.Objects;
/**
* Wraps {@link Entity} for ease of processing in collections.
*
* <p>Note that the {@link #hashCode}/{@link #equals} methods are based on both the entity's key and
* its properties.
*/
final class EntityWrapper {
private static final String TEST_ENTITY_KIND = "TestEntity";
private final Entity entity;
EntityWrapper(Entity entity) {
this.entity = entity;
}
public Entity getEntity() {
return entity;
}
@Override
public boolean equals(Object that) {
if (that instanceof EntityWrapper) {
EntityWrapper thatEntity = (EntityWrapper) that;
return entity.equals(thatEntity.entity)
&& entity.getProperties().equals(thatEntity.entity.getProperties());
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(entity.getKey(), entity.getProperties());
}
@Override
public String toString() {
return "EntityWrapper(" + entity + ")";
}
public static EntityWrapper from(int id, Property... properties) {
Entity entity = new Entity(TEST_ENTITY_KIND, id);
for (Property prop : properties) {
entity.setProperty(prop.name(), prop.value());
}
return new EntityWrapper(entity);
}
@AutoValue
abstract static class Property {
static Property create(String name, Object value) {
return new AutoValue_EntityWrapper_Property(name, value);
}
abstract String name();
abstract Object value();
}
}
@@ -47,8 +47,7 @@ import java.util.Map;
import java.util.Objects;
/** A command to execute an epp command. */
abstract class EppToolCommand extends ConfirmingCommand
implements CommandWithConnection, CommandWithRemoteApi {
abstract class EppToolCommand extends ConfirmingCommand implements CommandWithConnection {
@Parameter(
names = {"-u", "--superuser"},
@@ -60,7 +59,7 @@ abstract class EppToolCommand extends ConfirmingCommand
private List<XmlEppParameters> commands = new ArrayList<>();
private AppEngineConnection connection;
private ServiceConnection connection;
static class XmlEppParameters {
final String clientId;
@@ -97,7 +96,7 @@ abstract class EppToolCommand extends ConfirmingCommand
}
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -69,7 +69,7 @@ import org.joda.time.DateTime;
"Generates and persists the given number of AllocationTokens, "
+ "printing each token to stdout.")
@NonFinalForTesting
class GenerateAllocationTokensCommand implements CommandWithRemoteApi {
class GenerateAllocationTokensCommand implements Command {
@Parameter(
names = {"--tokens"},
@@ -42,7 +42,7 @@ import org.json.simple.JSONValue;
/** Command to generate a report of all DNS data. */
@Parameters(separators = " =", commandDescription = "Generate report of all DNS data in a TLD.")
final class GenerateDnsReportCommand implements CommandWithRemoteApi {
final class GenerateDnsReportCommand implements Command {
@Parameter(
names = {"-t", "--tld"},
@@ -43,7 +43,7 @@ import org.joda.time.DateTime;
* be stored in the specified manual subdirectory of the GCS RDE bucket.
*/
@Parameters(separators = " =", commandDescription = "Generate an XML escrow deposit.")
final class GenerateEscrowDepositCommand implements CommandWithRemoteApi {
final class GenerateEscrowDepositCommand implements Command {
@Parameter(
names = {"-t", "--tld"},
@@ -33,7 +33,7 @@ import org.joda.time.DateTime;
/** Command to generate a LORDN CSV file for an entire TLD. */
@Parameters(separators = " =", commandDescription = "Generate LORDN CSV file")
final class GenerateLordnCommand implements CommandWithRemoteApi {
final class GenerateLordnCommand implements Command {
@Parameter(
names = {"-t", "--tld"},
@@ -30,7 +30,7 @@ import org.joda.time.DateTime;
/** Command to generate zone files. */
@Parameters(separators = " =", commandDescription = "Generate zone files")
final class GenerateZoneFilesCommand implements CommandWithConnection, CommandWithRemoteApi {
final class GenerateZoneFilesCommand implements CommandWithConnection {
@Parameter(
description = "One or more TLDs to generate zone files for",
@@ -45,10 +45,10 @@ final class GenerateZoneFilesCommand implements CommandWithConnection, CommandWi
validateWith = DateParameter.class)
private DateTime exportDate = DateTime.now(UTC).minus(standardMinutes(2)).withTimeAtStartOfDay();
private AppEngineConnection connection;
private ServiceConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -32,7 +32,7 @@ import java.util.Optional;
/** Command to show allocation tokens. */
@Parameters(separators = " =", commandDescription = "Show allocation token(s)")
final class GetAllocationTokenCommand implements CommandWithRemoteApi {
final class GetAllocationTokenCommand implements Command {
@Parameter(
description = "Allocation token(s)",
@@ -34,7 +34,7 @@ import java.nio.file.Paths;
* currently storing in SQL.
*/
@Parameters(separators = " =", commandDescription = "Download the current claims list")
final class GetClaimsListCommand implements CommandWithRemoteApi {
final class GetClaimsListCommand implements Command {
@Parameter(
names = {"-o", "--output"},
@@ -23,7 +23,7 @@ import google.registry.model.common.TimedTransitionProperty;
/** A command to check the current Registry 3.0 migration state of the database. */
@DeleteAfterMigration
@Parameters(separators = " =", commandDescription = "Check current Registry 3.0 migration state")
public class GetDatabaseMigrationStateCommand implements CommandWithRemoteApi {
public class GetDatabaseMigrationStateCommand implements Command {
@Override
public void run() throws Exception {
@@ -26,7 +26,7 @@ import org.joda.time.DateTime;
/** Abstract command to print one or more resources to stdout. */
@Parameters(separators = " =")
abstract class GetEppResourceCommand implements CommandWithRemoteApi {
abstract class GetEppResourceCommand implements Command {
@Parameter(
names = "--read_timestamp",
@@ -60,11 +60,11 @@ abstract class GetEppResourceCommand implements CommandWithRemoteApi {
@Override
public void run() {
DateTime now = clock.nowUtc();
if (readTimestamp == null) {
readTimestamp = clock.nowUtc();
readTimestamp = now;
}
checkArgument(
!readTimestamp.isBefore(clock.nowUtc()), "--read_timestamp may not be in the past");
checkArgument(!readTimestamp.isBefore(now), "--read_timestamp may not be in the past");
runAndPrint();
}
}
@@ -34,7 +34,7 @@ import org.joda.time.DateTime;
@Parameters(
separators = " =",
commandDescription = "Show history entries that occurred in a given time range")
final class GetHistoryEntriesCommand implements CommandWithRemoteApi {
final class GetHistoryEntriesCommand implements Command {
@Parameter(
names = {"-a", "--after"},
@@ -30,10 +30,9 @@ import org.bouncycastle.openpgp.PGPKeyPair;
/** Retrieves ASCII-armored secrets from the active {@link Keyring} implementation. */
@Parameters(
separators = " =",
commandDescription = "Retrieves the value of a secret from the keyring."
)
final class GetKeyringSecretCommand implements CommandWithRemoteApi {
separators = " =",
commandDescription = "Retrieves the value of a secret from the keyring.")
final class GetKeyringSecretCommand implements Command {
@Inject Keyring keyring;
@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
/** Retrieves and prints one or more premium lists. */
@Parameters(separators = " =", commandDescription = "Show one or more premium lists")
public class GetPremiumListCommand implements CommandWithRemoteApi {
public class GetPremiumListCommand implements Command {
@Parameter(description = "Name(s) of the premium list(s) to retrieve", required = true)
private List<String> mainParameters;
@@ -23,7 +23,7 @@ import java.util.List;
/** Command to show a registrar record. */
@Parameters(separators = " =", commandDescription = "Show registrar record(s)")
final class GetRegistrarCommand implements CommandWithRemoteApi {
final class GetRegistrarCommand implements Command {
@Parameter(
description = "Client identifier of the registrar account(s)",
@@ -24,7 +24,7 @@ import java.util.stream.Collectors;
/** Retrieves and prints one or more reserved lists. */
@Parameters(separators = " =", commandDescription = "Show one or more reserved lists")
public class GetReservedListCommand implements CommandWithRemoteApi {
public class GetReservedListCommand implements Command {
@Parameter(
names = {"-n", "--name"},
@@ -23,7 +23,7 @@ import java.util.List;
/** Command to show a TLD record. */
@Parameters(separators = " =", commandDescription = "Show TLD record(s)")
final class GetTldCommand implements CommandWithRemoteApi {
final class GetTldCommand implements Command {
@Parameter(
description = "TLD(s) to show",
@@ -37,7 +37,7 @@ import org.bouncycastle.openpgp.PGPPublicKey;
/** Command to encrypt/decrypt {@code .ghostryde} files. */
@Parameters(separators = " =", commandDescription = "Encrypt/decrypt a ghostryde file.")
final class GhostrydeCommand implements CommandWithRemoteApi {
final class GhostrydeCommand implements Command {
@Parameter(
names = {"-e", "--encrypt"},
@@ -32,7 +32,7 @@ import java.util.Optional;
/** Lists {@link Cursor} timestamps used by locking rolling cursor tasks, like in RDE. */
@Parameters(separators = " =", commandDescription = "Lists cursor timestamps used by LRC tasks")
final class ListCursorsCommand implements CommandWithRemoteApi {
final class ListCursorsCommand implements Command {
@Parameter(names = "--type", description = "Which cursor to list.", required = true)
private CursorType cursorType;
@@ -33,7 +33,7 @@ import org.json.simple.JSONValue;
*
* <p>The formatting is done on the server side; this class just dumps the results to the screen.
*/
abstract class ListObjectsCommand implements CommandWithConnection, CommandWithRemoteApi {
abstract class ListObjectsCommand implements CommandWithConnection {
@Nullable
@Parameter(
@@ -54,10 +54,10 @@ abstract class ListObjectsCommand implements CommandWithConnection, CommandWithR
description = "Whether to print full field names in header row (as opposed to aliases)")
private boolean fullFieldNames = false;
private AppEngineConnection connection;
private ServiceConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -24,8 +24,7 @@ import google.registry.model.tld.Registries;
/** Command to initiate a load-test. */
@Parameters(separators = " =", commandDescription = "Run a load test.")
class LoadTestCommand extends ConfirmingCommand
implements CommandWithConnection, CommandWithRemoteApi {
class LoadTestCommand extends ConfirmingCommand implements CommandWithConnection {
// This is a mostly arbitrary value, roughly two and a half hours. It served as a generous
// timespan for initial backup/restore testing, but has no other special significance.
@@ -77,10 +76,10 @@ class LoadTestCommand extends ConfirmingCommand
description = "Time to run the load test in seconds.")
int runSeconds = DEFAULT_RUN_SECONDS;
private AppEngineConnection connection;
private ServiceConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -35,8 +35,7 @@ import java.util.List;
import javax.inject.Inject;
/** Shared base class for commands to registry lock or unlock a domain via EPP. */
public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -41,7 +41,7 @@ import java.util.Set;
import javax.annotation.Nullable;
/** A {@link ConfirmingCommand} that changes objects in Datastore. */
public abstract class MutatingCommand extends ConfirmingCommand implements CommandWithRemoteApi {
public abstract class MutatingCommand extends ConfirmingCommand {
/**
* A mutation of a specific entity, represented by an old and a new version of the entity. Storing
@@ -25,7 +25,7 @@ import javax.inject.Inject;
/** Command to show what escrow deposits are pending generation on the server. */
@Parameters(separators = " =", commandDescription = "List pending RDE/BRDA deposits.")
final class PendingEscrowCommand implements CommandWithRemoteApi {
final class PendingEscrowCommand implements Command {
private static final Ordering<PendingDeposit> SORTER =
new Ordering<PendingDeposit>() {
@@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -29,7 +28,6 @@ import com.google.common.base.Enums;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.tools.params.OptionalPhoneNumberParameter;
@@ -48,6 +46,7 @@ import java.util.Set;
import javax.annotation.Nullable;
/** Command for CRUD operations on {@link Registrar} contact list fields. */
@SuppressWarnings("OptionalAssignedToNull")
@Parameters(
separators = " =",
commandDescription = "Create/read/update/delete the various contact lists for a Registrar.")
@@ -78,9 +77,19 @@ final class RegistrarPocCommand extends MutatingCommand {
private List<String> contactTypeNames;
@Nullable
@Parameter(names = "--email", description = "Contact email address.")
@Parameter(
names = "--email",
description =
"Contact email address. Required when creating a contact"
+ " and will be used as the console login email, if --login_email is not specified.")
String email;
@Nullable
@Parameter(
names = "--login_email",
description = "Console login email address. If not specified, --email will be used.")
String loginEmail;
@Nullable
@Parameter(
names = "--registry_lock_email",
@@ -168,13 +177,14 @@ final class RegistrarPocCommand extends MutatingCommand {
// If the contact_type parameter is not specified, we should not make any changes.
if (contactTypeNames == null) {
contactTypes = null;
// It appears that when the user specifies "--contact_type=" with no types following, JCommander
// sets contactTypeNames to a one-element list containing the empty string. This is strange, but
// we need to handle this by setting the contact types to the empty set. Also do this if
// contactTypeNames is empty, which is what I would hope JCommander would return in some future,
// better world.
} else if (contactTypeNames.isEmpty()
|| ((contactTypeNames.size() == 1) && contactTypeNames.get(0).isEmpty())) {
// It appears that when the user specifies "--contact_type=" with no types following,
// JCommander sets contactTypeNames to a one-element list containing the empty string. This is
// strange, but we need to handle this by setting the contact types to the empty set. Also do
// this if contactTypeNames is empty, which is what I would hope JCommander would return in
// some future, better world.
} else //noinspection UnnecessaryParentheses
if (contactTypeNames.isEmpty()
|| (contactTypeNames.size() == 1 && contactTypeNames.get(0).isEmpty())) {
contactTypes = ImmutableSet.of();
} else {
contactTypes =
@@ -194,7 +204,7 @@ final class RegistrarPocCommand extends MutatingCommand {
break;
case CREATE:
stageEntityChange(null, createContact(registrar));
if ((visibleInDomainWhoisAsAbuse != null) && visibleInDomainWhoisAsAbuse) {
if (visibleInDomainWhoisAsAbuse != null && visibleInDomainWhoisAsAbuse) {
unsetOtherWhoisAbuseFlags(contacts, null);
}
break;
@@ -211,7 +221,7 @@ final class RegistrarPocCommand extends MutatingCommand {
"Cannot clear visible_in_domain_whois_as_abuse flag, as that would leave no domain"
+ " WHOIS abuse contacts; instead, set the flag on another contact");
stageEntityChange(oldContact, newContact);
if ((visibleInDomainWhoisAsAbuse != null) && visibleInDomainWhoisAsAbuse) {
if (visibleInDomainWhoisAsAbuse != null && visibleInDomainWhoisAsAbuse) {
unsetOtherWhoisAbuseFlags(contacts, oldContact.getEmailAddress());
}
break;
@@ -261,9 +271,7 @@ final class RegistrarPocCommand extends MutatingCommand {
builder.setTypes(nullToEmpty(contactTypes));
if (Objects.equals(allowConsoleAccess, Boolean.TRUE)) {
builder.setGaeUserId(checkArgumentNotNull(
GaeUserIdConverter.convertEmailAddressToGaeUserId(email),
String.format("Email address %s is not associated with any GAE ID", email)));
builder.setLoginEmailAddress(loginEmail == null ? email : loginEmail);
}
if (visibleInWhoisAsAdmin != null) {
builder.setVisibleInWhoisAsAdmin(visibleInWhoisAsAdmin);
@@ -311,11 +319,9 @@ final class RegistrarPocCommand extends MutatingCommand {
}
if (allowConsoleAccess != null) {
if (allowConsoleAccess.equals(Boolean.TRUE)) {
builder.setGaeUserId(checkArgumentNotNull(
GaeUserIdConverter.convertEmailAddressToGaeUserId(email),
String.format("Email address %s is not associated with any GAE ID", email)));
builder.setLoginEmailAddress(loginEmail == null ? email : loginEmail);
} else {
builder.setGaeUserId(null);
builder.setLoginEmailAddress(null);
}
}
if (allowedToSetRegistryLockPassword != null) {
@@ -15,26 +15,21 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.tools.Injector.injectReflectively;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.ParametersDelegate;
import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import google.registry.config.RegistryConfig;
import google.registry.model.ofy.ObjectifyService;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.tools.AuthModule.LoginRequiredException;
import google.registry.tools.params.ParameterFactory;
import java.io.ByteArrayInputStream;
import java.net.URL;
import java.security.Security;
import java.util.Map;
import java.util.Optional;
@@ -43,7 +38,7 @@ import org.postgresql.util.PSQLException;
/** Container class to create and run remote commands against a Datastore instance. */
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
final class RegistryCli implements AutoCloseable, CommandRunner {
final class RegistryCli implements CommandRunner {
// The environment parameter is parsed twice: once here, and once with {@link
// RegistryToolEnvironment#parseFromArgs} in the {@link RegistryTool#main} function.
@@ -79,9 +74,8 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
RegistryToolComponent component;
// These are created lazily on first use.
private AppEngineConnection connection;
private RemoteApiInstaller installer;
// This is created lazily on first use.
private ServiceConnection connection;
// The "shell" command should only exist on first use - so that we can't run "shell" inside
// "shell".
@@ -207,28 +201,10 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
}
}
@Override
public void close() {
if (installer != null) {
try {
installer.uninstall();
installer = null;
} catch (IllegalArgumentException e) {
// There is no point throwing the error if the API is already uninstalled, which is most
// likely caused by something wrong when installing the API. That something (e. g. no
// credential found) must have already thrown an error message earlier (e. g. must run
// "nomulus login" first). This error message here is non-actionable.
if (!e.getMessage().equals("remote API is already uninstalled")) {
throw e;
}
}
}
}
private AppEngineConnection getConnection() {
private ServiceConnection getConnection() {
// Get the App Engine connection, advise the user if they are not currently logged in..
if (connection == null) {
connection = component.appEngineConnection();
connection = component.serviceConnection();
}
return connection;
}
@@ -240,46 +216,14 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
((CommandWithConnection) command).setConnection(getConnection());
}
// CommandWithRemoteApis need to have the remote api installed to work.
if (command instanceof CommandWithRemoteApi) {
if (installer == null) {
installer = new RemoteApiInstaller();
RemoteApiOptions options = new RemoteApiOptions();
options.server(
getConnection().getServer().getHost(), getPort(getConnection().getServer()));
if (RegistryConfig.areServersLocal()) {
// Use dev credentials for localhost.
options.useDevelopmentServerCredential();
} else {
RemoteApiOptionsUtil.useGoogleCredentialStream(
options, new ByteArrayInputStream(component.googleCredentialJson().getBytes(UTF_8)));
}
installer.install(options);
// Database setup -- we also only ever do this if "installer" is null, just so that it's
// only done once.
// Ensure that all entity classes are loaded before command code runs.
ObjectifyService.initOfy();
// Make sure we start the command with a clean cache, so that any previous command won't
// interfere with this one.
ObjectifyService.auditedOfy().clearSessionCache();
// Enable Cloud SQL for command that needs remote API as they will very likely use
// Cloud SQL after the database migration. Note that the DB password is stored in Datastore
// and it is already initialized above.
TransactionManagerFactory.setJpaTm(
() -> component.nomulusToolJpaTransactionManager().get());
TransactionManagerFactory.setReplicaJpaTm(
() -> component.nomulusToolReplicaJpaTransactionManager().get());
}
}
// Reset the JPA transaction manager after every command to avoid a situation where a test can
// interfere with other tests
JpaTransactionManager cachedJpaTm = jpaTm();
TransactionManagerFactory.setJpaTm(() -> component.nomulusToolJpaTransactionManager().get());
TransactionManagerFactory.setReplicaJpaTm(
() -> component.nomulusToolReplicaJpaTransactionManager().get());
command.run();
}
private int getPort(URL url) {
return url.getPort() == -1 ? url.getDefaultPort() : url.getPort();
TransactionManagerFactory.setJpaTm(() -> cachedJpaTm);
}
void setEnvironment(RegistryToolEnvironment environment) {
@@ -17,7 +17,6 @@ package google.registry.tools;
import com.google.common.collect.ImmutableMap;
import google.registry.tools.javascrap.CompareEscrowDepositsCommand;
import google.registry.tools.javascrap.CreateCancellationsForOneTimesCommand;
import google.registry.tools.javascrap.CreateSyntheticDomainHistoriesCommand;
/** Container class to create and run remote commands against a Datastore instance. */
public final class RegistryTool {
@@ -48,7 +47,6 @@ public final class RegistryTool {
.put("create_registrar", CreateRegistrarCommand.class)
.put("create_registrar_groups", CreateRegistrarGroupsCommand.class)
.put("create_reserved_list", CreateReservedListCommand.class)
.put("create_synthetic_domain_histories", CreateSyntheticDomainHistoriesCommand.class)
.put("create_tld", CreateTldCommand.class)
.put("curl", CurlCommand.class)
.put("delete_allocation_tokens", DeleteAllocationTokensCommand.class)
@@ -123,8 +121,7 @@ public final class RegistryTool {
public static void main(String[] args) throws Exception {
RegistryToolEnvironment.parseFromArgs(args).setup();
try (RegistryCli cli = new RegistryCli("nomulus", COMMAND_MAP)) {
cli.run(args);
}
RegistryCli cli = new RegistryCli("nomulus", COMMAND_MAP);
cli.run(args);
}
}
@@ -42,7 +42,6 @@ import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.tools.javascrap.CompareEscrowDepositsCommand;
import google.registry.tools.javascrap.CreateCancellationsForOneTimesCommand;
import google.registry.tools.javascrap.CreateSyntheticDomainHistoriesCommand;
import google.registry.util.UtilsModule;
import google.registry.whois.NonCachingWhoisModule;
import javax.annotation.Nullable;
@@ -106,8 +105,6 @@ interface RegistryToolComponent {
void inject(CreateRegistrarCommand command);
void inject(CreateSyntheticDomainHistoriesCommand command);
void inject(CreateTldCommand command);
void inject(EncryptEscrowDepositCommand command);
@@ -176,7 +173,7 @@ interface RegistryToolComponent {
void inject(WhoisQueryCommand command);
AppEngineConnection appEngineConnection();
ServiceConnection serviceConnection();
@LocalCredentialJson
String googleCredentialJson();
@@ -106,7 +106,7 @@ public enum RegistryToolEnvironment {
// XXX: Kludge to stop processing once we reach what is *probably* the command name.
// This is necessary in order to allow commands to accept arguments named '-e'.
// This code should probably be updated, should any zero arity flags be added to
// RegistryCli, LoggingParameters, or AppEngineConnection.
// RegistryCli, LoggingParameters, or ServiceConnection.
if (args[i].startsWith("-")) {
expecting = !args[i].contains("=");
} else {
@@ -1,44 +0,0 @@
// Copyright 2018 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.tools;
import static com.google.common.base.Preconditions.checkState;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import java.io.InputStream;
import java.lang.reflect.Method;
/**
* Provides a method to access {@link RemoteApiOptions#useGoogleCredentialStream(InputStream)},
* which is a package private method.
*
* <p>This is obviously a hack, but until that method is exposed, we have to do this to set up the
* {@link RemoteApiOptions} with a JSON representing a user credential.
*/
public class RemoteApiOptionsUtil {
public static RemoteApiOptions useGoogleCredentialStream(
RemoteApiOptions options, InputStream stream) throws Exception {
Method method =
options.getClass().getDeclaredMethod("useGoogleCredentialStream", InputStream.class);
checkState(
!method.isAccessible(),
"RemoteApiOptoins#useGoogleCredentialStream(InputStream) is accessible."
+ " Stop using RemoteApiOptionsUtil.");
method.setAccessible(true);
method.invoke(options, stream);
method.setAccessible(false);
return options;
}
}
@@ -26,7 +26,7 @@ import javax.inject.Inject;
/** Command to send ICANN notification that an escrow deposit was uploaded. */
@Parameters(separators = " =", commandDescription = "Send an ICANN report of an uploaded deposit.")
final class SendEscrowReportToIcannCommand implements CommandWithRemoteApi {
final class SendEscrowReportToIcannCommand implements Command {
@Parameter(
description = "One or more foo-report.xml files.",
@@ -43,12 +43,12 @@ import javax.inject.Inject;
import org.json.simple.JSONValue;
/**
* An http connection to an appengine server.
* An HTTP connection to a service.
*
* <p>By default - connects to the TOOLS service. To create a Connection to another service, call
* the {@link #withService} function.
*/
public class AppEngineConnection {
public class ServiceConnection {
/** Pattern to heuristically extract title tag contents in HTML responses. */
private static final Pattern HTML_TITLE_TAG_PATTERN = Pattern.compile("<title>(.*?)</title>");
@@ -57,18 +57,18 @@ public class AppEngineConnection {
private final Service service;
@Inject
AppEngineConnection() {
ServiceConnection() {
service = Service.TOOLS;
}
private AppEngineConnection(Service service, HttpRequestFactory requestFactory) {
private ServiceConnection(Service service, HttpRequestFactory requestFactory) {
this.service = service;
this.requestFactory = requestFactory;
}
/** Returns a copy of this connection that talks to a different service. */
public AppEngineConnection withService(Service service) {
return new AppEngineConnection(service, requestFactory);
public ServiceConnection withService(Service service) {
return new ServiceConnection(service, requestFactory);
}
/** Returns the contents of the title tag in the given HTML, or null if not found. */
@@ -30,8 +30,7 @@ import org.joda.time.DateTime;
@Parameters(
separators = " =",
commandDescription = "Set the current database migration state schedule.")
public class SetDatabaseMigrationStateCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
public class SetDatabaseMigrationStateCommand extends ConfirmingCommand {
private static final String WARNING_MESSAGE =
"Attempting to change the schedule with an effect that would take place within the next 10 "
@@ -39,7 +39,7 @@ import javax.inject.Inject;
commandDescription =
"Set the number of instances for a given service and version. "
+ "Note that this command only works for manual scaling service.")
final class SetNumInstancesCommand implements CommandWithRemoteApi {
final class SetNumInstancesCommand implements Command {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -35,7 +35,7 @@ import javax.inject.Named;
/** Composite command to set up OT&E TLDs and accounts. */
@Parameters(separators = " =", commandDescription = "Set up OT&E TLDs and registrars")
final class SetupOteCommand extends ConfirmingCommand implements CommandWithRemoteApi {
final class SetupOteCommand extends ConfirmingCommand {
private static final int PASSWORD_LENGTH = 16;
@@ -58,7 +58,7 @@ import org.joda.time.DateTime;
*/
@Parameters(separators = " =", commandDescription = "Unrenew a domain.")
@NonFinalForTesting
class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemoteApi {
class UnrenewDomainCommand extends ConfirmingCommand {
@Parameter(
names = {"-p", "--period"},
@@ -29,7 +29,7 @@ import org.joda.time.DateTime;
/** Modifies {@link Cursor} timestamps used by locking rolling cursor tasks, like in RDE. */
@Parameters(separators = " =", commandDescription = "Modifies cursor timestamps used by LRC tasks")
final class UpdateCursorsCommand extends ConfirmingCommand implements CommandWithRemoteApi {
final class UpdateCursorsCommand extends ConfirmingCommand implements Command {
@Parameter(description = "TLDs on which to operate. Omit for global cursors.")
private List<String> tlds;
@@ -31,7 +31,7 @@ import javax.inject.Inject;
* Command to set and update ASCII-armored secret from the active {@code Keyring} implementation.
*/
@Parameters(separators = " =", commandDescription = "Update values of secret in the keyring.")
final class UpdateKeyringSecretCommand implements CommandWithRemoteApi {
final class UpdateKeyringSecretCommand implements Command {
@Inject SecretManagerKeyringUpdater secretManagerKeyringUpdater;
@@ -26,8 +26,7 @@ import google.registry.persistence.VKey;
import java.util.List;
/** Shared base class for commands to update or delete allocation tokens. */
abstract class UpdateOrDeleteAllocationTokensCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
abstract class UpdateOrDeleteAllocationTokensCommand extends ConfirmingCommand {
@Parameter(
names = {"-p", "--prefix"},
@@ -31,7 +31,7 @@ import java.util.List;
/** A command to upload a {@link ClaimsList}. */
@Parameters(separators = " =", commandDescription = "Manually upload a new claims list file")
final class UploadClaimsListCommand extends ConfirmingCommand implements CommandWithRemoteApi {
final class UploadClaimsListCommand extends ConfirmingCommand {
@Parameter(description = "Claims list filename")
private List<String> mainParameters = new ArrayList<>();
@@ -35,7 +35,7 @@ import javax.inject.Inject;
/** A command to test registrar login credentials. */
@Parameters(separators = " =", commandDescription = "Test registrar login credentials")
final class ValidateLoginCredentialsCommand implements CommandWithRemoteApi {
final class ValidateLoginCredentialsCommand implements Command {
@Parameter(
names = {"-c", "--client"},
@@ -45,7 +45,7 @@ import java.util.Objects;
@Parameters(
separators = " =",
commandDescription = "Verify passage of OT&E for specified (or all) registrars")
final class VerifyOteCommand implements CommandWithConnection, CommandWithRemoteApi {
final class VerifyOteCommand implements CommandWithConnection {
@Parameter(
description = "List of registrar names to check; must be the same names as the ones used "
@@ -62,10 +62,10 @@ final class VerifyOteCommand implements CommandWithConnection, CommandWithRemote
description = "Only show a summary of information")
private boolean summarize;
private AppEngineConnection connection;
private ServiceConnection connection;
@Override
public void setConnection(AppEngineConnection connection) {
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@@ -23,7 +23,7 @@ import javax.inject.Inject;
/** Command to execute a WHOIS query. */
@Parameters(separators = " =", commandDescription = "Manually perform a WHOIS query")
final class WhoisQueryCommand implements CommandWithRemoteApi {
final class WhoisQueryCommand implements Command {
@Parameter(
description = "WHOIS query string",
@@ -24,7 +24,6 @@ import google.registry.model.billing.BillingEvent.Cancellation;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.tools.CommandWithRemoteApi;
import google.registry.tools.ConfirmingCommand;
import google.registry.tools.params.LongParameter;
import java.util.List;
@@ -37,8 +36,7 @@ import java.util.List;
* refunds after the fact.
*/
@Parameters(separators = " =", commandDescription = "Manually create Cancellations for OneTimes.")
public class CreateCancellationsForOneTimesCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
public class CreateCancellationsForOneTimesCommand extends ConfirmingCommand {
@Parameter(
description = "Space-delimited billing event ID(s) to cancel",
@@ -1,209 +0,0 @@
// Copyright 2022 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.tools.javascrap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameters;
import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.domain.Domain;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.tools.AppEngineConnection;
import google.registry.tools.CommandWithConnection;
import google.registry.tools.CommandWithRemoteApi;
import google.registry.tools.ConfirmingCommand;
import google.registry.tools.RemoteApiOptionsUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* Command that creates an additional synthetic history object for domains.
*
* <p>This is created to fix the issue identified in b/248112997. After b/245940594, there were some
* domains where the most recent history object did not represent the state of the domain as it
* exists in the world. Because RDE loads only from DomainHistory objects, this means that RDE was
* producing wrong data. This command mitigates that issue by creating synthetic history events for
* every domain that was not deleted as of the start of the bad {@link
* google.registry.beam.resave.ResaveAllEppResourcesPipeline} -- then, we can guarantee that this
* new history object represents the state of the domain as far as we know.
*
* <p>A previous run of this command (in pipeline form) attempted to do this and succeeded in most
* cases. Unfortunately, that pipeline had an issue where it used self-allocated IDs for some of the
* dependent objects (e.g. {@link google.registry.model.domain.secdns.DomainDsDataHistory}). As a
* result, we want to run this again as a command using Datastore-allocated IDs to re-create
* synthetic history objects for any domain whose last history object is one of the
* potentially-incorrect synthetic objects.
*
* <p>We further restrict the domains to domains whose latest history object is before October 4.
* This is an arbitrary date that is suitably far after the previous incorrect run of this synthetic
* history pipeline, with the purpose of making future runs of this command idempotent (in case the
* command fails, we can just run it again and again).
*/
@Parameters(
separators = " =",
commandDescription = "Create synthetic domain history objects to fix RDE.")
public class CreateSyntheticDomainHistoriesCommand extends ConfirmingCommand
implements CommandWithRemoteApi, CommandWithConnection {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String HISTORY_REASON =
"Create synthetic domain histories to fix RDE for b/248112997";
private static final DateTime BAD_PIPELINE_END_TIME = DateTime.parse("2022-09-10T12:00:00.000Z");
private static final DateTime NEW_SYNTHETIC_ROUND_START =
DateTime.parse("2022-10-04T00:00:00.000Z");
private static final ExecutorService executor = Executors.newFixedThreadPool(20);
private static final AtomicInteger numDomainsProcessed = new AtomicInteger();
private AppEngineConnection connection;
@Inject
@Config("registryAdminClientId")
String registryAdminRegistrarId;
@Inject @CredentialModule.LocalCredentialJson String localCredentialJson;
private final ThreadLocal<RemoteApiInstaller> installerThreadLocal =
ThreadLocal.withInitial(this::createInstaller);
private ImmutableSet<String> domainRepoIds;
@Override
protected String prompt() {
jpaTm()
.transact(
() -> {
domainRepoIds =
jpaTm()
.query(
"SELECT dh.domainRepoId FROM DomainHistory dh JOIN Tld t ON t.tldStr ="
+ " dh.domainBase.tld WHERE t.tldType = 'REAL' AND dh.type ="
+ " 'SYNTHETIC' AND dh.modificationTime > :badPipelineEndTime AND"
+ " dh.modificationTime < :newSyntheticRoundStart AND"
+ " (dh.domainRepoId, dh.modificationTime) IN (SELECT domainRepoId,"
+ " MAX(modificationTime) FROM DomainHistory GROUP BY domainRepoId)",
String.class)
.setParameter("badPipelineEndTime", BAD_PIPELINE_END_TIME)
.setParameter("newSyntheticRoundStart", NEW_SYNTHETIC_ROUND_START)
.getResultStream()
.collect(toImmutableSet());
});
return String.format(
"Attempt to create synthetic history entries for %d domains?", domainRepoIds.size());
}
@Override
protected String execute() throws Exception {
List<Future<?>> futures = new ArrayList<>();
for (String domainRepoId : domainRepoIds) {
futures.add(
executor.submit(
() -> {
// Make sure the remote API is installed for ID generation
installerThreadLocal.get();
jpaTm()
.transact(
() -> {
Domain domain =
jpaTm().loadByKey(VKey.create(Domain.class, domainRepoId));
jpaTm()
.put(
HistoryEntry.createBuilderForResource(domain)
.setRegistrarId(registryAdminRegistrarId)
.setBySuperuser(true)
.setRequestedByRegistrar(false)
.setModificationTime(jpaTm().getTransactionTime())
.setReason(HISTORY_REASON)
.setType(HistoryEntry.Type.SYNTHETIC)
.build());
});
int numProcessed = numDomainsProcessed.incrementAndGet();
if (numProcessed % 1000 == 0) {
System.out.printf("Saved histories for %d domains%n", numProcessed);
}
return null;
}));
}
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error");
}
}
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
return String.format("Saved entries for %d domains", numDomainsProcessed.get());
}
@Override
public void setConnection(AppEngineConnection connection) {
this.connection = connection;
}
/**
* Installs the remote API so that the worker threads can use Datastore for ID generation.
*
* <p>Lifted from the RegistryCli class
*/
private RemoteApiInstaller createInstaller() {
RemoteApiInstaller installer = new RemoteApiInstaller();
RemoteApiOptions options = new RemoteApiOptions();
options.server(connection.getServer().getHost(), getPort(connection.getServer()));
if (RegistryConfig.areServersLocal()) {
// Use dev credentials for localhost.
options.useDevelopmentServerCredential();
} else {
try {
RemoteApiOptionsUtil.useGoogleCredentialStream(
options, new ByteArrayInputStream(localCredentialJson.getBytes(UTF_8)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
try {
installer.install(options);
} catch (IOException e) {
throw new RuntimeException(e);
}
ObjectifyService.initOfy();
return installer;
}
private static int getPort(URL url) {
return url.getPort() == -1 ? url.getDefaultPort() : url.getPort();
}
}

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