1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

19 Commits

Author SHA1 Message Date
Ben McIlwain ef9f3aeada Remove a couple unused variables (#913)
* Remove a couple unused variables
2020-12-23 17:19:02 -05:00
Shicong Huang 9c43aab8cd Convert HostCreateFlow and HostCheckFlow to tm() (#910) 2020-12-22 21:02:02 -05:00
Weimin Yu cb63c3dd80 Add unique constraints on domain_hosts (#911)
* Add unique constraints on domain_hosts

Add unique constraints on DomainHost (child of DomainBase) and
DomainHistoryHost (child of DomainHistory). DomainHost is non-entity
embedded object and Hibernate does not define indexes automatically.

This should improve read and write performance of the parent entities.
2020-12-21 18:22:24 -05:00
Ben McIlwain 2cf190e448 Add a fast mode to the ResaveAllEppResourcesAction mapreduce (#912)
* Add a fast mode to the ResaveAllEppResourcesAction mapreduce

This new mode avoids writing no-op mutations for entities that don't actually
have any changes to write. The cronjobs use fast mode by default, but manual
invocations do not, as manual invocations are often used to trigger @OnLoad
migrations, and fast mode won't pick up on those changes.
2020-12-21 18:07:59 -05:00
gbrodman e550c94cbc Convert AllocationToken-related classes to tm() (#909)
* Convert AllocationToken-related classes to tm()

For the most part this is a fairly simple converstion -- changing Key
references to VKey references, using JPA transactions when necessary,
and using the TransactionManager interface. There's a bit of cleanup too
in related code
2020-12-21 15:56:49 -05:00
Shicong Huang 6e2bbd1a7e Add BillingVKey to restore symmetric VKey in GracePeriodBase (#902)
* Use PollMessageVKey to replace VKey<PollMessage> in DomainBase

* Revert changes to DomainContent

* Use BillingVKey in GracePeriodBase to restore symmetric vkey

* Rebase on HEAD
2020-12-17 14:13:47 -05:00
Weimin Yu 495d7176d8 Validate SQL credentials in Secret Manager (#907)
* Validate SQL credentials in Secret Manager

Load SQL credentials from the SecretManager and compare them with the
ones currently in use in Nomulus server, beam pipeline, and the registry
tool. Normal operations are not affected by failures related to the
SecretManager, be it IOException, insufficient permission , or wrong or
missing credential.

The appengine and compute engine default service accounts must be
granted the permission to access the secret data. In the short term, we
will grant the secretmanager.secretAccessor role to these accounts. In
the long term, with the proposed privilege service, access will be granted
on per-secret basis.
2020-12-16 10:57:03 -05:00
Michael Muller d7aab524e5 Make config/presubmits.py use explicit encodings (#908)
For some reason, our docker build image has started using a non-utf8 default
encoding.  Specify the encoding explicitly on python "open()" to override.

Note that this might not entirely fix the build: it's possible that this
problem may affect other portions of the build.
2020-12-16 10:03:32 -05:00
sarahcaseybot c5bfe31b73 Modify SignedMarkRevocationList to throw Cloud SQL failures in unit tests (#898)
* Modify SignedMarkRevocationList to not swallow CloudSQL failures in unittests

* restore package-lock.json

* Added suppressExceptionUnlessInTest()

* Add a DatabaseMigrationUtils class

* small changes
2020-12-15 17:34:38 -05:00
sarahcaseybot 9975bc2195 Modify proxy to pass full certificate before login (#896)
* Modify proxy to pass full certificate until partner is logged in

* refactor tests

* revert package-lock.json

* add sample cert string to tests
2020-12-15 16:36:39 -05:00
gbrodman cb16a7649f Add a scrap command to backfill Spec11 threats (#897)
This parses through all pre-existing Spec11 files in GCS (starting at
2019-01-01 which is basically when the new format started) and maps them
to the new Spec11ThreatMatch objects.

Because the old format stored domain names only and the new format stores
names + repo IDs, we need to retrieve the DomainBase objects from the
point in time of the scan (failing if they don't exist). Because the
same domains appear multiple times (we estimate a total of 100k+ entries
but only 1-2k unique domains) we cache the DomainBase objects that we
retrieve from Datastore.
2020-12-15 16:18:27 -05:00
Michael Muller d7e2b24468 Allow disabling UpdateAutoTimestamp updates (#906)
* Allow disabling UpdateAutoTimestamp updates

Allow us to disable timestamp updates within a try-with-resources block for a
given thread.  This functionality will be needed for transaction replays both
to and from datastore.

As part of this, also upgrade the UpdateAutoTimestampTest to a
DualDatabaseTest so we can verify that the functionality works both on
Datastore and Cloud SQL.
2020-12-15 10:34:52 -05:00
gbrodman 7c364b4471 Add SetSqlReplayCheckpoint command for SQL replay (#895)
* Add SetSqlReplayCheckpoint command for SQL replay

We should set this to the same time that we initially populate the SQL
database from Datastore.
2020-12-11 17:41:06 -05:00
Shicong Huang b5137c3d05 Convert HostResourceTest to work with Cloud SQL (#905) 2020-12-11 13:17:55 -05:00
Ben McIlwain 6a9929019a Use 10 workers instead of the default 100 for re-save all EPP resources (#904)
* Use 10 workers instead of the default 100 for re-save all EPP resources

The intended/desired effect is to have a larger number of GCS commit log diffs
spread out over a longer period of time, with each diff itself being
significantly smaller. This should retain roughly the same amount of total work
for the async Cloud SQL replication action to have to deal with, but spread
across 10X as much time.
2020-12-10 15:25:25 -05:00
Weimin Yu 83ed448741 Add a credential store backed by Secret Manager (#901)
* Add a credential store backed by Secret Manager

Added a SqlCredentialStore that stores user credentials with one level
of indirection: for each credential, an addtional secret is used to
identify the 'live' version of the credential. This is a work in
progress and the overall design is explained in
go/dr-sql-security.

Also added two nomulus commands for credential management. They are
stop-gap measures that will be deprecated by the planned privilege
management system.
2020-12-10 11:29:44 -05:00
gbrodman 2c6ee6dae9 Parameterize the serialization of objects being written to SQL (#892)
* Parameterize the serialization of objects being written to SQL

We shouldn't require that objects written to SQL during a Beam pipeline
be VersionedEntity objects -- they may be non-Objectify entities. As a
result, we should allow the user to specify what the objects are that
should be written to SQL.

Note: we will need to clean up the Spec11PipelineTest more but that can
be out of the scope of this PR.

* Overload the method and add a bit of javadoc

* Actually use the overloaded function
2020-12-09 15:52:56 -05:00
Shicong Huang a181d6a720 Add a command to remove Registry 1.0 key in DomainBase (#900) 2020-12-09 10:30:23 -05:00
Michael Muller db19f9ea4f Resurrect symmetric vkeys when possible. (#899)
* Resurrect symmetric vkeys when possible.

AbstractVKeyConverter had never been updated to generate symmetric VKeys for
simple (i.e. non-composite) VKey types.  Update it to default to generating a
VKey with a simple Ofy key.

With this change, composite VKeys must specify the "compositeKey = true" field
in the With*VKey annotations.  Doing so creates an asymmetric (SQL only) VKey
that can triggers higher-level logic to generate a corrected VKey containing
the composite Key.
2020-12-08 15:13:44 -05:00
123 changed files with 7683 additions and 5175 deletions
+2 -2
View File
@@ -65,7 +65,7 @@ class PresubmitCheck:
for pattern in self.skipped_patterns:
if pattern in file:
return False
with open(file, "r") as f:
with open(file, "r", encoding='utf8') as f:
file_content = f.read()
matches = re.match(self.regex, file_content, re.DOTALL)
if self.regex_type == FORBIDDEN:
@@ -241,7 +241,7 @@ def verify_flyway_index():
# Remove the sequence numbers and compare against the index file contents.
files = [filename[1] for filename in sorted(files)]
with open('db/src/main/resources/sql/flyway.txt') as index:
with open('db/src/main/resources/sql/flyway.txt', encoding='utf8') as index:
indexed_files = index.read().splitlines()
if files != indexed_files:
unindexed = set(files) - set(indexed_files)
@@ -14,6 +14,7 @@
package google.registry.batch;
import static google.registry.mapreduce.MapreduceRunner.PARAM_FAST;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -24,6 +25,7 @@ import google.registry.mapreduce.MapreduceRunner;
import google.registry.mapreduce.inputs.EppResourceInputs;
import google.registry.model.EppResource;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import javax.inject.Inject;
@@ -39,6 +41,14 @@ import javax.inject.Inject;
* <p>Because there are no auth settings in the {@link Action} annotation, this command can only be
* run internally, or by pretending to be internal by setting the X-AppEngine-QueueName header,
* which only admin users can do.
*
* <p>If the <code>?fast=true</code> querystring parameter is passed, then entities that are not
* changed by {@link EppResource#cloneProjectedAtTime} will not be re-saved. This helps prevent
* mutation load on the DB and has the beneficial side effect of writing out smaller commit logs.
* Note that this does NOT pick up mutations caused by migrations using the {@link
* com.googlecode.objectify.annotation.OnLoad} annotation, so if you are running a one-off schema
* migration, do not use fast mode. Fast mode defaults to false for this reason, but is used by the
* monthly invocation of the mapreduce.
*/
@Action(
service = Action.Service.BACKEND,
@@ -48,15 +58,31 @@ public class ResaveAllEppResourcesAction implements Runnable {
@Inject MapreduceRunner mrRunner;
@Inject Response response;
@Inject ResaveAllEppResourcesAction() {}
@Inject
@Parameter(PARAM_FAST)
boolean isFast;
@Inject
ResaveAllEppResourcesAction() {}
/**
* The number of shards to run the map-only mapreduce on.
*
* <p>This is less than the default of 100 because we only run this action monthly and can afford
* it being slower, but we don't want to write out lots of large commit logs in a short period of
* time because they make the Cloud SQL migration tougher.
*/
private static final int NUM_SHARDS = 10;
@Override
public void run() {
mrRunner
.setJobName("Re-save all EPP resources")
.setModuleName("backend")
.setDefaultMapShards(NUM_SHARDS)
.runMapOnly(
new ResaveAllEppResourcesActionMapper(),
new ResaveAllEppResourcesActionMapper(isFast),
ImmutableList.of(EppResourceInputs.createKeyInput(EppResource.class)))
.sendLinkToMapreduceConsole(response);
}
@@ -66,23 +92,33 @@ public class ResaveAllEppResourcesAction implements Runnable {
extends Mapper<Key<EppResource>, Void, Void> {
private static final long serialVersionUID = -7721628665138087001L;
public ResaveAllEppResourcesActionMapper() {}
private final boolean isFast;
ResaveAllEppResourcesActionMapper(boolean isFast) {
this.isFast = isFast;
}
@Override
public final void map(final Key<EppResource> resourceKey) {
tm()
.transact(
() -> {
EppResource projectedResource =
ofy()
.load()
.key(resourceKey)
.now()
.cloneProjectedAtTime(tm().getTransactionTime());
ofy().save().entity(projectedResource).now();
});
getContext().incrementCounter(String.format("%s entities re-saved", resourceKey.getKind()));
boolean resaved =
tm().transact(
() -> {
EppResource originalResource = ofy().load().key(resourceKey).now();
EppResource projectedResource =
originalResource.cloneProjectedAtTime(tm().getTransactionTime());
if (isFast && originalResource.equals(projectedResource)) {
return false;
} else {
ofy().save().entity(projectedResource).now();
return true;
}
});
getContext()
.incrementCounter(
String.format(
"%s entities %s",
resourceKey.getKind(), resaved ? "re-saved" : "with no changes skipped"));
}
}
}
@@ -31,6 +31,7 @@ import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.JdbcJpaTm;
import google.registry.persistence.PersistenceModule.SocketFactoryJpaTm;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.util.UtilsModule;
import java.io.BufferedReader;
import java.io.IOException;
@@ -168,6 +169,7 @@ public class BeamJpaModule {
BeamJpaModule.class,
KmsModule.class,
PersistenceModule.class,
SecretManagerModule.class,
UtilsModule.class
})
public interface JpaTransactionManagerComponent {
@@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static google.registry.beam.initsql.BackupPaths.getCommitLogTimestamp;
import static google.registry.beam.initsql.BackupPaths.getExportFilePatterns;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.JpaRetries.isFailedTxnRetriable;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.setJpaTm;
@@ -42,7 +43,6 @@ import google.registry.backup.CommitLogImports;
import google.registry.backup.VersionedEntity;
import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.ofy.Ofy;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.tools.LevelDbLogReader;
import google.registry.util.SystemSleeper;
@@ -68,6 +68,7 @@ import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.ProcessFunction;
import org.apache.beam.sdk.transforms.SerializableFunction;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PBegin;
import org.apache.beam.sdk.values.PCollection;
@@ -264,9 +265,9 @@ public final class Transforms {
}
/**
* Returns a {@link PTransform} that writes a {@link PCollection} of entities to a SQL database.
* and outputs an empty {@code PCollection<Void>}. This allows other operations to {@link
* org.apache.beam.sdk.transforms.Wait wait} for the completion of this transform.
* Returns a {@link PTransform} that writes a {@link PCollection} of {@link VersionedEntity}s to a
* SQL database. and outputs an empty {@code PCollection<Void>}. This allows other operations to
* {@link org.apache.beam.sdk.transforms.Wait wait} for the completion of this transform.
*
* <p>Errors are handled according to the pipeline runner's default policy. As part of a one-time
* job, we will not add features unless proven necessary.
@@ -282,16 +283,53 @@ public final class Transforms {
int maxWriters,
int batchSize,
SerializableSupplier<JpaTransactionManager> jpaSupplier) {
return new PTransform<PCollection<VersionedEntity>, PCollection<Void>>() {
return writeToSql(
transformId,
maxWriters,
batchSize,
jpaSupplier,
(e) -> ofy().toPojo(e.getEntity().get()),
TypeDescriptor.of(VersionedEntity.class));
}
/**
* Returns a {@link PTransform} that writes a {@link PCollection} of entities to a SQL database.
* and outputs an empty {@code PCollection<Void>}. This allows other operations to {@link
* org.apache.beam.sdk.transforms.Wait wait} for the completion of this transform.
*
* <p>The converter and type descriptor are generics so that we can convert any type of entity to
* an object to be placed in SQL.
*
* <p>Errors are handled according to the pipeline runner's default policy. As part of a one-time
* job, we will not add features unless proven necessary.
*
* @param transformId a unique ID for an instance of the returned transform
* @param maxWriters the max number of concurrent writes to SQL, which also determines the max
* number of connection pools created
* @param batchSize the number of entities to write in each operation
* @param jpaSupplier supplier of a {@link JpaTransactionManager}
* @param jpaConverter the function that converts the input object to a JPA entity
* @param objectDescriptor the type descriptor of the input object
*/
public static <T> PTransform<PCollection<T>, PCollection<Void>> writeToSql(
String transformId,
int maxWriters,
int batchSize,
SerializableSupplier<JpaTransactionManager> jpaSupplier,
SerializableFunction<T, Object> jpaConverter,
TypeDescriptor<T> objectDescriptor) {
return new PTransform<PCollection<T>, PCollection<Void>>() {
@Override
public PCollection<Void> expand(PCollection<VersionedEntity> input) {
public PCollection<Void> expand(PCollection<T> input) {
return input
.apply(
"Shard data for " + transformId,
MapElements.into(kvs(integers(), TypeDescriptor.of(VersionedEntity.class)))
MapElements.into(kvs(integers(), objectDescriptor))
.via(ve -> KV.of(ThreadLocalRandom.current().nextInt(maxWriters), ve)))
.apply("Batch output by shard " + transformId, GroupIntoBatches.ofSize(batchSize))
.apply("Write in batch for " + transformId, ParDo.of(new SqlBatchWriter(jpaSupplier)));
.apply(
"Write in batch for " + transformId,
ParDo.of(new SqlBatchWriter<T>(jpaSupplier, jpaConverter)));
}
};
}
@@ -385,18 +423,21 @@ public final class Transforms {
* to hold the {@code JpaTransactionManager} instance, we must ensure that JpaTransactionManager
* is not changed or torn down while being used by some instance.
*/
private static class SqlBatchWriter extends DoFn<KV<Integer, Iterable<VersionedEntity>>, Void> {
private static class SqlBatchWriter<T> extends DoFn<KV<Integer, Iterable<T>>, Void> {
private static int instanceCount = 0;
private static JpaTransactionManager originalJpa;
private final SerializableSupplier<JpaTransactionManager> jpaSupplier;
private final SerializableFunction<T, Object> jpaConverter;
private transient Ofy ofy;
private transient SystemSleeper sleeper;
SqlBatchWriter(SerializableSupplier<JpaTransactionManager> jpaSupplier) {
SqlBatchWriter(
SerializableSupplier<JpaTransactionManager> jpaSupplier,
SerializableFunction<T, Object> jpaConverter) {
this.jpaSupplier = jpaSupplier;
this.jpaConverter = jpaConverter;
}
@Setup
@@ -405,7 +446,6 @@ public final class Transforms {
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
ObjectifyService.initOfy();
ofy = ObjectifyService.ofy();
}
synchronized (SqlBatchWriter.class) {
@@ -429,13 +469,11 @@ public final class Transforms {
}
@ProcessElement
public void processElement(@Element KV<Integer, Iterable<VersionedEntity>> kv) {
public void processElement(@Element KV<Integer, Iterable<T>> kv) {
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
ImmutableList<Object> ofyEntities =
Streams.stream(kv.getValue())
.map(VersionedEntity::getEntity)
.map(Optional::get)
.map(ofy::toPojo)
.map(this.jpaConverter::apply)
.collect(ImmutableList.toImmutableList());
retry(() -> jpaTm().transact(() -> jpaTm().putAll(ofyEntities)));
}
@@ -20,7 +20,7 @@ import static google.registry.beam.BeamUtils.getQueryFromFile;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import google.registry.backup.AppEngineEnvironment;
import google.registry.beam.initsql.Transforms;
import google.registry.beam.initsql.Transforms.SerializableSupplier;
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
import google.registry.config.CredentialModule.LocalCredential;
@@ -43,7 +43,6 @@ import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.ValueProvider;
import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.ParDo;
@@ -191,34 +190,27 @@ public class Spec11Pipeline implements Serializable {
PCollection<Subdomain> domains,
EvaluateSafeBrowsingFn evaluateSafeBrowsingFn,
ValueProvider<String> dateProvider) {
PCollection<KV<Subdomain, ThreatMatch>> subdomainsSql =
domains.apply("Run through SafeBrowsing API", ParDo.of(evaluateSafeBrowsingFn));
/* Store ThreatMatch objects in SQL. */
TypeDescriptor<KV<Subdomain, ThreatMatch>> descriptor =
new TypeDescriptor<KV<Subdomain, ThreatMatch>>() {};
subdomainsSql.apply(
ParDo.of(
new DoFn<KV<Subdomain, ThreatMatch>, Void>() {
@ProcessElement
public void processElement(ProcessContext context) {
// create the Spec11ThreatMatch from Subdomain and ThreatMatch
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
Subdomain subdomain = context.element().getKey();
Spec11ThreatMatch threatMatch =
new Spec11ThreatMatch.Builder()
.setThreatTypes(
ImmutableSet.of(
ThreatType.valueOf(context.element().getValue().threatType())))
.setCheckDate(
LocalDate.parse(dateProvider.get(), ISODateTimeFormat.date()))
.setDomainName(subdomain.domainName())
.setDomainRepoId(subdomain.domainRepoId())
.setRegistrarId(subdomain.registrarId())
.build();
JpaTransactionManager jpaTransactionManager = jpaSupplierFactory.get();
jpaTransactionManager.transact(() -> jpaTransactionManager.insert(threatMatch));
}
}
}));
Transforms.writeToSql(
"Spec11ThreatMatch",
4,
4,
jpaSupplierFactory,
(kv) -> {
Subdomain subdomain = kv.getKey();
return new Spec11ThreatMatch.Builder()
.setThreatTypes(ImmutableSet.of(ThreatType.valueOf(kv.getValue().threatType())))
.setCheckDate(LocalDate.parse(dateProvider.get(), ISODateTimeFormat.date()))
.setDomainName(subdomain.domainName())
.setDomainRepoId(subdomain.domainRepoId())
.setRegistrarId(subdomain.registrarId())
.build();
},
descriptor));
/* Store ThreatMatch objects in JSON. */
PCollection<KV<Subdomain, ThreatMatch>> subdomainsJson =
@@ -415,6 +415,14 @@ public final class RegistryConfig {
return config.cloudSql.instanceConnectionName;
}
@Provides
@Config("cloudSqlDbInstanceName")
public static String providesCloudSqlDbInstance(RegistryConfigSettings config) {
// Format of instanceConnectionName: project-id:region:instance-name
int lastColonIndex = config.cloudSql.instanceConnectionName.lastIndexOf(':');
return config.cloudSql.instanceConnectionName.substring(lastColonIndex + 1);
}
@Provides
@Config("cloudDnsRootUrl")
public static Optional<String> getCloudDnsRootUrl(RegistryConfigSettings config) {
@@ -80,7 +80,7 @@
</cron>
<cron>
<url><![CDATA[/_dr/task/resaveAllEppResources]]></url>
<url><![CDATA[/_dr/task/resaveAllEppResources?fast=true]]></url>
<description>
This job resaves all our resources, projected in time to "now".
It is needed for "deleteOldCommitLogs" to work correctly.
@@ -103,7 +103,7 @@
</cron>
<cron>
<url><![CDATA[/_dr/task/resaveAllEppResources]]></url>
<url><![CDATA[/_dr/task/resaveAllEppResources?fast=true]]></url>
<description>
This job resaves all our resources, projected in time to "now".
It is needed for "deleteOldCommitLogs" to work correctly.
@@ -21,7 +21,7 @@
</cron>
<cron>
<url><![CDATA[/_dr/task/resaveAllEppResources]]></url>
<url><![CDATA[/_dr/task/resaveAllEppResources?fast=true]]></url>
<description>
This job resaves all our resources, projected in time to "now".
It is needed for "deleteOldCommitLogs" to work correctly.
@@ -87,7 +87,7 @@
</cron>
<cron>
<url><![CDATA[/_dr/task/resaveAllEppResources]]></url>
<url><![CDATA[/_dr/task/resaveAllEppResources?fast=true]]></url>
<description>
This job resaves all our resources, projected in time to "now".
It is needed for "deleteOldCommitLogs" to work correctly.
@@ -110,7 +110,6 @@ import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.persistence.DomainHistoryVKey;
import google.registry.persistence.VKey;
import google.registry.tmch.LordnTaskUtils;
import java.util.Optional;
@@ -372,7 +371,7 @@ public class DomainCreateFlow implements TransactionalFlow {
&& TokenType.SINGLE_USE.equals(allocationToken.get().getTokenType())) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
allocationToken.get(), DomainHistoryVKey.create(Key.create(historyEntry))));
allocationToken.get(), HistoryEntry.createVKey(Key.create(historyEntry))));
}
enqueueTasks(newDomain, hasSignedMarks, hasClaimsNotice);
@@ -15,14 +15,13 @@
package google.registry.flows.domain.token;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.net.InternetDomainName;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
@@ -32,7 +31,8 @@ import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.registry.Registry;
import google.registry.persistence.DomainHistoryVKey;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
@@ -107,7 +107,7 @@ public class AllocationTokenFlowUtils {
/** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */
public AllocationToken redeemToken(
AllocationToken token, DomainHistoryVKey redemptionHistoryEntry) {
AllocationToken token, VKey<? extends HistoryEntry> redemptionHistoryEntry) {
checkArgument(
TokenType.SINGLE_USE.equals(token.getTokenType()),
"Only SINGLE_USE tokens can be marked as redeemed");
@@ -152,14 +152,15 @@ public class AllocationTokenFlowUtils {
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new InvalidAllocationTokenException();
}
AllocationToken tokenEntity = ofy().load().key(Key.create(AllocationToken.class, token)).now();
if (tokenEntity == null) {
Optional<AllocationToken> maybeTokenEntity =
tm().maybeLoad(VKey.create(AllocationToken.class, token));
if (maybeTokenEntity.isEmpty()) {
throw new InvalidAllocationTokenException();
}
if (tokenEntity.isRedeemed()) {
if (maybeTokenEntity.get().isRedeemed()) {
throw new AlreadyRedeemedAllocationTokenException();
}
return tokenEntity;
return maybeTokenEntity.get();
}
// Note: exception messages should be <= 32 characters long for domain check results
@@ -21,10 +21,8 @@ import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.union;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
@@ -137,13 +135,11 @@ public final class HostCreateFlow implements TransactionalFlow {
ImmutableSet<ImmutableObject> entitiesToSave =
ImmutableSet.of(
newHost,
historyBuilder.build(),
historyBuilder.build().toChildHistoryEntity(),
ForeignKeyIndex.create(newHost, newHost.getDeletionTime()),
EppResourceIndex.create(Key.create(newHost)));
if (superordinateDomain.isPresent()) {
entitiesToSave =
union(
entitiesToSave,
tm().update(
superordinateDomain
.get()
.asBuilder()
@@ -153,7 +149,7 @@ public final class HostCreateFlow implements TransactionalFlow {
// they are only written as NS records from the referencing domain.
dnsQueue.addHostRefreshTask(targetId);
}
ofy().save().entities(entitiesToSave);
tm().insertAll(entitiesToSave);
return responseBuilder.setResData(HostCreateData.create(targetId, now)).build();
}
@@ -15,6 +15,7 @@
package google.registry.mapreduce;
import static google.registry.mapreduce.MapreduceRunner.PARAM_DRY_RUN;
import static google.registry.mapreduce.MapreduceRunner.PARAM_FAST;
import static google.registry.mapreduce.MapreduceRunner.PARAM_MAP_SHARDS;
import static google.registry.mapreduce.MapreduceRunner.PARAM_REDUCE_SHARDS;
import static google.registry.request.RequestParameters.extractBooleanParameter;
@@ -36,6 +37,12 @@ public final class MapreduceModule {
return extractBooleanParameter(req, PARAM_DRY_RUN);
}
@Provides
@Parameter(PARAM_FAST)
static boolean provideIsFast(HttpServletRequest req) {
return extractBooleanParameter(req, PARAM_FAST);
}
@Provides
@Parameter(PARAM_MAP_SHARDS)
static Optional<Integer> provideMapShards(HttpServletRequest req) {
@@ -55,6 +55,7 @@ public class MapreduceRunner {
public static final String PARAM_DRY_RUN = "dryRun";
public static final String PARAM_MAP_SHARDS = "mapShards";
public static final String PARAM_REDUCE_SHARDS = "reduceShards";
public static final String PARAM_FAST = "fast";
private static final String BASE_URL = "/_dr/mapreduce/";
private static final String QUEUE_NAME = "mapreduce";
@@ -0,0 +1,38 @@
// 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.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
/** Utility methods related to migrating dual-read/dual-write entities. */
public class DatabaseMigrationUtils {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Throws exceptions only in unit tests, otherwise only logs exceptions. */
public static void suppressExceptionUnlessInTest(Runnable work, String message) {
try {
work.run();
} catch (Exception e) {
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)) {
throw e;
}
logger.atWarning().withCause(e).log(message);
}
}
private DatabaseMigrationUtils() {}
}
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.latestOf;
@@ -135,7 +136,7 @@ public final class EppResourceUtils {
useCache
? ForeignKeyIndex.loadCached(clazz, ImmutableList.of(foreignKey), now)
.getOrDefault(foreignKey, null)
: ofy().load().type(ForeignKeyIndex.mapToFkiClass(clazz)).id(foreignKey).now();
: ForeignKeyIndex.load(clazz, foreignKey, now);
// The value of fki.getResourceKey() might be null for hard-deleted prober data.
if (fki == null || isAtOrAfter(now, fki.getDeletionTime()) || fki.getResourceKey() == null) {
return Optional.empty();
@@ -143,7 +144,7 @@ public final class EppResourceUtils {
T resource =
useCache
? EppResource.loadCached(fki.getResourceKey())
: tm().maybeLoad(fki.getResourceKey()).orElse(null);
: transactIfJpaTm(() -> tm().maybeLoad(fki.getResourceKey()).orElse(null));
if (resource == null || isAtOrAfter(now, resource.getDeletionTime())) {
return Optional.empty();
}
@@ -22,12 +22,17 @@ import javax.annotation.Nullable;
import org.joda.time.DateTime;
/**
* A timestamp that auto-updates on each save to Datastore.
* A timestamp that auto-updates on each save to Datastore/Cloud SQL.
*
* @see UpdateAutoTimestampTranslatorFactory
*/
public class UpdateAutoTimestamp extends ImmutableObject {
// When set to true, database converters/translators should do tha auto update. When set to
// false, auto update should be suspended (this exists to allow us to preserve the original value
// during a replay).
private static ThreadLocal<Boolean> autoUpdateEnabled = ThreadLocal.withInitial(() -> true);
DateTime timestamp;
/** Returns the timestamp, or {@code START_OF_TIME} if it's null. */
@@ -40,4 +45,30 @@ public class UpdateAutoTimestamp extends ImmutableObject {
instance.timestamp = timestamp;
return instance;
}
// TODO(b/175610935): Remove the auto-update disabling code below after migration.
/** Class to allow us to safely disable auto-update in a try-with-resources block. */
public static class DisableAutoUpdateResource implements AutoCloseable {
DisableAutoUpdateResource() {
autoUpdateEnabled.set(false);
}
@Override
public void close() {
autoUpdateEnabled.set(true);
}
}
/**
* Resturns a resource that disables auto-updates on all {@link UpdateAutoTimestamp}s in the
* current thread, suitable for use with in a try-with-resources block.
*/
public static DisableAutoUpdateResource disableAutoUpdate() {
return new DisableAutoUpdateResource();
}
public static boolean autoUpdateEnabled() {
return autoUpdateEnabled.get();
}
}
@@ -290,7 +290,7 @@ public abstract class BillingEvent extends ImmutableObject
@javax.persistence.Index(columnList = "allocationToken")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_event_id"))
@WithLongVKey
@WithLongVKey(compositeKey = true)
public static class OneTime extends BillingEvent implements DatastoreAndSqlEntity {
/** The billable value. */
@@ -464,7 +464,7 @@ public abstract class BillingEvent extends ImmutableObject
@javax.persistence.Index(columnList = "recurrence_time_of_year")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_recurrence_id"))
@WithLongVKey
@WithLongVKey(compositeKey = true)
public static class Recurring extends BillingEvent implements DatastoreAndSqlEntity {
/**
@@ -559,7 +559,7 @@ public abstract class BillingEvent extends ImmutableObject
@javax.persistence.Index(columnList = "billingTime")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_cancellation_id"))
@WithLongVKey
@WithLongVKey(compositeKey = true)
public static class Cancellation extends BillingEvent implements DatastoreAndSqlEntity {
/** The billing time of the charge that is being cancelled. */
@@ -680,7 +680,7 @@ public abstract class BillingEvent extends ImmutableObject
/** An event representing a modification of an existing one-time billing event. */
@ReportedOn
@Entity
@WithLongVKey
@WithLongVKey(compositeKey = true)
public static class Modification extends BillingEvent implements DatastoreOnlyEntity {
/** The change in cost that should be applied to the original billing event. */
@@ -75,7 +75,9 @@ public class DomainBase extends DomainContent
}
@ElementCollection
@JoinTable(name = "DomainHost")
@JoinTable(
name = "DomainHost",
indexes = {@Index(columnList = "domain_repo_id,host_repo_id", unique = true)})
@Access(AccessType.PROPERTY)
@Column(name = "host_repo_id")
public Set<VKey<HostResource>> getNsHosts() {
@@ -96,7 +96,14 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
// TODO(b/166776754): Investigate if we can reuse domainContent.nsHosts for storing host keys.
@Ignore
@ElementCollection
@JoinTable(name = "DomainHistoryHost")
@JoinTable(
name = "DomainHistoryHost",
indexes = {
@Index(
columnList =
"domain_history_history_revision_id,domain_history_domain_repo_id,host_repo_id",
unique = true),
})
@Column(name = "host_repo_id")
Set<VKey<HostResource>> nsHosts;
@@ -23,6 +23,8 @@ import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.ofy.ObjectifyService;
import google.registry.persistence.BillingVKey.BillingEventVKey;
import google.registry.persistence.BillingVKey.BillingRecurrenceVKey;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import javax.annotation.Nullable;
@@ -82,10 +84,8 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
instance.domainRepoId = checkArgumentNotNull(domainRepoId);
instance.expirationTime = checkArgumentNotNull(expirationTime);
instance.clientId = checkArgumentNotNull(clientId);
instance.billingEventOneTime = billingEventOneTime;
instance.billingEventOneTimeHistoryId = DomainBase.getHistoryId(billingEventOneTime);
instance.billingEventRecurring = billingEventRecurring;
instance.billingEventRecurringHistoryId = DomainBase.getHistoryId(billingEventRecurring);
instance.billingEventOneTime = BillingEventVKey.create(billingEventOneTime);
instance.billingEventRecurring = BillingRecurrenceVKey.create(billingEventRecurring);
return instance;
}
@@ -178,7 +178,6 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
public GracePeriod cloneAfterOfyLoad(String domainRepoId) {
GracePeriod clone = clone(this);
clone.domainRepoId = checkArgumentNotNull(domainRepoId);
clone.restoreHistoryIds();
return clone;
}
@@ -190,7 +189,7 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
*/
public GracePeriod cloneWithRecurringBillingEvent(VKey<BillingEvent.Recurring> recurring) {
GracePeriod clone = clone(this);
clone.billingEventRecurring = recurring;
clone.billingEventRecurring = BillingRecurrenceVKey.create(recurring);
return clone;
}
@@ -232,9 +231,7 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
instance.expirationTime = gracePeriod.expirationTime;
instance.clientId = gracePeriod.clientId;
instance.billingEventOneTime = gracePeriod.billingEventOneTime;
instance.billingEventOneTimeHistoryId = gracePeriod.billingEventOneTimeHistoryId;
instance.billingEventRecurring = gracePeriod.billingEventRecurring;
instance.billingEventRecurringHistoryId = gracePeriod.billingEventRecurringHistoryId;
return instance;
}
}
@@ -14,14 +14,14 @@
package google.registry.model.domain;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.ModelUtils;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.persistence.BillingVKey.BillingEventVKey;
import google.registry.persistence.BillingVKey.BillingRecurrenceVKey;
import google.registry.persistence.VKey;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
@@ -68,24 +68,16 @@ public class GracePeriodBase extends ImmutableObject {
* billingEventRecurring}) or for redemption grace periods (since deletes have no cost).
*/
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
@Column(name = "billing_event_id")
VKey<OneTime> billingEventOneTime = null;
@Ignore
@Column(name = "billing_event_history_id")
Long billingEventOneTimeHistoryId;
@Access(AccessType.FIELD)
BillingEventVKey billingEventOneTime = null;
/**
* The recurring billing event corresponding to the action that triggered this grace period, if
* applicable - i.e. if the action was an autorenew - or null in all other cases.
*/
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
@Column(name = "billing_recurrence_id")
VKey<BillingEvent.Recurring> billingEventRecurring = null;
@Ignore
@Column(name = "billing_recurrence_history_id")
Long billingEventRecurringHistoryId;
@Access(AccessType.FIELD)
BillingRecurrenceVKey billingEventRecurring = null;
public long getGracePeriodId() {
return gracePeriodId;
@@ -123,8 +115,7 @@ public class GracePeriodBase extends ImmutableObject {
* period is not AUTO_RENEW.
*/
public VKey<BillingEvent.OneTime> getOneTimeBillingEvent() {
restoreOfyKeys();
return billingEventOneTime;
return billingEventOneTime == null ? null : billingEventOneTime.createVKey();
}
/**
@@ -132,18 +123,7 @@ public class GracePeriodBase extends ImmutableObject {
* period is AUTO_RENEW.
*/
public VKey<BillingEvent.Recurring> getRecurringBillingEvent() {
restoreOfyKeys();
return billingEventRecurring;
}
/**
* Restores history ids for composite VKeys after a load from datastore.
*
* <p>For use by DomainContent.load() ONLY.
*/
protected void restoreHistoryIds() {
billingEventOneTimeHistoryId = DomainBase.getHistoryId(billingEventOneTime);
billingEventRecurringHistoryId = DomainBase.getHistoryId(billingEventRecurring);
return billingEventRecurring == null ? null : billingEventRecurring.createVKey();
}
/**
@@ -152,7 +132,6 @@ public class GracePeriodBase extends ImmutableObject {
*/
@Override
protected Map<Field, Object> getSignificantFields() {
restoreOfyKeys();
// Can't use streams or ImmutableMap because we can have null values.
Map<Field, Object> result = new LinkedHashMap();
for (Map.Entry<Field, Object> entry : ModelUtils.getFieldValues(this).entrySet()) {
@@ -162,33 +141,4 @@ public class GracePeriodBase extends ImmutableObject {
}
return result;
}
/**
* Restores Ofy keys in the billing events.
*
* <p>This must be called by all methods that access the one time or recurring billing event keys.
* When the billing event keys are loaded from SQL, they are loaded as asymmetric keys because the
* database columns that we load them from do not contain all of the information necessary to
* reconsitute the Ofy side of the key. In other cases, we restore the Ofy key during the
* hibernate {@link javax.persistence.PostLoad} method from the other fields of the object, but we
* have been unable to make this work with hibernate's internal persistence model in this case
* because the {@link GracePeriod}'s hash code is evaluated prior to these calls, and would be
* invalidated by changing the fields.
*/
private final synchronized void restoreOfyKeys() {
if (billingEventOneTime != null && !billingEventOneTime.maybeGetOfyKey().isPresent()) {
billingEventOneTime =
DomainBase.restoreOfyFrom(
Key.create(DomainBase.class, domainRepoId),
billingEventOneTime,
billingEventOneTimeHistoryId);
}
if (billingEventRecurring != null && !billingEventRecurring.maybeGetOfyKey().isPresent()) {
billingEventRecurring =
DomainBase.restoreOfyFrom(
Key.create(DomainBase.class, domainRepoId),
billingEventRecurring,
billingEventRecurringHistoryId);
}
}
}
@@ -111,9 +111,9 @@ public class AllocationToken extends BackupGroupRoot implements Buildable, Datas
@Nullable
@Index
@AttributeOverrides({
@AttributeOverride(name = "domainRepoId", column = @Column(name = "redemption_domain_repo_id")),
@AttributeOverride(name = "repoId", column = @Column(name = "redemption_domain_repo_id")),
@AttributeOverride(
name = "domainHistoryId",
name = "historyRevisionId",
column = @Column(name = "redemption_domain_history_id"))
})
DomainHistoryVKey redemptionHistoryEntry;
@@ -192,8 +192,9 @@ public class AllocationToken extends BackupGroupRoot implements Buildable, Datas
return token;
}
public Optional<VKey<HistoryEntry>> getRedemptionHistoryEntry() {
return Optional.ofNullable(redemptionHistoryEntry);
public Optional<VKey<? extends HistoryEntry>> getRedemptionHistoryEntry() {
return Optional.ofNullable(
redemptionHistoryEntry == null ? null : redemptionHistoryEntry.createDomainHistoryVKey());
}
public boolean isRedeemed() {
@@ -291,9 +292,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable, Datas
return this;
}
public Builder setRedemptionHistoryEntry(DomainHistoryVKey redemptionHistoryEntry) {
public Builder setRedemptionHistoryEntry(VKey<? extends HistoryEntry> redemptionHistoryEntry) {
checkArgumentNotNull(redemptionHistoryEntry, "Redemption history entry must not be null");
getInstance().redemptionHistoryEntry =
checkArgumentNotNull(redemptionHistoryEntry, "Redemption history entry must not be null");
DomainHistoryVKey.create(redemptionHistoryEntry.getOfyKey());
return this;
}
@@ -15,9 +15,11 @@
package google.registry.model.index;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.TypeUtils.instantiate;
@@ -29,6 +31,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
@@ -44,6 +47,8 @@ import google.registry.model.host.HostResource;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreOnlyEntity;
import google.registry.util.NonFinalForTesting;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@@ -76,13 +81,21 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
public static class ForeignKeyHostIndex extends ForeignKeyIndex<HostResource>
implements DatastoreOnlyEntity {}
static final ImmutableMap<Class<? extends EppResource>, Class<? extends ForeignKeyIndex<?>>>
private static final ImmutableMap<
Class<? extends EppResource>, Class<? extends ForeignKeyIndex<?>>>
RESOURCE_CLASS_TO_FKI_CLASS =
ImmutableMap.of(
ContactResource.class, ForeignKeyContactIndex.class,
DomainBase.class, ForeignKeyDomainIndex.class,
HostResource.class, ForeignKeyHostIndex.class);
private static final ImmutableMap<Class<? extends EppResource>, String>
RESOURCE_CLASS_TO_FKI_PROPERTY =
ImmutableMap.of(
ContactResource.class, "contactId",
DomainBase.class, "fullyQualifiedDomainName",
HostResource.class, "fullyQualifiedHostName");
@Id String foreignKey;
/**
@@ -179,9 +192,42 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
*/
public static <E extends EppResource> ImmutableMap<String, ForeignKeyIndex<E>> load(
Class<E> clazz, Iterable<String> foreignKeys, final DateTime now) {
return ofy().load().type(mapToFkiClass(clazz)).ids(foreignKeys).entrySet().stream()
.filter(e -> now.isBefore(e.getValue().deletionTime))
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
if (tm().isOfy()) {
return ofy().load().type(mapToFkiClass(clazz)).ids(foreignKeys).entrySet().stream()
.filter(e -> now.isBefore(e.getValue().deletionTime))
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
} else {
String property = RESOURCE_CLASS_TO_FKI_PROPERTY.get(clazz);
List<E> entities =
tm().transact(
() -> {
String entityName =
jpaTm().getEntityManager().getMetamodel().entity(clazz).getName();
return jpaTm()
.getEntityManager()
.createQuery(
String.format(
"FROM %s WHERE %s IN :propertyValue and deletionTime > :now ",
entityName, property),
clazz)
.setParameter("propertyValue", foreignKeys)
.setParameter("now", now)
.getResultList();
});
// We need to find and return the entities with the maximum deletionTime for each foreign key.
return Multimaps.index(entities, EppResource::getForeignKey).asMap().entrySet().stream()
.map(
entry ->
Maps.immutableEntry(
entry.getKey(),
entry.getValue().stream()
.max(Comparator.comparing(EppResource::getDeletionTime))
.get()))
.collect(
toImmutableMap(
Map.Entry::getKey,
entry -> create(entry.getValue(), entry.getValue().getDeletionTime())));
}
}
static final CacheLoader<Key<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>> CACHE_LOADER =
@@ -266,7 +312,7 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
.filter(entry -> entry.getValue().isPresent())
.filter(entry -> now.isBefore(entry.getValue().get().getDeletionTime()))
.collect(
ImmutableMap.toImmutableMap(
toImmutableMap(
entry -> entry.getKey().getName(),
entry -> (ForeignKeyIndex<E>) entry.getValue().get()));
return fkisFromCache;
@@ -42,8 +42,8 @@ import google.registry.model.translators.CidrAddressBlockTranslatorFactory;
import google.registry.model.translators.CommitLogRevisionsTranslatorFactory;
import google.registry.model.translators.CreateAutoTimestampTranslatorFactory;
import google.registry.model.translators.CurrencyUnitTranslatorFactory;
import google.registry.model.translators.DomainHistoryVKeyTranslatorFactory;
import google.registry.model.translators.DurationTranslatorFactory;
import google.registry.model.translators.EppHistoryVKeyTranslatorFactory;
import google.registry.model.translators.InetAddressTranslatorFactory;
import google.registry.model.translators.ReadableInstantUtcTranslatorFactory;
import google.registry.model.translators.UpdateAutoTimestampTranslatorFactory;
@@ -128,7 +128,7 @@ public class ObjectifyService {
new CreateAutoTimestampTranslatorFactory(),
new CurrencyUnitTranslatorFactory(),
new DurationTranslatorFactory(),
new DomainHistoryVKeyTranslatorFactory(),
new EppHistoryVKeyTranslatorFactory(),
new InetAddressTranslatorFactory(),
new MoneyStringTranslatorFactory(),
new ReadableInstantUtcTranslatorFactory(),
@@ -279,7 +279,7 @@ public abstract class PollMessage extends ImmutableObject
@EntitySubclass(index = false)
@javax.persistence.Entity
@DiscriminatorValue("ONE_TIME")
@WithLongVKey
@WithLongVKey(compositeKey = true)
public static class OneTime extends PollMessage {
// Response data. Objectify cannot persist a base class type, so we must have a separate field
@@ -432,7 +432,7 @@ public abstract class PollMessage extends ImmutableObject
@EntitySubclass(index = false)
@javax.persistence.Entity
@DiscriminatorValue("AUTORENEW")
@WithLongVKey
@WithLongVKey(compositeKey = true)
public static class Autorenew extends PollMessage {
/** The target id of the autorenew event. */
@@ -21,10 +21,10 @@ import javax.persistence.TemporalType;
import org.joda.time.LocalDate;
/**
* Data access object for {@link google.registry.model.reporting.Spec11ThreatMatch}.
* Data access object for {@link Spec11ThreatMatch}.
*
* <p>A JpaTransactionManager is passed into each static method because they are called from a BEAM
* pipeline and we don't know where it's coming from.
* <p>The transaction manager is passed as a parameter because this could be called either from a
* BEAM pipeline or standard non-BEAM code.
*/
public class Spec11ThreatMatchDao {
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.isEmpty;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.allocateId;
import static google.registry.model.ofy.ObjectifyService.ofy;
@@ -32,7 +33,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EmbedMap;
import com.googlecode.objectify.annotation.Entity;
@@ -82,8 +82,6 @@ import org.joda.time.DateTime;
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
public class SignedMarkRevocationList extends ImmutableObject implements NonReplicatedEntity {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting static final int SHARD_SIZE = 10000;
/** Common ancestor for queries. */
@@ -121,12 +119,11 @@ public class SignedMarkRevocationList extends ImmutableObject implements NonRepl
memoizeWithShortExpiration(
() -> {
SignedMarkRevocationList datastoreList = loadFromDatastore();
// Also load the list from Cloud SQL, compare the two lists, and log if different.
try {
loadAndCompareCloudSqlList(datastoreList);
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing signed mark revocation lists.");
}
suppressExceptionUnlessInTest(
() -> {
loadAndCompareCloudSqlList(datastoreList);
},
"Error comparing signed mark revocation lists.");
return datastoreList;
});
@@ -229,11 +226,12 @@ public class SignedMarkRevocationList extends ImmutableObject implements NonRepl
Maps.difference(datastoreList.revokes, cloudSqlList.revokes);
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
logger.atWarning().log(
String message =
String.format(
"Unequal SM revocation lists detected, Cloud SQL list with revision id %d has %d"
+ " different records than the current Datastore list.",
cloudSqlList.revisionId, diff.entriesDiffering().size()));
cloudSqlList.revisionId, diff.entriesDiffering().size());
throw new RuntimeException(message);
} else {
StringBuilder diffMessage = new StringBuilder("Unequal SM revocation lists detected:\n");
diff.entriesDiffering()
@@ -243,11 +241,13 @@ public class SignedMarkRevocationList extends ImmutableObject implements NonRepl
String.format(
"SMD %s has key %s in Datastore and key %s in Cloud SQL.\n",
label, valueDiff.leftValue(), valueDiff.rightValue())));
logger.atWarning().log(diffMessage.toString());
throw new RuntimeException(diffMessage.toString());
}
}
} else {
logger.atWarning().log("Signed mark revocation list in Cloud SQL is empty.");
if (datastoreList.size() != 0) {
throw new RuntimeException("Signed mark revocation list in Cloud SQL is empty.");
}
}
}
@@ -15,6 +15,7 @@
package google.registry.model.smd;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.base.Supplier;
@@ -60,14 +61,14 @@ public class SignedMarkRevocationListDao {
* the authoritative database.
*/
static void trySave(SignedMarkRevocationList signedMarkRevocationList) {
try {
SignedMarkRevocationListDao.save(signedMarkRevocationList);
logger.atInfo().log(
"Inserted %,d signed mark revocations into Cloud SQL",
signedMarkRevocationList.revokes.size());
} catch (Throwable e) {
logger.atSevere().withCause(e).log("Error inserting signed mark revocations into Cloud SQL");
}
suppressExceptionUnlessInTest(
() -> {
SignedMarkRevocationListDao.save(signedMarkRevocationList);
logger.atInfo().log(
"Inserted %,d signed mark revocations into Cloud SQL.",
signedMarkRevocationList.revokes.size());
},
"Error inserting signed mark revocations into Cloud SQL.");
}
private static void save(SignedMarkRevocationList signedMarkRevocationList) {
@@ -1,48 +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.translators;
import com.google.appengine.api.datastore.Key;
import google.registry.persistence.DomainHistoryVKey;
import javax.annotation.Nullable;
/** Translator factory for {@link DomainHistoryVKey}. */
public class DomainHistoryVKeyTranslatorFactory
extends AbstractSimpleTranslatorFactory<DomainHistoryVKey, Key> {
public DomainHistoryVKeyTranslatorFactory() {
super(DomainHistoryVKey.class);
}
@Override
SimpleTranslator<DomainHistoryVKey, Key> createTranslator() {
return new SimpleTranslator<DomainHistoryVKey, Key>() {
@Nullable
@Override
public DomainHistoryVKey loadValue(@Nullable Key datastoreValue) {
return datastoreValue == null
? null
: DomainHistoryVKey.create(com.googlecode.objectify.Key.create(datastoreValue));
}
@Nullable
@Override
public Key saveValue(@Nullable DomainHistoryVKey pojoValue) {
return pojoValue == null ? null : pojoValue.getOfyKey().getRaw();
}
};
}
}
@@ -0,0 +1,110 @@
// 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.translators;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static java.util.function.Function.identity;
import com.google.appengine.api.datastore.Key;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.persistence.BillingVKey.BillingEventVKey;
import google.registry.persistence.BillingVKey.BillingRecurrenceVKey;
import google.registry.persistence.DomainHistoryVKey;
import google.registry.persistence.EppHistoryVKey;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.annotation.Nullable;
/** Translator factory for {@link EppHistoryVKey}. */
public class EppHistoryVKeyTranslatorFactory
extends AbstractSimpleTranslatorFactory<EppHistoryVKey, Key> {
public EppHistoryVKeyTranslatorFactory() {
super(EppHistoryVKey.class);
}
// This map is used when we need to convert the raw Datastore key to its VKey instance. We have
// one dedicated VKey class, e.g. DomainHistoryVKey, for each such kind of entity, and we need
// a way to map the raw Datastore key to its VKey class. So, we use the kind path as the key of
// the map, and the kind path is created by concatenating all the kind strings in a raw Datastore
// key, e.g. the map key for ContactPollMessageVKey is "ContactResource/HistoryEntry/PollMessage".
@VisibleForTesting
static final ImmutableMap<String, Class<? extends EppHistoryVKey>> kindPathToVKeyClass =
ImmutableSet.of(DomainHistoryVKey.class, BillingEventVKey.class, BillingRecurrenceVKey.class)
.stream()
.collect(toImmutableMap(EppHistoryVKeyTranslatorFactory::getKindPath, identity()));
/**
* Gets the kind path string for the given {@link Class}.
*
* <p>This method calls the getKindPath method on an instance of the given {@link Class} to get
* the kind path string.
*/
private static String getKindPath(Class<? extends EppHistoryVKey> clazz) {
try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object instance = constructor.newInstance();
Method getKindPathMethod = EppHistoryVKey.class.getDeclaredMethod("getKindPath");
getKindPathMethod.setAccessible(true);
return (String) getKindPathMethod.invoke(instance);
} catch (Throwable t) {
throw new IllegalStateException(t);
}
}
@Override
SimpleTranslator<EppHistoryVKey, Key> createTranslator() {
return new SimpleTranslator<EppHistoryVKey, Key>() {
@Nullable
@Override
public EppHistoryVKey loadValue(@Nullable Key datastoreValue) {
if (datastoreValue == null) {
return null;
} else {
com.googlecode.objectify.Key<?> ofyKey =
com.googlecode.objectify.Key.create(datastoreValue);
String kindPath = EppHistoryVKey.createKindPath(ofyKey);
if (kindPathToVKeyClass.containsKey(kindPath)) {
Class<? extends EppHistoryVKey> vKeyClass = kindPathToVKeyClass.get(kindPath);
try {
Method createVKeyMethod =
vKeyClass.getDeclaredMethod("create", com.googlecode.objectify.Key.class);
return (EppHistoryVKey) createVKeyMethod.invoke(null, ofyKey);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(
"Missing static method create(com.googlecode.objectify.Key) on " + vKeyClass);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException("Error invoking createVKey on " + vKeyClass, e);
}
} else {
throw new IllegalStateException(
"Missing EppHistoryVKey implementation for kind path: " + kindPath);
}
}
}
@Nullable
@Override
public Key saveValue(@Nullable EppHistoryVKey pojoValue) {
return pojoValue == null ? null : pojoValue.createOfyKey().getRaw();
}
};
}
}
@@ -46,7 +46,10 @@ public class UpdateAutoTimestampTranslatorFactory
/** Save a timestamp, setting it to the current time. */
@Override
public Date saveValue(UpdateAutoTimestamp pojoValue) {
return tm().getTransactionTime().toDate();
}};
return UpdateAutoTimestamp.autoUpdateEnabled()
? tm().getTransactionTime().toDate()
: pojoValue.getTimestamp().toDate();
}
};
}
}
@@ -0,0 +1,134 @@
// 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.persistence;
import com.googlecode.objectify.Key;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.DomainBase;
import google.registry.model.reporting.HistoryEntry;
import javax.annotation.Nullable;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.MappedSuperclass;
/** Base class for {@link BillingEvent}'s {@link VKey}. */
@MappedSuperclass
public abstract class BillingVKey<K> extends EppHistoryVKey<K, DomainBase> {
Long billingId;
// Hibernate requires a default constructor.
BillingVKey() {}
BillingVKey(String repoId, long historyRevisionId, long billingId) {
super(repoId, historyRevisionId);
this.billingId = billingId;
}
Key<HistoryEntry> createHistoryEntryKey() {
return Key.create(Key.create(DomainBase.class, repoId), HistoryEntry.class, historyRevisionId);
}
@Override
public Object createSqlKey() {
return billingId;
}
/** VKey class for {@link BillingEvent.OneTime} that belongs to a {@link DomainBase} entity. */
@Embeddable
@AttributeOverrides({
@AttributeOverride(name = "repoId", column = @Column(name = "billing_event_domain_repo_id")),
@AttributeOverride(
name = "historyRevisionId",
column = @Column(name = "billing_event_history_id")),
@AttributeOverride(name = "billingId", column = @Column(name = "billing_event_id"))
})
public static class BillingEventVKey extends BillingVKey<OneTime> {
// Hibernate requires this default constructor
private BillingEventVKey() {}
private BillingEventVKey(String repoId, long historyRevisionId, long billingEventId) {
super(repoId, historyRevisionId, billingEventId);
}
@Override
public Key<OneTime> createOfyKey() {
return Key.create(createHistoryEntryKey(), BillingEvent.OneTime.class, billingId);
}
/** Creates a {@link BillingEventVKey} instance from the given {@link Key} instance. */
public static BillingEventVKey create(@Nullable Key<BillingEvent.OneTime> ofyKey) {
if (ofyKey == null) {
return null;
}
long billingEventId = ofyKey.getId();
long historyRevisionId = ofyKey.getParent().getId();
String repoId = ofyKey.getParent().getParent().getName();
return new BillingEventVKey(repoId, historyRevisionId, billingEventId);
}
/** Creates a {@link BillingEventVKey} instance from the given {@link VKey} instance. */
public static BillingEventVKey create(@Nullable VKey<BillingEvent.OneTime> vKey) {
return vKey == null ? null : create(vKey.getOfyKey());
}
}
/** VKey class for {@link BillingEvent.Recurring} that belongs to a {@link DomainBase} entity. */
@Embeddable
@AttributeOverrides({
@AttributeOverride(
name = "repoId",
column = @Column(name = "billing_recurrence_domain_repo_id")),
@AttributeOverride(
name = "historyRevisionId",
column = @Column(name = "billing_recurrence_history_id")),
@AttributeOverride(name = "billingId", column = @Column(name = "billing_recurrence_id"))
})
public static class BillingRecurrenceVKey extends BillingVKey<Recurring> {
// Hibernate requires this default constructor
private BillingRecurrenceVKey() {}
private BillingRecurrenceVKey(String repoId, long historyRevisionId, long billingEventId) {
super(repoId, historyRevisionId, billingEventId);
}
@Override
public Key<Recurring> createOfyKey() {
return Key.create(createHistoryEntryKey(), BillingEvent.Recurring.class, billingId);
}
/** Creates a {@link BillingRecurrenceVKey} instance from the given {@link Key} instance. */
public static BillingRecurrenceVKey create(@Nullable Key<BillingEvent.Recurring> ofyKey) {
if (ofyKey == null) {
return null;
}
long billingEventId = ofyKey.getId();
long historyRevisionId = ofyKey.getParent().getId();
String repoId = ofyKey.getParent().getParent().getName();
return new BillingRecurrenceVKey(repoId, historyRevisionId, billingEventId);
}
/** Creates a {@link BillingRecurrenceVKey} instance from the given {@link VKey} instance. */
public static BillingRecurrenceVKey create(@Nullable VKey<BillingEvent.Recurring> vKey) {
return vKey == null ? null : create(vKey.getOfyKey());
}
}
}
@@ -18,45 +18,44 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.googlecode.objectify.Key;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.reporting.HistoryEntry;
import javax.persistence.Embeddable;
import javax.persistence.PostLoad;
/** {@link VKey} for {@link HistoryEntry} which parent is {@link DomainBase}. */
@Embeddable
public class DomainHistoryVKey extends VKey<HistoryEntry> {
private String domainRepoId;
private Long domainHistoryId;
public class DomainHistoryVKey extends EppHistoryVKey<HistoryEntry, DomainBase> {
// Hibernate requires a default constructor
private DomainHistoryVKey() {}
private DomainHistoryVKey(String domainRepoId, long domainHistoryId) {
initWith(domainRepoId, domainHistoryId);
private DomainHistoryVKey(String repoId, long historyRevisionId) {
super(repoId, historyRevisionId);
}
@PostLoad
void postLoad() {
initWith(domainRepoId, domainHistoryId);
@Override
public Object createSqlKey() {
return new DomainHistoryId(repoId, historyRevisionId);
}
private void initWith(String domainRepoId, long domainHistoryId) {
this.kind = HistoryEntry.class;
this.ofyKey =
Key.create(Key.create(DomainBase.class, domainRepoId), HistoryEntry.class, domainHistoryId);
this.sqlKey = new DomainHistoryId(domainRepoId, domainHistoryId);
this.domainRepoId = domainRepoId;
this.domainHistoryId = domainHistoryId;
@Override
public Key<HistoryEntry> createOfyKey() {
return Key.create(Key.create(DomainBase.class, repoId), HistoryEntry.class, historyRevisionId);
}
/** Creates {@link DomainHistoryVKey} from the given {@link Key} instance. */
public static DomainHistoryVKey create(Key<HistoryEntry> ofyKey) {
public static DomainHistoryVKey create(Key<? extends HistoryEntry> ofyKey) {
checkArgumentNotNull(ofyKey, "ofyKey must be specified");
String domainRepoId = ofyKey.getParent().getName();
long domainHistoryId = ofyKey.getId();
return new DomainHistoryVKey(domainRepoId, domainHistoryId);
String repoId = ofyKey.getParent().getName();
long historyRevisionId = ofyKey.getId();
return new DomainHistoryVKey(repoId, historyRevisionId);
}
public VKey<? extends HistoryEntry> createDomainHistoryVKey() {
return VKey.create(
DomainHistory.class,
createSqlKey(),
Key.create(Key.create(DomainBase.class, repoId), DomainHistory.class, historyRevisionId));
}
}
@@ -0,0 +1,107 @@
// 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.persistence;
import com.google.common.base.Joiner;
import com.googlecode.objectify.Key;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.reporting.HistoryEntry;
import google.registry.util.TypeUtils.TypeInstantiator;
import java.io.Serializable;
import javax.annotation.Nullable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.MappedSuperclass;
/**
* Base class for {@link VKey} which ofyKey has a {@link HistoryEntry} key as its parent and a key
* for EPP resource as its grandparent.
*
* <p>For such a {@link VKey}, we need to provide two type parameters to indicate the type of {@link
* VKey} itself and the type of EPP resource respectively.
*
* @param <K> type of the {@link VKey}
* @param <E> type of the EPP resource that the key belongs to
*/
@MappedSuperclass
@Access(AccessType.FIELD)
public abstract class EppHistoryVKey<K, E extends EppResource> extends ImmutableObject
implements Serializable {
private static final long serialVersionUID = -3906580677709539818L;
String repoId;
Long historyRevisionId;
// Hibernate requires a default constructor.
EppHistoryVKey() {}
EppHistoryVKey(String repoId, long historyRevisionId) {
this.repoId = repoId;
this.historyRevisionId = historyRevisionId;
}
/**
* Returns the kind path for the ofyKey in this instance.
*
* <p>This method is only used reflectively by {@link EppHistoryVKeyTranslatorFactory} to get the
* kind path for a given {@link EppHistoryVKey} instance so it is marked as a private method.
*
* @see #createKindPath(Key)
*/
@SuppressWarnings("unused")
private String getKindPath() {
String eppKind = Key.getKind(new TypeInstantiator<E>(getClass()) {}.getExactType());
String keyKind = Key.getKind(new TypeInstantiator<K>(getClass()) {}.getExactType());
if (keyKind.equals(Key.getKind(HistoryEntry.class))) {
return createKindPath(eppKind, keyKind);
} else {
return createKindPath(eppKind, Key.getKind(HistoryEntry.class), keyKind);
}
}
/**
* Creates the kind path for the given ofyKey}.
*
* <p>The kind path is a string including all kind names(delimited by slash) of a hierarchical
* {@link Key}, e.g., the kind path for BillingEvent.OneTime is "DomainBase/HistoryEntry/OneTime".
*/
@Nullable
public static String createKindPath(@Nullable Key<?> ofyKey) {
if (ofyKey == null) {
return null;
} else if (ofyKey.getParent() == null) {
return ofyKey.getKind();
} else {
return createKindPath(createKindPath(ofyKey.getParent()), ofyKey.getKind());
}
}
private static String createKindPath(String... kinds) {
return Joiner.on("/").join(kinds);
}
/** Creates a {@link VKey} from this instance. */
public VKey<K> createVKey() {
Class<K> vKeyType = new TypeInstantiator<K>(getClass()) {}.getExactType();
return VKey.create(vKeyType, createSqlKey(), createOfyKey());
}
public abstract Object createSqlKey();
public abstract Key<K> createOfyKey();
}
@@ -20,6 +20,7 @@ import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.keyring.kms.KmsModule;
import google.registry.persistence.PersistenceModule.AppEngineJpaTm;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.util.UtilsModule;
import javax.inject.Singleton;
import javax.persistence.EntityManagerFactory;
@@ -32,6 +33,7 @@ import javax.persistence.EntityManagerFactory;
CredentialModule.class,
KmsModule.class,
PersistenceModule.class,
SecretManagerModule.class,
UtilsModule.class
})
public interface PersistenceComponent {
@@ -26,6 +26,7 @@ import com.google.api.client.auth.oauth2.Credential;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
@@ -33,6 +34,11 @@ import google.registry.keyring.kms.KmsKeyring;
import google.registry.persistence.transaction.CloudSqlCredentialSupplier;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.privileges.secretmanager.SqlCredential;
import google.registry.privileges.secretmanager.SqlCredentialStore;
import google.registry.privileges.secretmanager.SqlUser;
import google.registry.privileges.secretmanager.SqlUser.RobotId;
import google.registry.privileges.secretmanager.SqlUser.RobotUser;
import google.registry.tools.AuthModule.CloudSqlClientCredential;
import google.registry.util.Clock;
import java.lang.annotation.Documented;
@@ -47,6 +53,8 @@ import org.hibernate.cfg.Environment;
/** Dagger module class for the persistence layer. */
@Module
public class PersistenceModule {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// This name must be the same as the one defined in persistence.xml.
public static final String PERSISTENCE_UNIT_NAME = "nomulus";
public static final String HIKARI_CONNECTION_TIMEOUT = "hibernate.hikari.connectionTimeout";
@@ -122,11 +130,17 @@ public class PersistenceModule {
static JpaTransactionManager provideAppEngineJpaTm(
@Config("cloudSqlUsername") String username,
KmsKeyring kmsKeyring,
SqlCredentialStore credentialStore,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
Clock clock) {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
overrides.put(Environment.USER, username);
overrides.put(Environment.PASS, kmsKeyring.getCloudSqlPassword());
validateCredentialStore(
credentialStore,
new RobotUser(RobotId.NOMULUS),
overrides.get(Environment.USER),
overrides.get(Environment.PASS));
return new JpaTransactionManagerImpl(create(overrides), clock);
}
@@ -136,6 +150,7 @@ public class PersistenceModule {
static JpaTransactionManager provideNomulusToolJpaTm(
@Config("toolsCloudSqlUsername") String username,
KmsKeyring kmsKeyring,
SqlCredentialStore credentialStore,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
@CloudSqlClientCredential Credential credential,
Clock clock) {
@@ -143,6 +158,11 @@ public class PersistenceModule {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
overrides.put(Environment.USER, username);
overrides.put(Environment.PASS, kmsKeyring.getToolsCloudSqlPassword());
validateCredentialStore(
credentialStore,
new RobotUser(RobotId.TOOL),
overrides.get(Environment.USER),
overrides.get(Environment.PASS));
return new JpaTransactionManagerImpl(create(overrides), clock);
}
@@ -150,6 +170,7 @@ public class PersistenceModule {
@Singleton
@SocketFactoryJpaTm
static JpaTransactionManager provideSocketFactoryJpaTm(
SqlCredentialStore credentialStore,
@Config("beamCloudSqlUsername") String username,
@Config("beamCloudSqlPassword") String password,
@Config("beamHibernateHikariMaximumPoolSize") int hikariMaximumPoolSize,
@@ -159,6 +180,12 @@ public class PersistenceModule {
overrides.put(Environment.USER, username);
overrides.put(Environment.PASS, password);
overrides.put(HIKARI_MAXIMUM_POOL_SIZE, String.valueOf(hikariMaximumPoolSize));
// TODO(b/175700623): consider assigning different logins to pipelines
validateCredentialStore(
credentialStore,
new RobotUser(RobotId.NOMULUS),
overrides.get(Environment.USER),
overrides.get(Environment.PASS));
return new JpaTransactionManagerImpl(create(overrides), clock);
}
@@ -203,6 +230,30 @@ public class PersistenceModule {
return emf;
}
/** Verifies that the credential from the Secret Manager matches the one currently in use.
*
* <p>This is a helper for the transition to the Secret Manager, and will be removed once data
* and permissions are properly set up for all projects.
**/
private static void validateCredentialStore(
SqlCredentialStore credentialStore, SqlUser sqlUser, String login, String password) {
try {
SqlCredential credential = credentialStore.getCredential(sqlUser);
if (!credential.login().equals(login)) {
logger.atWarning().log(
"Wrong login for %s. Expecting %s, found %s.",
sqlUser.geUserName(), login, credential.login());
return;
}
if (!credential.password().equals(password)) {
logger.atWarning().log("Wrong password for %s.", sqlUser.geUserName());
}
logger.atWarning().log("Credentials in the kerying and the secret manager match.");
} catch (Throwable e) {
logger.atWarning().log(e.getMessage());
}
}
/** Dagger qualifier for {@link JpaTransactionManager} used for App Engine application. */
@Qualifier
@Documented
@@ -31,4 +31,12 @@ public @interface WithLongVKey {
* class name will be "VKeyConverter_" concatenated with the suffix.
*/
String classNameSuffix() default "";
/**
* Set to true if this is a composite vkey.
*
* <p>For composite VKeys, we don't attempt to define an objectify key when loading from SQL: the
* enclosing class has to take care of that.
*/
boolean compositeKey() default false;
}
@@ -31,4 +31,12 @@ public @interface WithStringVKey {
* class name will be "VKeyConverter_" concatenated with the suffix.
*/
String classNameSuffix() default "";
/**
* Set to true if this is a composite vkey.
*
* <p>For composite VKeys, we don't attempt to define an objectify key when loading from SQL: the
* enclosing class has to take care of that.
*/
boolean compositeKey() default false;
}
@@ -31,7 +31,14 @@ public class UpdateAutoTimestampConverter
@Override
public Timestamp convertToDatabaseColumn(UpdateAutoTimestamp entity) {
return Timestamp.from(DateTimeUtils.toZonedDateTime(jpaTm().getTransactionTime()).toInstant());
return Timestamp.from(
DateTimeUtils.toZonedDateTime(
UpdateAutoTimestamp.autoUpdateEnabled()
|| entity == null
|| entity.getTimestamp() == null
? jpaTm().getTransactionTime()
: entity.getTimestamp())
.toInstant());
}
@Override
@@ -14,6 +14,7 @@
package google.registry.persistence.converter;
import com.googlecode.objectify.Key;
import google.registry.persistence.VKey;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
@@ -29,7 +30,28 @@ public abstract class VKeyConverter<T, C> implements AttributeConverter<VKey<? e
@Override
@Nullable
public VKey<? extends T> convertToEntityAttribute(@Nullable C dbData) {
return dbData == null ? null : VKey.createSql(getAttributeClass(), dbData);
if (dbData == null) {
return null;
}
Class<? extends T> clazz = getAttributeClass();
Key ofyKey = null;
if (!hasCompositeOfyKey()) {
// If this isn't a composite key, we can create the Ofy key from the SQL key.
ofyKey =
dbData instanceof String
? Key.create(clazz, (String) dbData)
: Key.create(clazz, (Long) dbData);
return VKey.create(clazz, dbData, ofyKey);
} else {
// We don't know how to create the Ofy key and probably don't have everything necessary to do
// it anyway, so just create an asymmetric key - the containing object will have to convert it
// into a symmetric key.
return VKey.createSql(clazz, dbData);
}
}
protected boolean hasCompositeOfyKey() {
return false;
}
/** Returns the class of the attribute. */
@@ -29,6 +29,11 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig;
import google.registry.model.ImmutableObject;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyContactIndex;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyHostIndex;
import google.registry.persistence.JpaRetries;
import google.registry.persistence.VKey;
import google.registry.util.Clock;
@@ -56,6 +61,18 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
// The entity of classes in this set will be simply ignored when passed to modification
// operations, i.e. insert, put, update and delete. This is to help maintain a single code path
// when we switch from ofy() to tm() for the database migration as we don't need have a condition
// to exclude the Datastore specific entities when the underlying tm() is jpaTm().
// TODO(b/176108270): Remove this property after database migration.
private static final ImmutableSet<Class<? extends ImmutableObject>> IGNORED_ENTITY_CLASSES =
ImmutableSet.of(
EppResourceIndex.class,
ForeignKeyContactIndex.class,
ForeignKeyDomainIndex.class,
ForeignKeyHostIndex.class);
// EntityManagerFactory is thread safe.
private final EntityManagerFactory emf;
private final Clock clock;
@@ -228,6 +245,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public void insert(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
if (isEntityOfIgnoredClass(entity)) {
return;
}
assertInTransaction();
getEntityManager().persist(entity);
transactionInfo.get().addUpdate(entity);
@@ -253,6 +273,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public void put(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
if (isEntityOfIgnoredClass(entity)) {
return;
}
assertInTransaction();
getEntityManager().merge(entity);
transactionInfo.get().addUpdate(entity);
@@ -278,6 +301,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public void update(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
if (isEntityOfIgnoredClass(entity)) {
return;
}
assertInTransaction();
checkArgument(exists(entity), "Given entity does not exist");
getEntityManager().merge(entity);
@@ -414,6 +440,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public void delete(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
if (isEntityOfIgnoredClass(entity)) {
return;
}
assertInTransaction();
Object managedEntity = entity;
if (!getEntityManager().contains(entity)) {
@@ -464,6 +493,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
private static boolean isEntityOfIgnoredClass(Object entity) {
return IGNORED_ENTITY_CLASSES.contains(entity.getClass());
}
private static ImmutableSet<EntityId> getEntityIdsFromEntity(
EntityType<?> entityType, Object entity) {
if (entityType.hasSingleIdAttribute()) {
@@ -22,6 +22,9 @@ import java.util.Optional;
/** A Cloud Secret Manager client for Nomulus, bound to a specific GCP project. */
public interface SecretManagerClient {
/** Returns the project name with which this client is associated. */
String getProject();
/**
* Creates a new secret in the Cloud Secret Manager with no data.
*
@@ -32,6 +35,9 @@ public interface SecretManagerClient {
*/
void createSecret(String secretId);
/** Checks if a secret with the given {@code secretId} already exists. */
boolean secretExists(String secretId);
/** Returns all secret IDs in the Cloud Secret Manager. */
Iterable<String> listSecrets();
@@ -67,6 +73,24 @@ public interface SecretManagerClient {
*/
String getSecretData(String secretId, Optional<String> version);
/**
* Enables a secret version.
*
* @param secretId The ID of the secret
* @param version The version of the secret to fetch. If not provided, the {@code latest} version
* will be returned
*/
void enableSecretVersion(String secretId, String version);
/**
* Disables a secret version.
*
* @param secretId The ID of the secret
* @param version The version of the secret to fetch. If not provided, the {@code latest} version
* will be returned
*/
void disableSecretVersion(String secretId, String version);
/**
* Destroys a secret version.
*
@@ -29,12 +29,11 @@ import com.google.cloud.secretmanager.v1.SecretName;
import com.google.cloud.secretmanager.v1.SecretPayload;
import com.google.cloud.secretmanager.v1.SecretVersion;
import com.google.cloud.secretmanager.v1.SecretVersionName;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.protobuf.ByteString;
import google.registry.util.Retrier;
import java.util.Optional;
import java.util.concurrent.Callable;
import javax.inject.Inject;
/** Implements {@link SecretManagerClient} on Google Cloud Platform. */
public class SecretManagerClientImpl implements SecretManagerClient {
@@ -42,13 +41,17 @@ public class SecretManagerClientImpl implements SecretManagerClient {
private final SecretManagerServiceClient csmClient;
private final Retrier retrier;
@Inject
SecretManagerClientImpl(String project, SecretManagerServiceClient csmClient, Retrier retrier) {
this.project = project;
this.csmClient = csmClient;
this.retrier = retrier;
}
@Override
public String getProject() {
return project;
}
@Override
public void createSecret(String secretId) {
checkNotNull(secretId, "secretId");
@@ -57,12 +60,25 @@ public class SecretManagerClientImpl implements SecretManagerClient {
() -> csmClient.createSecret(ProjectName.of(project), secretId, secretSettings));
}
@Override
public boolean secretExists(String secretId) {
checkNotNull(secretId, "secretId");
try {
callSecretManager(() -> csmClient.getSecret(SecretName.of(project, secretId)));
return true;
} catch (NoSuchSecretResourceException e) {
return false;
}
}
@Override
public Iterable<String> listSecrets() {
ListSecretsPagedResponse response =
callSecretManager(() -> csmClient.listSecrets(ProjectName.of(project)));
return Iterables.transform(
response.iterateAll(), secret -> SecretName.parse(secret.getName()).getSecret());
return () ->
Streams.stream(response.iterateAll())
.map(secret -> SecretName.parse(secret.getName()).getSecret())
.iterator();
}
@Override
@@ -70,8 +86,10 @@ public class SecretManagerClientImpl implements SecretManagerClient {
checkNotNull(secretId, "secretId");
ListSecretVersionsPagedResponse response =
callSecretManager(() -> csmClient.listSecretVersions(SecretName.of(project, secretId)));
return Iterables.transform(
response.iterateAll(), SecretManagerClientImpl::toSecretVersionState);
return () ->
Streams.stream(response.iterateAll())
.map(SecretManagerClientImpl::toSecretVersionState)
.iterator();
}
private static SecretVersionState toSecretVersionState(SecretVersion secretVersion) {
@@ -108,6 +126,22 @@ public class SecretManagerClientImpl implements SecretManagerClient {
.toStringUtf8());
}
@Override
public void enableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
callSecretManager(
() -> csmClient.enableSecretVersion(SecretVersionName.of(project, secretId, version)));
}
@Override
public void disableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
callSecretManager(
() -> csmClient.disableSecretVersion(SecretVersionName.of(project, secretId, version)));
}
@Override
public void destroySecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
@@ -14,41 +14,47 @@
package google.registry.privileges.secretmanager;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import dagger.Component;
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.config.CredentialModule.DefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
import google.registry.util.Retrier;
import google.registry.util.UtilsModule;
import java.io.IOException;
import javax.inject.Singleton;
/** Provides bindings for {@link SecretManagerClient}. */
@Module
public class SecretManagerModule {
private final String project;
public SecretManagerModule(String project) {
this.project = checkNotNull(project, "project");
}
public abstract class SecretManagerModule {
@Provides
@Singleton
SecretManagerClient provideSecretManagerClient(Retrier retrier) {
static SecretManagerServiceSettings provideSecretManagerSetting(
@DefaultCredential GoogleCredentialsBundle credentialsBundle) {
try {
return new SecretManagerClientImpl(project, SecretManagerServiceClient.create(), retrier);
return SecretManagerServiceSettings.newBuilder()
.setCredentialsProvider(() -> credentialsBundle.getGoogleCredentials())
.build();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Provides
@Singleton
@Component(modules = {ConfigModule.class, SecretManagerModule.class, UtilsModule.class})
public interface SecretManagerComponent {
SecretManagerClient secretManagerClient();
static SecretManagerClient provideSecretManagerClient(
SecretManagerServiceSettings serviceSettings,
@Config("projectId") String project,
Retrier retrier) {
try {
SecretManagerServiceClient stub = SecretManagerServiceClient.create(serviceSettings);
Runtime.getRuntime().addShutdownHook(new Thread(stub::close));
return new SecretManagerClientImpl(project, stub, retrier);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,55 @@
// 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.privileges.secretmanager;
import static avro.shaded.com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import java.util.List;
/**
* Contains the login name and password of a Cloud SQL user.
*
* <p>User must take care not to include the {@link #SEPARATOR} in property values.
*/
@AutoValue
public abstract class SqlCredential {
public static final Character SEPARATOR = ' ';
public abstract String login();
public abstract String password();
@Override
public final String toString() {
// Use Object.toString(), which does not show object data.
return super.toString();
}
public final String toFormattedString() {
return String.format("%s%c%s", login(), SEPARATOR, password());
}
public static SqlCredential fromFormattedString(String sqlCredential) {
List<String> items = com.google.common.base.Splitter.on(SEPARATOR).splitToList(sqlCredential);
checkState(items.size() == 2, "Invalid SqlCredential string.");
return of(items.get(0), items.get(1));
}
public static SqlCredential of(String login, String password) {
return new AutoValue_SqlCredential(login, password);
}
}
@@ -0,0 +1,118 @@
// 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.privileges.secretmanager;
import com.google.cloud.secretmanager.v1.SecretVersionName;
import google.registry.config.RegistryConfig.Config;
import google.registry.privileges.secretmanager.SecretManagerClient.NoSuchSecretResourceException;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretAlreadyExistsException;
import java.util.Optional;
import javax.inject.Inject;
/**
* Storage of SQL users' login credentials, backed by Cloud Secret Manager.
*
* <p>A user's credential is stored with one level of indirection using two secret IDs: Each version
* of the <em>credential data</em> is stored as follows: its secret ID is determined by {@link
* #getCredentialDataSecretId(SqlUser, String dbInstance)}, and the value of each version is a
* {@link SqlCredential}, serialized using {@link SqlCredential#toFormattedString}. The 'live'
* version of the credential is saved under the 'live pointer' secret explained below.
*
* <p>The pointer to the 'live' version of the credential data is stored as follows: its secret ID
* is determined by {@link #getLiveLabelSecretId(SqlUser, String dbInstance)}; and the value of each
* version is a {@link SecretVersionName} in String form, pointing to a version of the credential
* data. Only the 'latest' version of this secret should be used. It is guaranteed to be valid.
*
* <p>The indirection in credential storage makes it easy to handle failures in the credential
* change process.
*/
public class SqlCredentialStore {
private final SecretManagerClient csmClient;
private final String dbInstance;
@Inject
SqlCredentialStore(
SecretManagerClient csmClient, @Config("cloudSqlDbInstanceName") String dbInstance) {
this.csmClient = csmClient;
this.dbInstance = dbInstance;
}
public SqlCredential getCredential(SqlUser user) {
SecretVersionName credentialName = getLiveCredentialSecretVersion(user);
return SqlCredential.fromFormattedString(
csmClient.getSecretData(
credentialName.getSecret(), Optional.of(credentialName.getSecretVersion())));
}
public void createOrUpdateCredential(SqlUser user, String password) {
SecretVersionName dataName = saveCredentialData(user, password);
saveLiveLabel(user, dataName);
}
public void deleteCredential(SqlUser user) {
try {
csmClient.deleteSecret(getCredentialDataSecretId(user, dbInstance));
} catch (NoSuchSecretResourceException e) {
// ok
}
try {
csmClient.deleteSecret(getLiveLabelSecretId(user, dbInstance));
} catch (NoSuchSecretResourceException e) {
// ok.
}
}
private void createSecretIfAbsent(String secretId) {
try {
csmClient.createSecret(secretId);
} catch (SecretAlreadyExistsException ignore) {
// Not a problem.
}
}
private SecretVersionName saveCredentialData(SqlUser user, String password) {
String credentialDataSecretId = getCredentialDataSecretId(user, dbInstance);
createSecretIfAbsent(credentialDataSecretId);
String credentialVersion =
csmClient.addSecretVersion(
credentialDataSecretId,
SqlCredential.of(createDatabaseLoginName(user), password).toFormattedString());
return SecretVersionName.of(csmClient.getProject(), credentialDataSecretId, credentialVersion);
}
private void saveLiveLabel(SqlUser user, SecretVersionName dataVersionName) {
String liveLabelSecretId = getLiveLabelSecretId(user, dbInstance);
createSecretIfAbsent(liveLabelSecretId);
csmClient.addSecretVersion(liveLabelSecretId, dataVersionName.toString());
}
private SecretVersionName getLiveCredentialSecretVersion(SqlUser user) {
return SecretVersionName.parse(
csmClient.getSecretData(getLiveLabelSecretId(user, dbInstance), Optional.empty()));
}
private static String getLiveLabelSecretId(SqlUser user, String dbInstance) {
return String.format("sql-cred-live-label-%s-%s", user.geUserName(), dbInstance);
}
private static String getCredentialDataSecretId(SqlUser user, String dbInstance) {
return String.format("sql-cred-data-%s-%s", user.geUserName(), dbInstance);
}
// WIP: when b/170230882 is complete, login will be versioned.
private static String createDatabaseLoginName(SqlUser user) {
return user.geUserName();
}
}
@@ -0,0 +1,67 @@
// 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.privileges.secretmanager;
import com.google.common.base.Ascii;
/**
* SQL user information for privilege management purposes.
*
* <p>A {@link RobotUser} represents a software system accessing the database using its own
* credential. Robots are well known and enumerated in {@link RobotId}.
*/
public abstract class SqlUser {
private final UserType type;
private final String userName;
protected SqlUser(UserType type, String userName) {
this.type = type;
this.userName = userName;
}
public UserType getType() {
return type;
}
public String geUserName() {
return userName;
}
/** Cloud SQL user types. Please see class javadoc of {@link SqlUser} for more information. */
enum UserType {
// Work in progress. Human user will be added.
ROBOT
}
/** Enumerates the {@link RobotUser RobotUsers} in the system. */
public enum RobotId {
NOMULUS,
/**
* Credential for RegistryTool. This is temporary, and will be removed when tool users are
* assigned their personal credentials.
*/
TOOL;
}
/** Information of a RobotUser for privilege management purposes. */
// Work in progress. Eventually will be provided based on configuration.
public static class RobotUser extends SqlUser {
public RobotUser(RobotId robot) {
super(UserType.ROBOT, Ascii.toLowerCase(robot.name()));
}
}
}
@@ -14,9 +14,11 @@
package google.registry.reporting.spec11;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
@@ -59,6 +61,24 @@ public class Spec11RegistrarThreatMatchesParser {
return getFromFile(getGcsFilename(date));
}
/** Returns registrar:set-of-threat-match pairings from the file, or empty if it doesn't exist. */
public ImmutableSet<RegistrarThreatMatches> getFromFile(GcsFilename spec11ReportFilename)
throws IOException {
if (!gcsUtils.existsAndNotEmpty(spec11ReportFilename)) {
return ImmutableSet.of();
}
try (InputStream in = gcsUtils.openInputStream(spec11ReportFilename);
InputStreamReader isr = new InputStreamReader(in, UTF_8)) {
// Skip the header at line 0
return Splitter.on("\n")
.omitEmptyStrings()
.splitToStream(CharStreams.toString(isr))
.skip(1)
.map(this::parseRegistrarThreatMatch)
.collect(toImmutableSet());
}
}
public Optional<LocalDate> getPreviousDateWithMatches(LocalDate date) {
LocalDate yesterday = date.minusDays(1);
GcsFilename gcsFilename = getGcsFilename(yesterday);
@@ -82,20 +102,6 @@ public class Spec11RegistrarThreatMatchesParser {
return new GcsFilename(reportingBucket, Spec11Pipeline.getSpec11ReportFilePath(localDate));
}
private ImmutableSet<RegistrarThreatMatches> getFromFile(GcsFilename spec11ReportFilename)
throws IOException, JSONException {
ImmutableSet.Builder<RegistrarThreatMatches> builder = ImmutableSet.builder();
try (InputStream in = gcsUtils.openInputStream(spec11ReportFilename)) {
ImmutableList<String> reportLines =
ImmutableList.copyOf(CharStreams.toString(new InputStreamReader(in, UTF_8)).split("\n"));
// Iterate from 1 to size() to skip the header at line 0.
for (int i = 1; i < reportLines.size(); i++) {
builder.add(parseRegistrarThreatMatch(reportLines.get(i)));
}
return builder.build();
}
}
private RegistrarThreatMatches parseRegistrarThreatMatch(String line) throws JSONException {
JSONObject reportJSON = new JSONObject(line);
String clientId = reportJSON.getString(Spec11Pipeline.REGISTRAR_CLIENT_ID_FIELD);
@@ -45,10 +45,10 @@ import google.registry.model.domain.DomainBase;
@Parameters(
separators = " =",
commandDescription = "Dedupe BillingEvent.OneTime entities with duplicate IDs.")
public class DedupeOneTimeBillingEventIdsCommand extends DedupeEntityIdsCommand<OneTime> {
public class DedupeOneTimeBillingEventIdsCommand extends ReadEntityFromKeyPathCommand<OneTime> {
@Override
void dedupe(OneTime entity) {
void process(OneTime entity) {
Key<BillingEvent> key = Key.create(entity);
Key<DomainBase> domainKey = getGrandParentAsDomain(key);
DomainBase domain = ofy().load().key(domainKey).now();
@@ -54,11 +54,10 @@ import java.util.Set;
@Parameters(
separators = " =",
commandDescription = "Dedupe BillingEvent.Recurring entities with duplicate IDs.")
public class DedupeRecurringBillingEventIdsCommand
extends DedupeEntityIdsCommand<BillingEvent.Recurring> {
public class DedupeRecurringBillingEventIdsCommand extends ReadEntityFromKeyPathCommand<Recurring> {
@Override
void dedupe(Recurring recurring) {
void process(Recurring recurring) {
// Loads the associated DomainBase and BillingEvent.OneTime entities that
// may have reference to this BillingEvent.Recurring entity.
Key<DomainBase> domainKey = getGrandParentAsDomain(Key.create(recurring));
@@ -14,19 +14,19 @@
package google.registry.tools;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.partition;
import static com.google.common.collect.Streams.stream;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.model.domain.token.AllocationToken;
import google.registry.persistence.VKey;
import java.util.List;
/**
@@ -48,7 +48,7 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
private static final int BATCH_SIZE = 20;
private static final Joiner JOINER = Joiner.on(", ");
private ImmutableSet<Key<AllocationToken>> tokensToDelete;
private ImmutableSet<VKey<AllocationToken>> tokensToDelete;
@Override
public void init() {
@@ -71,26 +71,24 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
}
/** Deletes a (filtered) batch of AllocationTokens and returns how many were deleted. */
private long deleteBatch(List<Key<AllocationToken>> batch) {
private long deleteBatch(List<VKey<AllocationToken>> batch) {
// Load the tokens in the same transaction as they are deleted to verify they weren't redeemed
// since the query ran. This also filters out per-domain tokens if they're not to be deleted.
ImmutableSet<AllocationToken> tokensToDelete =
ofy().load().keys(batch).values().stream()
.filter(t -> withDomains || !t.getDomainName().isPresent())
ImmutableSet<VKey<AllocationToken>> tokensToDelete =
tm().load(batch).values().stream()
.filter(t -> withDomains || t.getDomainName().isEmpty())
.filter(t -> SINGLE_USE.equals(t.getTokenType()))
.filter(t -> !t.isRedeemed())
.map(AllocationToken::createVKey)
.collect(toImmutableSet());
if (!dryRun) {
ofy().delete().entities(tokensToDelete);
tm().delete(tokensToDelete);
}
System.out.printf(
"%s tokens: %s\n",
dryRun ? "Would delete" : "Deleted",
JOINER.join(
tokensToDelete.stream()
.map(AllocationToken::getToken)
.sorted()
.collect(toImmutableSet())));
tokensToDelete.stream().map(VKey::getSqlKey).sorted().collect(toImmutableList())));
return tokensToDelete.size();
}
}
@@ -20,8 +20,8 @@ import static com.google.common.collect.Queues.newArrayDeque;
import static com.google.common.collect.Sets.difference;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.StringGenerator.DEFAULT_PASSWORD_LENGTH;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -38,10 +38,10 @@ import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.io.Files;
import com.googlecode.objectify.Key;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.persistence.VKey;
import google.registry.tools.params.TransitionListParameter.TokenStatusTransitions;
import google.registry.util.CollectionUtils;
import google.registry.util.NonFinalForTesting;
@@ -258,8 +258,13 @@ class GenerateAllocationTokensCommand implements CommandWithRemoteApi {
@VisibleForTesting
int saveTokens(final ImmutableSet<AllocationToken> tokens) {
Collection<AllocationToken> savedTokens =
dryRun ? tokens : tm().transact(() -> ofy().save().entities(tokens).now().values());
Collection<AllocationToken> savedTokens;
if (dryRun) {
savedTokens = tokens;
} else {
transactIfJpaTm(() -> tm().transact(() -> tm().putAll(tokens)));
savedTokens = tm().transact(() -> tm().loadAll(tokens));
}
savedTokens.forEach(
t -> System.out.println(SKIP_NULLS.join(t.getDomainName().orElse(null), t.getToken())));
return savedTokens.size();
@@ -282,12 +287,14 @@ class GenerateAllocationTokensCommand implements CommandWithRemoteApi {
}
private ImmutableSet<String> getExistingTokenStrings(ImmutableSet<String> candidates) {
ImmutableSet<Key<AllocationToken>> existingTokenKeys =
ImmutableSet<VKey<AllocationToken>> existingTokenKeys =
candidates.stream()
.map(input -> Key.create(AllocationToken.class, input))
.map(input -> VKey.create(AllocationToken.class, input))
.collect(toImmutableSet());
return ofy().load().keys(existingTokenKeys).values().stream()
.map(AllocationToken::getToken)
.collect(toImmutableSet());
return transactIfJpaTm(
() ->
tm().load(existingTokenKeys).values().stream()
.map(AllocationToken::getToken)
.collect(toImmutableSet()));
}
}
@@ -15,7 +15,6 @@
package google.registry.tools;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
@@ -26,6 +25,7 @@ import com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.token.AllocationToken;
import google.registry.persistence.VKey;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -45,22 +45,26 @@ final class GetAllocationTokenCommand implements CommandWithRemoteApi {
public void run() {
ImmutableMap.Builder<String, AllocationToken> builder = new ImmutableMap.Builder<>();
for (List<String> tokens : Lists.partition(mainParameters, BATCH_SIZE)) {
ImmutableList<Key<AllocationToken>> tokenKeys =
tokens.stream().map(t -> Key.create(AllocationToken.class, t)).collect(toImmutableList());
ofy().load().keys(tokenKeys).forEach((k, v) -> builder.put(k.getName(), v));
ImmutableList<VKey<AllocationToken>> tokenKeys =
tokens.stream()
.map(t -> VKey.create(AllocationToken.class, t))
.collect(toImmutableList());
tm().load(tokenKeys).forEach((k, v) -> builder.put(k.getSqlKey().toString(), v));
}
ImmutableMap<String, AllocationToken> loadedTokens = builder.build();
ImmutableMap<Key<DomainBase>, DomainBase> domains = loadRedeemedDomains(loadedTokens.values());
ImmutableMap<VKey<DomainBase>, DomainBase> domains = loadRedeemedDomains(loadedTokens.values());
for (String token : mainParameters) {
if (loadedTokens.containsKey(token)) {
AllocationToken loadedToken = loadedTokens.get(token);
System.out.println(loadedToken.toString());
if (!loadedToken.getRedemptionHistoryEntry().isPresent()) {
if (loadedToken.getRedemptionHistoryEntry().isEmpty()) {
System.out.printf("Token %s was not redeemed.\n", token);
} else {
Key<DomainBase> domainOfyKey =
loadedToken.getRedemptionHistoryEntry().get().getOfyKey().getParent();
DomainBase domain =
domains.get(loadedToken.getRedemptionHistoryEntry().get().getOfyKey().getParent());
domains.get(VKey.create(DomainBase.class, domainOfyKey.getName(), domainOfyKey));
if (domain == null) {
System.out.printf("ERROR: Token %s was redeemed but domain can't be loaded.\n", token);
} else {
@@ -76,19 +80,23 @@ final class GetAllocationTokenCommand implements CommandWithRemoteApi {
}
}
private static ImmutableMap<Key<DomainBase>, DomainBase> loadRedeemedDomains(
@SuppressWarnings("unchecked")
private static ImmutableMap<VKey<DomainBase>, DomainBase> loadRedeemedDomains(
Collection<AllocationToken> tokens) {
ImmutableList<Key<DomainBase>> domainKeys =
ImmutableList<VKey<DomainBase>> domainKeys =
tokens.stream()
.map(AllocationToken::getRedemptionHistoryEntry)
.filter(Optional::isPresent)
.map(Optional::get)
.map(key -> tm().load(key))
.map(he -> (Key<DomainBase>) he.getParent())
.map(key -> VKey.create(DomainBase.class, key.getName(), key))
.collect(toImmutableList());
ImmutableMap.Builder<Key<DomainBase>, DomainBase> domainsBuilder = new ImmutableMap.Builder<>();
for (List<Key<DomainBase>> keys : Lists.partition(domainKeys, BATCH_SIZE)) {
domainsBuilder.putAll(ofy().load().keys(keys));
ImmutableMap.Builder<VKey<DomainBase>, DomainBase> domainsBuilder =
new ImmutableMap.Builder<>();
for (List<VKey<DomainBase>> keys : Lists.partition(domainKeys, BATCH_SIZE)) {
tm().load(ImmutableList.copyOf(keys))
.forEach((k, v) -> domainsBuilder.put((VKey<DomainBase>) k, v));
}
return domainsBuilder.build();
}
@@ -0,0 +1,73 @@
// 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.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Ascii;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretManagerException;
import google.registry.privileges.secretmanager.SqlCredential;
import google.registry.privileges.secretmanager.SqlCredentialStore;
import google.registry.privileges.secretmanager.SqlUser;
import google.registry.privileges.secretmanager.SqlUser.RobotUser;
import google.registry.tools.params.PathParameter;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import javax.inject.Inject;
/**
* Command to get a Cloud SQL credential in the Secret Manager.
*
* <p>This command is a short-term tool that will be deprecated by the planned privilege server.
*/
@Parameters(separators = " =", commandDescription = "Get the Cloud SQL Credential for a given user")
public class GetSqlCredentialCommand implements Command {
@Inject SqlCredentialStore store;
@Parameter(names = "--user", description = "The Cloud SQL user.", required = true)
private String user;
@Parameter(
names = {"-o", "--output"},
description = "Name of output file for key data.",
validateWith = PathParameter.OutputFile.class)
private Path outputPath = null;
@Inject
GetSqlCredentialCommand() {}
@Override
public void run() throws Exception {
SqlUser sqlUser = new RobotUser(SqlUser.RobotId.valueOf(Ascii.toUpperCase(user)));
SqlCredential credential;
try {
credential = store.getCredential(sqlUser);
} catch (SecretManagerException e) {
System.out.println(e.getMessage());
return;
}
if (outputPath == null) {
System.out.printf("[%s]\n", credential.toFormattedString());
return;
}
try (FileOutputStream out = new FileOutputStream(outputPath.toFile())) {
out.write(credential.toFormattedString().getBytes(StandardCharsets.UTF_8));
}
}
}
@@ -33,8 +33,13 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
/** Base Command to dedupe entities with duplicate IDs. */
abstract class DedupeEntityIdsCommand<T> extends MutatingCommand {
/**
* Base Command to read entities from Datastore by their key paths retrieved from BigQuery.
*
* <p>The key path is the value of column __key__.path of the entity's BigQuery table. Its value is
* converted from the entity's key.
*/
abstract class ReadEntityFromKeyPathCommand<T> extends MutatingCommand {
@Parameter(
names = "--key_paths_file",
@@ -48,7 +53,7 @@ abstract class DedupeEntityIdsCommand<T> extends MutatingCommand {
private StringBuilder changeMessage = new StringBuilder();
abstract void dedupe(T entity);
abstract void process(T entity);
@Override
protected void init() throws Exception {
@@ -69,7 +74,7 @@ abstract class DedupeEntityIdsCommand<T> extends MutatingCommand {
}
Class<T> clazz = new TypeInstantiator<T>(getClass()) {}.getExactType();
if (clazz.isInstance(entity)) {
dedupe((T) entity);
process((T) entity);
} else {
throw new IllegalArgumentException("Unsupported entity key: " + untypedKey);
}
@@ -16,6 +16,7 @@ package google.registry.tools;
import com.google.common.collect.ImmutableMap;
import google.registry.tools.javascrap.BackfillRegistryLocksCommand;
import google.registry.tools.javascrap.BackfillSpec11ThreatMatchesCommand;
import google.registry.tools.javascrap.PopulateNullRegistrarFieldsCommand;
import google.registry.tools.javascrap.RemoveIpAddressCommand;
@@ -32,6 +33,7 @@ public final class RegistryTool {
new ImmutableMap.Builder<String, Class<? extends Command>>()
.put("ack_poll_messages", AckPollMessagesCommand.class)
.put("backfill_registry_locks", BackfillRegistryLocksCommand.class)
.put("backfill_spec11_threat_matches", BackfillSpec11ThreatMatchesCommand.class)
.put("canonicalize_labels", CanonicalizeLabelsCommand.class)
.put("check_domain", CheckDomainCommand.class)
.put("check_domain_claims", CheckDomainClaimsCommand.class)
@@ -78,6 +80,7 @@ public final class RegistryTool {
.put("get_routing_map", GetRoutingMapCommand.class)
.put("get_schema", GetSchemaCommand.class)
.put("get_schema_tree", GetSchemaTreeCommand.class)
.put("get_sql_credential", GetSqlCredentialCommand.class)
.put("get_tld", GetTldCommand.class)
.put("ghostryde", GhostrydeCommand.class)
.put("hash_certificate", HashCertificateCommand.class)
@@ -99,12 +102,15 @@ public final class RegistryTool {
.put("populate_null_registrar_fields", PopulateNullRegistrarFieldsCommand.class)
.put("registrar_contact", RegistrarContactCommand.class)
.put("remove_ip_address", RemoveIpAddressCommand.class)
.put("remove_registry_one_key", RemoveRegistryOneKeyCommand.class)
.put("renew_domain", RenewDomainCommand.class)
.put("resave_entities", ResaveEntitiesCommand.class)
.put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class)
.put("resave_epp_resource", ResaveEppResourceCommand.class)
.put("save_sql_credential", SaveSqlCredentialCommand.class)
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
.put("set_num_instances", SetNumInstancesCommand.class)
.put("set_sql_replay_checkpoint", SetSqlReplayCheckpointCommand.class)
.put("setup_ote", SetupOteCommand.class)
.put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class)
.put("unlock_domain", UnlockDomainCommand.class)
@@ -34,6 +34,7 @@ import google.registry.keyring.kms.KmsModule;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.NomulusToolJpaTm;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.RdeModule;
import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module;
@@ -74,6 +75,7 @@ import javax.inject.Singleton;
PersistenceModule.class,
RdeModule.class,
RequestFactoryModule.class,
SecretManagerModule.class,
URLFetchServiceModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class,
@@ -118,6 +120,8 @@ interface RegistryToolComponent {
void inject(GetOperationStatusCommand command);
void inject(GetSqlCredentialCommand command);
void inject(GhostrydeCommand command);
void inject(ImportDatastoreCommand command);
@@ -138,10 +142,14 @@ interface RegistryToolComponent {
void inject(RenewDomainCommand command);
void inject(SaveSqlCredentialCommand command);
void inject(SendEscrowReportToIcannCommand command);
void inject(SetNumInstancesCommand command);
void inject(SetSqlReplayCheckpointCommand command);
void inject(SetupOteCommand command);
void inject(UnlockDomainCommand command);
@@ -0,0 +1,69 @@
// 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.tools;
import com.beust.jcommander.Parameters;
import com.googlecode.objectify.Key;
import google.registry.model.domain.DomainBase;
import google.registry.persistence.VKey;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Command to remove the Registry 1.0 key in {@link DomainBase} entity. */
@Parameters(separators = " =", commandDescription = "Remove .")
public class RemoveRegistryOneKeyCommand extends ReadEntityFromKeyPathCommand<DomainBase> {
@Override
void process(DomainBase entity) {
// Assert that the DomainBase entity must be deleted before 2017-08-01(most of the problematic
// entities were deleted before 2017, though there are still a few entities deleted in 2017-07).
// This is because we finished the Registry 2.0 migration in 2017 and should not generate any
// Registry 1.0 key after it.
if (!entity.getDeletionTime().isBefore(DateTime.parse("2017-08-01T00:00:00Z"))) {
throw new IllegalStateException(
String.format(
"Entity's deletion time %s is not before 2017-08-01T00:00:00Z",
entity.getDeletionTime()));
}
boolean hasChange = false;
DomainBase.Builder domainBuilder = entity.asBuilder();
// We only found the registry 1.0 key existed in fields autorenewBillingEvent,
// autorenewPollMessage and deletePollMessage so we just need to check these fields for each
// entity.
if (isRegistryOneKey(entity.getAutorenewBillingEvent())) {
domainBuilder.setAutorenewBillingEvent(null);
hasChange = true;
}
if (isRegistryOneKey(entity.getAutorenewPollMessage())) {
domainBuilder.setAutorenewPollMessage(null);
hasChange = true;
}
if (isRegistryOneKey(entity.getDeletePollMessage())) {
domainBuilder.setDeletePollMessage(null);
hasChange = true;
}
if (hasChange) {
stageEntityChange(entity, domainBuilder.build());
}
}
private static boolean isRegistryOneKey(@Nullable VKey<?> vKey) {
if (vKey == null || vKey.getOfyKey() == null || vKey.getOfyKey().getParent() == null) {
return false;
}
Key<?> parentKey = vKey.getOfyKey().getParent();
return parentKey.getKind().equals("EntityGroupRoot") && parentKey.getName().equals("per-tld");
}
}
@@ -0,0 +1,69 @@
// 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.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Ascii;
import google.registry.privileges.secretmanager.SqlCredentialStore;
import google.registry.privileges.secretmanager.SqlUser;
import google.registry.privileges.secretmanager.SqlUser.RobotUser;
import google.registry.tools.params.PathParameter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
/**
* Command to create or update a Cloud SQL credential in the Secret Manager.
*
* <p>This command is a short-term tool that will be deprecated by the planned privilege server.
*/
@Parameters(
separators = " =",
commandDescription = "Create or update the Cloud SQL Credential for a given user")
public class SaveSqlCredentialCommand implements Command {
@Inject SqlCredentialStore store;
@Parameter(names = "--user", description = "The Cloud SQL user.", required = true)
private String user;
@Parameter(
names = {"--input"},
description =
"Name of input file for the password. If absent, command will prompt for "
+ "password in console.",
validateWith = PathParameter.InputFile.class)
private Path inputPath = null;
@Inject
SaveSqlCredentialCommand() {}
@Override
public void run() throws Exception {
String password = getPassword();
SqlUser sqlUser = new RobotUser(SqlUser.RobotId.valueOf(Ascii.toUpperCase(user)));
store.createOrUpdateCredential(sqlUser, password);
System.out.printf("\nDone:[%s]\n", password);
}
private String getPassword() throws Exception {
if (inputPath != null) {
return Files.readAllLines(inputPath, StandardCharsets.UTF_8).get(0);
}
return System.console().readLine("Please enter the password: ").trim();
}
}
@@ -0,0 +1,48 @@
// 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.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.Iterables;
import google.registry.schema.replay.SqlReplayCheckpoint;
import java.util.List;
import org.joda.time.DateTime;
/** Command to set {@link SqlReplayCheckpoint} to a particular, post-initial-population time. */
@Parameters(separators = " =", commandDescription = "Set SqlReplayCheckpoint to a particular time")
public class SetSqlReplayCheckpointCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
@Parameter(description = "Time to which SqlReplayCheckpoint will be set", required = true)
List<DateTime> mainParameters;
@Override
protected String prompt() {
checkArgument(mainParameters.size() == 1, "Must provide exactly one DateTime to set");
return String.format(
"Set SqlReplayCheckpoint to %s?", Iterables.getOnlyElement(mainParameters));
}
@Override
protected String execute() {
DateTime dateTime = Iterables.getOnlyElement(mainParameters);
jpaTm().transact(() -> SqlReplayCheckpoint.set(dateTime));
return String.format("Set SqlReplayCheckpoint time to %s", dateTime);
}
}
@@ -18,8 +18,8 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.partition;
import static com.google.common.collect.Streams.stream;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
@@ -105,13 +105,17 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
}
tokensToSave =
ofy().load().keys(getTokenKeys()).values().stream()
.collect(toImmutableMap(Function.identity(), this::updateToken))
.entrySet()
.stream()
.filter(entry -> !entry.getKey().equals(entry.getValue())) // only update changed tokens
.map(Map.Entry::getValue)
.collect(toImmutableSet());
transactIfJpaTm(
() ->
tm().load(getTokenKeys()).values().stream()
.collect(toImmutableMap(Function.identity(), this::updateToken))
.entrySet()
.stream()
.filter(
entry ->
!entry.getKey().equals(entry.getValue())) // only update changed tokens
.map(Map.Entry::getValue)
.collect(toImmutableSet()));
}
@Override
@@ -123,7 +127,7 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
protected String execute() {
long numUpdated =
stream(partition(tokensToSave, BATCH_SIZE))
.mapToLong(batch -> tm().transact(() -> saveBatch(batch)))
.mapToLong(batch -> tm().transact(() -> saveBatch(ImmutableList.copyOf(batch))))
.sum();
return String.format("Updated %d tokens in total.", numUpdated);
}
@@ -141,9 +145,9 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
return builder.build();
}
private long saveBatch(List<AllocationToken> batch) {
private long saveBatch(ImmutableList<AllocationToken> batch) {
if (!dryRun) {
ofy().save().entities(batch);
tm().putAll(batch);
}
System.out.printf(
"%s tokens: %s\n",
@@ -15,13 +15,15 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import com.beust.jcommander.Parameter;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.model.domain.token.AllocationToken;
import google.registry.persistence.VKey;
import java.util.List;
/** Shared base class for commands to update or delete allocation tokens. */
@@ -47,19 +49,28 @@ abstract class UpdateOrDeleteAllocationTokensCommand extends ConfirmingCommand
description = "Do not actually update or delete the tokens; defaults to false")
protected boolean dryRun;
protected ImmutableSet<Key<AllocationToken>> getTokenKeys() {
protected ImmutableSet<VKey<AllocationToken>> getTokenKeys() {
checkArgument(
tokens == null ^ prefix == null,
"Must provide one of --tokens or --prefix, not both / neither");
if (tokens != null) {
return tokens.stream()
.map(token -> Key.create(AllocationToken.class, token))
.collect(toImmutableSet());
ImmutableSet<VKey<AllocationToken>> keys =
tokens.stream()
.map(token -> VKey.create(AllocationToken.class, token))
.collect(toImmutableSet());
ImmutableSet<VKey<AllocationToken>> nonexistentKeys =
transactIfJpaTm(
() -> keys.stream().filter(key -> !tm().exists(key)).collect(toImmutableSet()));
checkState(nonexistentKeys.isEmpty(), "Tokens with keys %s did not exist.", nonexistentKeys);
return keys;
} else {
checkArgument(!prefix.isEmpty(), "Provided prefix should not be blank");
return ofy().load().type(AllocationToken.class).keys().list().stream()
.filter(key -> key.getName().startsWith(prefix))
.collect(toImmutableSet());
return transactIfJpaTm(
() ->
tm().loadAll(AllocationToken.class).stream()
.filter(token -> token.getToken().startsWith(prefix))
.map(AllocationToken::createVKey)
.collect(toImmutableSet()));
}
}
}
@@ -0,0 +1,216 @@
// 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.tools.javascrap;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableListMultimap.flatteningToImmutableListMultimap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import google.registry.beam.spec11.ThreatMatch;
import google.registry.model.domain.DomainBase;
import google.registry.model.reporting.Spec11ThreatMatch;
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
import google.registry.model.reporting.Spec11ThreatMatchDao;
import google.registry.reporting.spec11.RegistrarThreatMatches;
import google.registry.reporting.spec11.Spec11RegistrarThreatMatchesParser;
import google.registry.tools.CommandWithRemoteApi;
import google.registry.tools.ConfirmingCommand;
import google.registry.util.Clock;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import javax.inject.Inject;
import org.joda.time.LocalDate;
/**
* Scrap tool to backfill {@link Spec11ThreatMatch} objects from prior days.
*
* <p>This will load the previously-existing Spec11 files from GCS (looking back to 2019-01-01 (a
* rough estimate of when we started using this format) and convert those RegistrarThreatMatches
* objects into the new Spec11ThreatMatch format. It will then insert these entries into SQL.
*
* <p>Note that the script will attempt to find the corresponding {@link DomainBase} object for each
* domain name on the day of the scan. It will fail if it cannot find a corresponding domain object,
* or if the domain objects were not active at the time of the scan.
*/
@Parameters(
commandDescription =
"Backfills Spec11 threat match entries from the old and deprecated GCS JSON files to the "
+ "Cloud SQL database.")
public class BackfillSpec11ThreatMatchesCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
private static final LocalDate START_DATE = new LocalDate(2019, 1, 1);
@Parameter(
names = {"-o", "--overwrite_existing_dates"},
description =
"Whether the command will overwrite data that already exists for dates that exist in the "
+ "GCS bucket. Defaults to false.")
private boolean overrideExistingDates;
@Inject Spec11RegistrarThreatMatchesParser threatMatchesParser;
// Inject the clock for testing purposes
@Inject Clock clock;
@Override
protected String prompt() {
return String.format("Backfill Spec11 results from %d files?", getDatesToBackfill().size());
}
@Override
protected String execute() {
ImmutableList<LocalDate> dates = getDatesToBackfill();
ImmutableListMultimap.Builder<LocalDate, RegistrarThreatMatches> threatMatchesBuilder =
new ImmutableListMultimap.Builder<>();
for (LocalDate date : dates) {
try {
// It's OK if the file doesn't exist for a particular date; the result will be empty.
threatMatchesBuilder.putAll(date, threatMatchesParser.getRegistrarThreatMatches(date));
} catch (IOException e) {
throw new RuntimeException(
String.format("Error parsing through file with date %s.", date), e);
}
}
ImmutableListMultimap<LocalDate, RegistrarThreatMatches> threatMatches =
threatMatchesBuilder.build();
// Look up all possible DomainBases for these domain names, any of which can be in the past
ImmutableListMultimap<String, DomainBase> domainsByDomainName =
getDomainsByDomainName(threatMatches);
// For each date, convert all threat matches with the proper domain repo ID
int totalNumThreats = 0;
for (LocalDate date : threatMatches.keySet()) {
ImmutableList.Builder<Spec11ThreatMatch> spec11ThreatsBuilder = new ImmutableList.Builder<>();
for (RegistrarThreatMatches rtm : threatMatches.get(date)) {
rtm.threatMatches().stream()
.map(
threatMatch ->
threatMatchToCloudSqlObject(
threatMatch, date, rtm.clientId(), domainsByDomainName))
.forEach(spec11ThreatsBuilder::add);
}
ImmutableList<Spec11ThreatMatch> spec11Threats = spec11ThreatsBuilder.build();
jpaTm()
.transact(
() -> {
Spec11ThreatMatchDao.deleteEntriesByDate(jpaTm(), date);
jpaTm().putAll(spec11Threats);
});
totalNumThreats += spec11Threats.size();
}
return String.format(
"Successfully parsed through %d files with %d threats.", dates.size(), totalNumThreats);
}
/** Returns a per-domain list of possible DomainBase objects, starting with the most recent. */
private ImmutableListMultimap<String, DomainBase> getDomainsByDomainName(
ImmutableListMultimap<LocalDate, RegistrarThreatMatches> threatMatchesByDate) {
return threatMatchesByDate.values().stream()
.map(RegistrarThreatMatches::threatMatches)
.flatMap(ImmutableList::stream)
.map(ThreatMatch::fullyQualifiedDomainName)
.distinct()
.collect(
flatteningToImmutableListMultimap(
Function.identity(),
(domainName) -> {
List<DomainBase> domains =
ofy()
.load()
.type(DomainBase.class)
.filter("fullyQualifiedDomainName", domainName)
.list();
domains.sort(Comparator.comparing(DomainBase::getCreationTime).reversed());
checkState(
!domains.isEmpty(),
"Domain name %s had no associated DomainBase objects.",
domainName);
return domains.stream();
}));
}
/** Converts the previous {@link ThreatMatch} object to {@link Spec11ThreatMatch}. */
private Spec11ThreatMatch threatMatchToCloudSqlObject(
ThreatMatch threatMatch,
LocalDate date,
String registrarId,
ImmutableListMultimap<String, DomainBase> domainsByDomainName) {
DomainBase domain =
findDomainAsOfDateOrThrow(
threatMatch.fullyQualifiedDomainName(), date, domainsByDomainName);
return new Spec11ThreatMatch.Builder()
.setThreatTypes(ImmutableSet.of(ThreatType.valueOf(threatMatch.threatType())))
.setCheckDate(date)
.setRegistrarId(registrarId)
.setDomainName(threatMatch.fullyQualifiedDomainName())
.setDomainRepoId(domain.getRepoId())
.build();
}
/** Returns the DomainBase object as of the particular date, which is likely in the past. */
private DomainBase findDomainAsOfDateOrThrow(
String domainName,
LocalDate date,
ImmutableListMultimap<String, DomainBase> domainsByDomainName) {
ImmutableList<DomainBase> domains = domainsByDomainName.get(domainName);
for (DomainBase domain : domains) {
// We only know the date (not datetime) of the threat scan, so we approximate
LocalDate creationDate = domain.getCreationTime().toLocalDate();
LocalDate deletionDate = domain.getDeletionTime().toLocalDate();
if (!date.isBefore(creationDate) && !date.isAfter(deletionDate)) {
return domain;
}
}
throw new IllegalStateException(
String.format("Could not find a DomainBase valid for %s on day %s.", domainName, date));
}
/** Returns the list of dates between {@link #START_DATE} and now (UTC), inclusive. */
private ImmutableList<LocalDate> getDatesToBackfill() {
ImmutableSet<LocalDate> datesToSkip =
overrideExistingDates ? ImmutableSet.of() : getExistingDates();
ImmutableList.Builder<LocalDate> result = new ImmutableList.Builder<>();
LocalDate endDate = clock.nowUtc().toLocalDate();
for (LocalDate currentDate = START_DATE;
!currentDate.isAfter(endDate);
currentDate = currentDate.plusDays(1)) {
if (!datesToSkip.contains(currentDate)) {
result.add(currentDate);
}
}
return result.build();
}
private ImmutableSet<LocalDate> getExistingDates() {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"SELECT DISTINCT stm.checkDate FROM Spec11ThreatMatch stm", LocalDate.class)
.getResultStream()
.collect(toImmutableSet()));
}
}
@@ -55,6 +55,19 @@ class ResaveAllEppResourcesActionTest extends MapreduceTestCase<ResaveAllEppReso
.isGreaterThan(creationTime);
}
@Test
void test_fastMode_doesNotResaveEntityWithNoChanges() throws Exception {
ContactResource contact = persistActiveContact("test123");
DateTime creationTime = contact.getUpdateTimestamp().getTimestamp();
assertThat(ofy().load().entity(contact).now().getUpdateTimestamp().getTimestamp())
.isEqualTo(creationTime);
ofy().clearSessionCache();
action.isFast = true;
runMapreduce();
assertThat(ofy().load().entity(contact).now().getUpdateTimestamp().getTimestamp())
.isEqualTo(creationTime);
}
@Test
void test_mapreduceResolvesPendingTransfer() throws Exception {
DateTime now = DateTime.now(UTC);
@@ -20,7 +20,6 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
@@ -285,8 +284,7 @@ class Spec11PipelineTest {
.build();
verify(mockJpaTm).transact(any(Runnable.class));
verify(mockJpaTm).insert(expected);
verifyNoMoreInteractions(mockJpaTm);
verify(mockJpaTm).putAll(ImmutableList.of(expected));
}
/**
@@ -100,7 +100,11 @@ public abstract class FlowTestCase<F extends Flow> {
@RegisterExtension
final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
AppEngineExtension.builder()
.withClock(clock)
.withDatastoreAndCloudSql()
.withTaskQueue()
.build();
@BeforeEach
public void beforeEachFlowTestCase() {
@@ -288,7 +292,7 @@ public abstract class FlowTestCase<F extends Flow> {
e);
}
// Clear the cache so that we don't see stale results in tests.
ofy().clearSessionCache();
tm().clearSessionCache();
return output;
}
@@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.tmch.ClaimsListShardTest.createTestClaimsListShard;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
@@ -72,7 +74,7 @@ public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource
protected R reloadResourceByForeignKey(DateTime now) throws Exception {
// Force the session to be cleared so that when we read it back, we read from Datastore and not
// from the transaction's session cache.
ofy().clearSessionCache();
tm().clearSessionCache();
return loadByForeignKey(getResourceClass(), getUniqueIdFromCommand(), now).orElse(null);
}
@@ -83,9 +85,9 @@ public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource
protected <T extends EppResource> T reloadResourceAndCloneAtTime(T resource, DateTime now) {
// Force the session to be cleared.
ofy().clearSessionCache();
tm().clearSessionCache();
@SuppressWarnings("unchecked")
T refreshedResource = (T) ofy().load().entity(resource).now().cloneProjectedAtTime(now);
T refreshedResource = (T) transactIfJpaTm(() -> tm().load(resource)).cloneProjectedAtTime(now);
return refreshedResource;
}
@@ -71,7 +71,6 @@ import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.DomainHistoryVKey;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
@@ -169,7 +168,7 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryEntry(DomainHistoryVKey.create(historyEntryKey))
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey))
.build());
doCheckTest(
create(false, "example1.tld", "In use"),
@@ -162,7 +162,6 @@ import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.persistence.DomainHistoryVKey;
import google.registry.testing.ReplayExtension;
import google.registry.testing.TaskQueueHelper.TaskMatcher;
import java.math.BigDecimal;
@@ -505,7 +504,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryEntry(DomainHistoryVKey.create(historyEntryKey))
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey))
.build());
clock.advanceOneMilli();
EppException thrown =
@@ -528,7 +527,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
HistoryEntry historyEntry =
ofy().load().type(HistoryEntry.class).ancestor(reloadResourceByForeignKey()).first().now();
assertThat(ofy().load().entity(token).now().getRedemptionHistoryEntry())
.hasValue(DomainHistoryVKey.create(Key.create(historyEntry)));
.hasValue(HistoryEntry.createVKey(Key.create(historyEntry)));
}
@Test
@@ -1275,7 +1274,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertThat(reloadedToken.isRedeemed()).isTrue();
assertThat(reloadedToken.getRedemptionHistoryEntry())
.hasValue(
DomainHistoryVKey.create(
HistoryEntry.createVKey(
Key.create(getHistoryEntries(reloadResourceByForeignKey()).get(0))));
}
@@ -49,7 +49,6 @@ import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.DomainHistoryVKey;
import google.registry.testing.AppEngineExtension;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -198,7 +197,7 @@ class AllocationTokenFlowUtilsTest {
new AllocationToken.Builder()
.setToken("tokeN")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryEntry(DomainHistoryVKey.create(historyEntryKey))
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey))
.build());
assertThat(
flowUtils
@@ -24,16 +24,18 @@ import google.registry.flows.EppException;
import google.registry.flows.ResourceCheckFlowTestCase;
import google.registry.flows.exceptions.TooManyResourceChecksException;
import google.registry.model.host.HostResource;
import org.junit.jupiter.api.Test;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
/** Unit tests for {@link HostCheckFlow}. */
@DualDatabaseTest
class HostCheckFlowTest extends ResourceCheckFlowTestCase<HostCheckFlow, HostResource> {
HostCheckFlowTest() {
setEppInput("host_check.xml");
}
@Test
@TestOfyAndSql
void testNothingExists() throws Exception {
// These ids come from the check xml.
doCheckTest(
@@ -42,7 +44,7 @@ class HostCheckFlowTest extends ResourceCheckFlowTestCase<HostCheckFlow, HostRes
create(true, "ns3.example.tld", null));
}
@Test
@TestOfyAndSql
void testOneExists() throws Exception {
persistActiveHost("ns1.example.tld");
// These ids come from the check xml.
@@ -52,7 +54,7 @@ class HostCheckFlowTest extends ResourceCheckFlowTestCase<HostCheckFlow, HostRes
create(true, "ns3.example.tld", null));
}
@Test
@TestOfyAndSql
void testOneExistsButWasDeleted() throws Exception {
persistDeletedHost("ns1.example.tld", clock.nowUtc().minusDays(1));
// These ids come from the check xml.
@@ -62,27 +64,27 @@ class HostCheckFlowTest extends ResourceCheckFlowTestCase<HostCheckFlow, HostRes
create(true, "ns3.example.tld", null));
}
@Test
@TestOfyAndSql
void testXmlMatches() throws Exception {
persistActiveHost("ns2.example.tld");
runFlowAssertResponse(loadFile("host_check_response.xml"));
}
@Test
@TestOfyAndSql
void test50IdsAllowed() throws Exception {
// Make sure we don't have a regression that reduces the number of allowed checks.
setEppInput("host_check_50.xml");
runFlow();
}
@Test
@TestOfyAndSql
void testTooManyIds() {
setEppInput("host_check_51.xml");
EppException thrown = assertThrows(TooManyResourceChecksException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-host-check");
@@ -16,6 +16,7 @@ package google.registry.flows.host;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.createTlds;
@@ -53,10 +54,12 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link HostCreateFlow}. */
@DualDatabaseTest
class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResource> {
private void setEppHostCreateInput(String hostName, String hostAddrs) {
@@ -90,7 +93,9 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
.hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.HOST_CREATE);
assertNoBillingEvents();
assertEppResourceIndexEntityFor(reloadResourceByForeignKey());
if (tm().isOfy()) {
assertEppResourceIndexEntityFor(reloadResourceByForeignKey());
}
}
private void doSuccessfulInternalTest(String tld) throws Exception {
@@ -100,19 +105,19 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
doSuccessfulTest();
}
@Test
@TestOfyAndSql
void testDryRun() throws Exception {
dryRunFlowAssertResponse(loadFile("host_create_response.xml"));
}
@Test
@TestOfyAndSql
void testSuccess_externalNeverExisted() throws Exception {
doSuccessfulTest();
assertAboutHosts().that(reloadResourceByForeignKey()).hasSuperordinateDomain(null);
assertNoDnsTasksEnqueued();
}
@Test
@TestOfyAndSql
void testSuccess_internalNeverExisted() throws Exception {
doSuccessfulInternalTest("tld");
HostResource host = reloadResourceByForeignKey();
@@ -123,7 +128,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertDnsTasksEnqueued("ns1.example.tld");
}
@Test
@TestOfyAndSql
void testFailure_multipartTLDsAndInvalidHost() {
createTlds("bar.tld", "tld");
@@ -132,7 +137,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testSuccess_externalExistedButWasDeleted() throws Exception {
persistDeletedHost(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
doSuccessfulTest();
@@ -140,7 +145,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertNoDnsTasksEnqueued();
}
@Test
@TestOfyAndSql
void testSuccess_internalExistedButWasDeleted() throws Exception {
persistDeletedHost(getUniqueIdFromCommand(), clock.nowUtc().minusDays(1));
doSuccessfulInternalTest("tld");
@@ -152,7 +157,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertDnsTasksEnqueued("ns1.example.tld");
}
@Test
@TestOfyAndSql
void testFailure_subordinateNeedsIps() {
setEppHostCreateInput("ns1.example.tld", null);
createTld("tld");
@@ -161,7 +166,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_externalMustNotHaveIps() {
setEppHostCreateInputWithIps("ns1.example.external");
createTld("tld");
@@ -170,7 +175,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_superordinateMissing() {
setEppHostCreateInput("ns1.example.tld", null);
createTld("tld");
@@ -179,7 +184,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertThat(thrown).hasMessageThat().contains("(example.tld)");
}
@Test
@TestOfyAndSql
void testFailure_superordinateInPendingDelete() {
setEppHostCreateInputWithIps("ns1.example.tld");
createTld("tld");
@@ -197,7 +202,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
.contains("Superordinate domain for this hostname is in pending delete");
}
@Test
@TestOfyAndSql
void testFailure_alreadyExists() throws Exception {
setEppHostCreateInput("ns1.example.tld", null);
persistActiveHost(getUniqueIdFromCommand());
@@ -209,7 +214,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
String.format("Object with given ID (%s) already exists", getUniqueIdFromCommand()));
}
@Test
@TestOfyAndSql
void testFailure_resourceContention() throws Exception {
setEppHostCreateInput("ns1.example.tld", null);
String targetId = getUniqueIdFromCommand();
@@ -226,14 +231,14 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_nonLowerCaseHostname() {
setEppHostCreateInput("ns1.EXAMPLE.tld", null);
EppException thrown = assertThrows(HostNameNotLowerCaseException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_nonPunyCodedHostname() {
setEppHostCreateInput("ns1.çauçalito.みんな", null);
HostNameNotPunyCodedException thrown =
@@ -241,21 +246,21 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertThat(thrown).hasMessageThat().contains("expected ns1.xn--aualito-txac.xn--q9jyb4c");
}
@Test
@TestOfyAndSql
void testFailure_nonCanonicalHostname() {
setEppHostCreateInput("ns1.example.tld.", null);
EppException thrown = assertThrows(HostNameNotNormalizedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_longHostName() {
setEppHostCreateInputWithIps("a" + Strings.repeat(".labelpart", 25) + ".tld");
EppException thrown = assertThrows(HostNameTooLongException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_ip4AddressWithIp6Declaration() {
setEppHostCreateInput(
"ns1.example.tld",
@@ -272,37 +277,37 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_badCharacter() {
doFailingHostNameTest("foo bar", InvalidHostNameException.class);
}
@Test
@TestOfyAndSql
void testFailure_tooShallowPublicSuffix() {
doFailingHostNameTest("example.tld", HostNameTooShallowException.class);
}
@Test
@TestOfyAndSql
void testFailure_tooShallowCcTld() {
doFailingHostNameTest("foo.co.uk", HostNameTooShallowException.class);
}
@Test
@TestOfyAndSql
void testFailure_barePublicSuffix() {
doFailingHostNameTest("com", HostNameTooShallowException.class);
}
@Test
@TestOfyAndSql
void testFailure_bareCcTld() {
doFailingHostNameTest("co.uk", HostNameTooShallowException.class);
}
@Test
@TestOfyAndSql
void testFailure_tooShallowNewTld() {
doFailingHostNameTest("example.lol", HostNameTooShallowException.class);
}
@Test
@TestOfyAndSql
void testFailure_ccTldInBailiwick() {
createTld("co.uk");
setEppHostCreateInputWithIps("foo.co.uk");
@@ -310,7 +315,7 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, HostResour
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-host-create");
@@ -26,66 +26,68 @@ import google.registry.flows.host.HostFlowUtils.HostNameTooLongException;
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.Test;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link HostFlowUtils}. */
@DualDatabaseTest
class HostFlowUtilsTest {
@RegisterExtension
final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
@Test
@TestOfyAndSql
void test_validExternalHostName_validates() throws Exception {
assertThat(validateHostName("host.example.com").toString()).isEqualTo("host.example.com");
}
@Test
@TestOfyAndSql
void test_validExternalHostNameOnRegistrySuffixList_validates() throws Exception {
assertThat(validateHostName("host.blogspot.com").toString()).isEqualTo("host.blogspot.com");
}
@Test
@TestOfyAndSql
void test_validExternalHostNameOnRegistrySuffixList_multipartTLD_validates() throws Exception {
assertThat(validateHostName("ns1.host.co.uk").toString()).isEqualTo("ns1.host.co.uk");
}
@Test
@TestOfyAndSql
void test_validExternalHostNameOnRegistrySuffixList_multipartTLD_tooShallow() {
assertThrows(
HostNameTooShallowException.class, () -> validateHostName("host.co.uk").toString());
}
@Test
@TestOfyAndSql
void test_validateHostName_hostNameTooLong() {
assertThrows(
HostNameTooLongException.class,
() -> validateHostName(Strings.repeat("na", 200) + ".wat.man"));
}
@Test
@TestOfyAndSql
void test_validateHostName_hostNameNotLowerCase() {
assertThrows(HostNameNotLowerCaseException.class, () -> validateHostName("NA.CAPS.TLD"));
}
@Test
@TestOfyAndSql
void test_validateHostName_hostNameNotPunyCoded() {
assertThrows(
HostNameNotPunyCodedException.class, () -> validateHostName("motörhead.death.metal"));
}
@Test
@TestOfyAndSql
void test_validateHostName_hostNameNotNormalized() {
assertThrows(HostNameNotNormalizedException.class, () -> validateHostName("root.node.yeah."));
}
@Test
@TestOfyAndSql
void test_validateHostName_hostNameHasLeadingHyphen() {
assertThrows(InvalidHostNameException.class, () -> validateHostName("-giga.mega.tld"));
}
@Test
@TestOfyAndSql
void test_validateHostName_hostNameTooShallow() {
assertThrows(HostNameTooShallowException.class, () -> validateHostName("domain.tld"));
}
@@ -28,6 +28,7 @@ import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.annotation.Serialize;
import com.googlecode.objectify.cmd.Query;
import google.registry.model.ofy.Ofy;
import google.registry.persistence.EppHistoryVKey;
import google.registry.persistence.VKey;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
@@ -173,7 +174,8 @@ public abstract class EntityTestCase {
}
// Descend into persisted ImmutableObject classes, but not anything else.
if (ImmutableObject.class.isAssignableFrom(fieldClass)
&& !VKey.class.isAssignableFrom(fieldClass)) {
&& !VKey.class.isAssignableFrom(fieldClass)
&& !EppHistoryVKey.class.isAssignableFrom(fieldClass)) {
getAllPotentiallyIndexedFieldPaths(fieldClass).stream()
.map(subfield -> field.getName() + "." + subfield)
.distinct()
@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newHostResource;
import static google.registry.testing.DatabaseHelper.persistNewRegistrars;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistResourceWithCommitLog;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -26,16 +27,19 @@ import static org.joda.time.DateTimeZone.UTC;
import google.registry.model.host.HostResource;
import google.registry.model.ofy.Ofy;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link EppResourceUtils}. */
public class EppResourceUtilsTest {
@DualDatabaseTest
class EppResourceUtilsTest {
@RegisterExtension
public final AppEngineExtension appEngine =
@@ -51,7 +55,7 @@ public class EppResourceUtilsTest {
inject.setStaticField(Ofy.class, "clock", clock);
}
@Test
@TestOfyAndSql
void testLoadAtPointInTime_beforeCreated_returnsNull() {
clock.advanceOneMilli();
// Don't save a commit log, we shouldn't need one.
@@ -62,7 +66,7 @@ public class EppResourceUtilsTest {
assertThat(loadAtPointInTime(host, clock.nowUtc().minus(Duration.millis(1))).now()).isNull();
}
@Test
@TestOfyAndSql
void testLoadAtPointInTime_atOrAfterLastAutoUpdateTime_returnsResource() {
clock.advanceOneMilli();
// Don't save a commit log, we shouldn't need one.
@@ -73,8 +77,9 @@ public class EppResourceUtilsTest {
assertThat(loadAtPointInTime(host, clock.nowUtc()).now()).isEqualTo(host);
}
@Test
@TestOfyOnly
void testLoadAtPointInTime_usingIntactRevisionHistory_returnsMutationValue() {
persistNewRegistrars("OLD", "NEW");
clock.advanceOneMilli();
// Save resource with a commit log that we can read in later as a revisions map value.
HostResource oldHost = persistResourceWithCommitLog(
@@ -94,7 +99,7 @@ public class EppResourceUtilsTest {
.isEqualTo(oldHost);
}
@Test
@TestOfyOnly
void testLoadAtPointInTime_brokenRevisionHistory_returnsResourceAsIs() {
// Don't save a commit log since we want to test the handling of a broken revisions key.
HostResource oldHost = persistResource(
@@ -114,7 +119,7 @@ public class EppResourceUtilsTest {
assertThat(loadAtPointInTime(host, clock.nowUtc().minusMillis(1)).now()).isEqualTo(host);
}
@Test
@TestOfyOnly
void testLoadAtPointInTime_fallback_returnsMutationValueForOldestRevision() {
clock.advanceOneMilli();
// Save a commit log that we can fall back to.
@@ -136,7 +141,7 @@ public class EppResourceUtilsTest {
.isEqualTo(oldHost);
}
@Test
@TestOfyOnly
void testLoadAtPointInTime_ultimateFallback_onlyOneRevision_returnsCurrentResource() {
clock.advanceOneMilli();
// Don't save a commit log; we want to test that we load from the current resource.
@@ -151,7 +156,7 @@ public class EppResourceUtilsTest {
assertThat(loadAtPointInTime(host, clock.nowUtc().minusMillis(1)).now()).isEqualTo(host);
}
@Test
@TestOfyOnly
void testLoadAtPointInTime_moreThanThirtyDaysInPast_historyIsPurged() {
clock.advanceOneMilli();
HostResource host =
@@ -15,64 +15,106 @@
package google.registry.model;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.common.CrossTldSingleton;
import google.registry.persistence.VKey;
import google.registry.schema.replay.EntityTest.EntityForTesting;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link UpdateAutoTimestamp}. */
@DualDatabaseTest
public class UpdateAutoTimestampTest {
FakeClock clock = new FakeClock();
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder()
.withDatastoreAndCloudSql()
.withJpaUnitTestEntities(UpdateAutoTimestampTestObject.class)
.withOfyTestEntities(UpdateAutoTimestampTestObject.class)
.withClock(clock)
.build();
/** Timestamped class. */
@Entity(name = "UatTestEntity")
@javax.persistence.Entity
@EntityForTesting
public static class UpdateAutoTimestampTestObject extends CrossTldSingleton {
@Ignore @javax.persistence.Id long id = SINGLETON_ID;
UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null);
}
private UpdateAutoTimestampTestObject reload() {
return ofy().load().entity(new UpdateAutoTimestampTestObject()).now();
return tm().transact(
() ->
tm().load(
VKey.create(
UpdateAutoTimestampTestObject.class,
1L,
Key.create(new UpdateAutoTimestampTestObject()))));
}
@Test
@TestOfyAndSql
void testSaveSetsTime() {
DateTime transactionTime =
tm().transact(
() -> {
UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject();
assertThat(object.updateTime.timestamp).isNull();
ofy().save().entity(object);
tm().insert(object);
return tm().getTransactionTime();
});
ofy().clearSessionCache();
tm().clearSessionCache();
assertThat(reload().updateTime.timestamp).isEqualTo(transactionTime);
}
@Test
@TestOfyAndSql
void testDisabledUpdates() throws Exception {
DateTime initialTime =
tm().transact(
() -> {
tm().insert(new UpdateAutoTimestampTestObject());
return tm().getTransactionTime();
});
UpdateAutoTimestampTestObject object = reload();
clock.advanceOneMilli();
try (UpdateAutoTimestamp.DisableAutoUpdateResource disabler =
new UpdateAutoTimestamp.DisableAutoUpdateResource()) {
DateTime secondTransactionTime =
tm().transact(
() -> {
tm().put(object);
return tm().getTransactionTime();
});
assertThat(secondTransactionTime).isGreaterThan(initialTime);
}
assertThat(reload().updateTime.timestamp).isEqualTo(initialTime);
}
@TestOfyAndSql
void testResavingOverwritesOriginalTime() {
DateTime transactionTime =
tm().transact(
() -> {
UpdateAutoTimestampTestObject object = new UpdateAutoTimestampTestObject();
object.updateTime = UpdateAutoTimestamp.create(DateTime.now(UTC).minusDays(1));
ofy().save().entity(object);
tm().insert(object);
return tm().getTransactionTime();
});
ofy().clearSessionCache();
tm().clearSessionCache();
assertThat(reload().updateTime.timestamp).isEqualTo(transactionTime);
}
}
@@ -84,10 +84,10 @@ public class DomainBaseSqlTest {
saveRegistrar("registrar1");
saveRegistrar("registrar2");
saveRegistrar("registrar3");
contactKey = VKey.createSql(ContactResource.class, "contact_id1");
contact2Key = VKey.createSql(ContactResource.class, "contact_id2");
contactKey = createKey(ContactResource.class, "contact_id1");
contact2Key = createKey(ContactResource.class, "contact_id2");
host1VKey = VKey.createSql(HostResource.class, "host1");
host1VKey = createKey(HostResource.class, "host1");
domain =
new DomainBase.Builder()
@@ -633,14 +633,13 @@ public class DomainBaseSqlTest {
"4-COM",
END_OF_TIME,
"registrar1",
createLegacyVKey(
BillingEvent.OneTime.class, oneTimeBillingEvent.getId())),
oneTimeBillingEvent.createVKey()),
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
"4-COM",
END_OF_TIME,
"registrar1",
createLegacyVKey(BillingEvent.Recurring.class, billEvent.getId())));
billEvent.createVKey()));
jpaTm().insert(contact);
jpaTm().insert(contact2);
@@ -696,6 +695,10 @@ public class DomainBaseSqlTest {
assertThat(domain.getGracePeriods()).isEqualTo(gracePeriods);
}
private <T> VKey<T> createKey(Class<T> clazz, String name) {
return VKey.create(clazz, name, Key.create(clazz, name));
}
private <T> VKey<T> createLegacyVKey(Class<T> clazz, long id) {
return VKey.create(
clazz, id, Key.create(Key.create(EntityGroupRoot.class, "per-tld"), clazz, id));
@@ -875,16 +875,13 @@ public class DomainBaseTest extends EntityTestCase {
ImmutableSet<BillEventInfo> historyIds =
domain.getGracePeriods().stream()
.map(
gp ->
new BillEventInfo(
gp.getRecurringBillingEvent(), gp.billingEventRecurringHistoryId,
gp.getOneTimeBillingEvent(), gp.billingEventOneTimeHistoryId))
gp -> new BillEventInfo(gp.getRecurringBillingEvent(), gp.getOneTimeBillingEvent()))
.collect(toImmutableSet());
assertThat(historyIds)
.isEqualTo(
ImmutableSet.of(
new BillEventInfo(null, null, oneTimeBillKey, historyEntryKey.getId()),
new BillEventInfo(recurringBillKey, historyEntryKey.getId(), null, null)));
new BillEventInfo(null, oneTimeBillKey),
new BillEventInfo(recurringBillKey, null)));
}
static class BillEventInfo extends ImmutableObject {
@@ -895,13 +892,9 @@ public class DomainBaseTest extends EntityTestCase {
BillEventInfo(
VKey<BillingEvent.Recurring> billingEventRecurring,
Long billingEventRecurringHistoryId,
VKey<BillingEvent.OneTime> billingEventOneTime,
Long billingEventOneTimeHistoryId) {
VKey<BillingEvent.OneTime> billingEventOneTime) {
this.billingEventRecurring = billingEventRecurring;
this.billingEventRecurringHistoryId = billingEventRecurringHistoryId;
this.billingEventOneTime = billingEventOneTime;
this.billingEventOneTimeHistoryId = billingEventOneTimeHistoryId;
}
}
}
@@ -63,11 +63,11 @@ public class GracePeriodTest {
recurringKey =
VKey.create(
Recurring.class,
12345,
12345L,
Key.create(
Key.create(Key.create(DomainBase.class, "1-TEST"), HistoryEntry.class, 343L),
Recurring.class,
12345));
12345L));
}
@Test
@@ -80,8 +80,6 @@ public class GracePeriodTest {
assertThat(gracePeriod.getClientId()).isEqualTo("TheRegistrar");
assertThat(gracePeriod.getExpirationTime()).isEqualTo(now.plusDays(1));
assertThat(gracePeriod.hasBillingEvent()).isTrue();
assertThat(gracePeriod.billingEventOneTimeHistoryId).isEqualTo(12345L);
assertThat(gracePeriod.billingEventRecurringHistoryId).isNull();
}
@Test
@@ -96,8 +94,6 @@ public class GracePeriodTest {
assertThat(gracePeriod.getClientId()).isEqualTo("TheRegistrar");
assertThat(gracePeriod.getExpirationTime()).isEqualTo(now.plusDays(1));
assertThat(gracePeriod.hasBillingEvent()).isTrue();
assertThat(gracePeriod.billingEventOneTimeHistoryId).isNull();
assertThat(gracePeriod.billingEventRecurringHistoryId).isEqualTo(343L);
}
@Test
@@ -16,15 +16,14 @@ package google.registry.model.domain.token;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.CANCELLED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.ENDED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.NOT_STARTED;
import static google.registry.model.domain.token.AllocationToken.TokenStatus.VALID;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -40,13 +39,14 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.DomainHistoryVKey;
import google.registry.persistence.VKey;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link AllocationToken}. */
@DualDatabaseTest
public class AllocationTokenTest extends EntityTestCase {
public AllocationTokenTest() {
@@ -58,7 +58,7 @@ public class AllocationTokenTest extends EntityTestCase {
createTld("foo");
}
@Test
@TestOfyAndSql
void testPersistence() {
AllocationToken unlimitedUseToken =
persistResource(
@@ -78,7 +78,7 @@ public class AllocationTokenTest extends EntityTestCase {
.put(DateTime.now(UTC).plusWeeks(8), TokenStatus.ENDED)
.build())
.build());
assertThat(ofy().load().entity(unlimitedUseToken).now()).isEqualTo(unlimitedUseToken);
assertThat(transactIfJpaTm(() -> tm().load(unlimitedUseToken))).isEqualTo(unlimitedUseToken);
DomainBase domain = persistActiveDomain("example.foo");
Key<HistoryEntry> historyEntryKey = Key.create(Key.create(domain), HistoryEntry.class, 1);
@@ -86,37 +86,15 @@ public class AllocationTokenTest extends EntityTestCase {
persistResource(
new AllocationToken.Builder()
.setToken("abc123Single")
.setRedemptionHistoryEntry(DomainHistoryVKey.create(historyEntryKey))
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey))
.setDomainName("example.foo")
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.setTokenType(SINGLE_USE)
.build());
assertThat(ofy().load().entity(singleUseToken).now()).isEqualTo(singleUseToken);
jpaTm()
.transact(
() -> {
jpaTm().insert(unlimitedUseToken);
jpaTm().insert(singleUseToken);
});
jpaTm()
.transact(
() -> {
assertAboutImmutableObjects()
.that(jpaTm().load(VKey.createSql(AllocationToken.class, "abc123Unlimited")))
.isEqualExceptFields(
unlimitedUseToken,
"creationTime",
"updateTimestamp",
"redemptionHistoryEntry");
assertAboutImmutableObjects()
.that(jpaTm().load(VKey.createSql(AllocationToken.class, "abc123Single")))
.isEqualExceptFields(
singleUseToken, "creationTime", "updateTimestamp", "redemptionHistoryEntry");
});
assertThat(transactIfJpaTm(() -> tm().load(singleUseToken))).isEqualTo(singleUseToken);
}
@Test
@TestOfyOnly
void testIndexing() throws Exception {
DomainBase domain = persistActiveDomain("blahdomain.foo");
Key<HistoryEntry> historyEntryKey = Key.create(Key.create(domain), HistoryEntry.class, 1);
@@ -125,7 +103,7 @@ public class AllocationTokenTest extends EntityTestCase {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRedemptionHistoryEntry(DomainHistoryVKey.create(historyEntryKey))
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey))
.setDomainName("blahdomain.foo")
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.build()),
@@ -134,7 +112,7 @@ public class AllocationTokenTest extends EntityTestCase {
"domainName");
}
@Test
@TestOfyAndSql
void testCreationTime_autoPopulates() {
AllocationToken tokenBeforePersisting =
new AllocationToken.Builder().setToken("abc123").setTokenType(SINGLE_USE).build();
@@ -143,7 +121,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(tokenAfterPersisting.getCreationTime()).hasValue(fakeClock.nowUtc());
}
@Test
@TestOfyAndSql
void testSetCreationTime_cantCallMoreThanOnce() {
AllocationToken.Builder builder =
new AllocationToken.Builder()
@@ -157,7 +135,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Creation time can only be set once");
}
@Test
@TestOfyAndSql
void testSetToken_cantCallMoreThanOnce() {
AllocationToken.Builder builder = new AllocationToken.Builder().setToken("foobar");
IllegalStateException thrown =
@@ -165,7 +143,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Token can only be set once");
}
@Test
@TestOfyAndSql
void testSetTokenType_cantCallMoreThanOnce() {
AllocationToken.Builder builder =
new AllocationToken.Builder().setTokenType(TokenType.UNLIMITED_USE);
@@ -174,7 +152,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Token type can only be set once");
}
@Test
@TestOfyAndSql
void testBuild_DomainNameWithLessThanTwoParts() {
IllegalArgumentException thrown =
assertThrows(
@@ -192,7 +170,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Invalid domain name: example");
}
@Test
@TestOfyAndSql
void testBuild_invalidTld() {
IllegalArgumentException thrown =
assertThrows(
@@ -210,7 +188,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Invalid domain name: example.nosuchtld");
}
@Test
@TestOfyAndSql
void testBuild_domainNameOnlyOnSingleUse() {
AllocationToken.Builder builder =
new AllocationToken.Builder()
@@ -223,7 +201,7 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Domain name can only be specified for SINGLE_USE tokens");
}
@Test
@TestOfyAndSql
void testBuild_redemptionHistoryEntryOnlyInSingleUse() {
DomainBase domain = persistActiveDomain("blahdomain.foo");
Key<HistoryEntry> historyEntryKey = Key.create(Key.create(domain), HistoryEntry.class, 1);
@@ -231,14 +209,14 @@ public class AllocationTokenTest extends EntityTestCase {
new AllocationToken.Builder()
.setToken("foobar")
.setTokenType(TokenType.UNLIMITED_USE)
.setRedemptionHistoryEntry(DomainHistoryVKey.create(historyEntryKey));
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey));
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Redemption history entry can only be specified for SINGLE_USE tokens");
}
@Test
@TestOfyAndSql
void testSetTransitions_notStartOfTime() {
IllegalArgumentException thrown =
assertThrows(
@@ -256,7 +234,7 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("tokenStatusTransitions map must start at START_OF_TIME.");
}
@Test
@TestOfyAndSql
void testSetTransitions_badInitialValue() {
IllegalArgumentException thrown =
assertThrows(
@@ -273,14 +251,14 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("tokenStatusTransitions must start with NOT_STARTED");
}
@Test
@TestOfyAndSql
void testSetTransitions_invalidInitialTransitions() {
// NOT_STARTED can only go to VALID or CANCELLED
assertBadInitialTransition(NOT_STARTED);
assertBadInitialTransition(ENDED);
}
@Test
@TestOfyAndSql
void testSetTransitions_badTransitionsFromValid() {
// VALID can only go to ENDED or CANCELLED
assertBadTransition(
@@ -301,14 +279,14 @@ public class AllocationTokenTest extends EntityTestCase {
NOT_STARTED);
}
@Test
@TestOfyAndSql
void testSetTransitions_terminalTransitions() {
// both ENDED and CANCELLED are terminal
assertTerminal(ENDED);
assertTerminal(CANCELLED);
}
@Test
@TestOfyAndSql
void testSetDiscountFractionTooHigh() {
IllegalArgumentException thrown =
assertThrows(
@@ -319,7 +297,7 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Discount fraction must be between 0 and 1 inclusive");
}
@Test
@TestOfyAndSql
void testSetDiscountFractionTooLow() {
IllegalArgumentException thrown =
assertThrows(
@@ -330,7 +308,7 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Discount fraction must be between 0 and 1 inclusive");
}
@Test
@TestOfyAndSql
void testSetDiscountYearsTooHigh() {
IllegalArgumentException thrown =
assertThrows(
@@ -341,7 +319,7 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Discount years must be between 1 and 10 inclusive");
}
@Test
@TestOfyAndSql
void testSetDiscountYearsTooLow() {
IllegalArgumentException thrown =
assertThrows(
@@ -352,7 +330,7 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Discount years must be between 1 and 10 inclusive");
}
@Test
@TestOfyAndSql
void testBuild_noTokenType() {
IllegalArgumentException thrown =
assertThrows(
@@ -361,7 +339,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Token type must be specified");
}
@Test
@TestOfyAndSql
void testBuild_noToken() {
IllegalArgumentException thrown =
assertThrows(
@@ -370,7 +348,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Token must not be null or empty");
}
@Test
@TestOfyAndSql
void testBuild_emptyToken() {
IllegalArgumentException thrown =
assertThrows(
@@ -379,7 +357,7 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().isEqualTo("Token must not be blank");
}
@Test
@TestOfyAndSql
void testBuild_discountPremiumsRequiresDiscountFraction() {
IllegalArgumentException thrown =
assertThrows(
@@ -395,7 +373,7 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Discount premiums can only be specified along with a discount fraction");
}
@Test
@TestOfyAndSql
void testBuild_discountYearsRequiresDiscountFraction() {
IllegalArgumentException thrown =
assertThrows(
@@ -14,7 +14,6 @@
package google.registry.model.history;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
@@ -92,10 +91,7 @@ public class DomainHistoryTest extends EntityTestCase {
assertDomainHistoriesEqual(fromDatabase, domainHistory);
assertThat(fromDatabase.getParentVKey()).isEqualTo(domainHistory.getParentVKey());
assertThat(fromDatabase.getNsHosts())
.containsExactlyElementsIn(
domainHistory.getNsHosts().stream()
.map(key -> VKey.createSql(HostResource.class, key.getSqlKey()))
.collect(toImmutableSet()));
.containsExactlyElementsIn(domainHistory.getNsHosts());
});
}
@@ -17,13 +17,17 @@ package google.registry.model.host;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.cloneAndSetAutoTimestamps;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistNewRegistrars;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.HostResourceSubject.assertAboutHosts;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InetAddresses;
import google.registry.model.EntityTestCase;
@@ -32,11 +36,14 @@ import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link HostResource}. */
@DualDatabaseTest
class HostResourceTest extends EntityTestCase {
private final DateTime day3 = fakeClock.nowUtc();
@@ -49,6 +56,7 @@ class HostResourceTest extends EntityTestCase {
@BeforeEach
void setUp() {
createTld("com");
persistNewRegistrars("gaining", "losing", "thisRegistrar", "thatRegistrar");
// Set up a new persisted registrar entity.
domain =
persistResource(
@@ -71,9 +79,9 @@ class HostResourceTest extends EntityTestCase {
new HostResource.Builder()
.setRepoId("DEADBEEF-COM")
.setHostName("ns1.example.com")
.setCreationClientId("a registrar")
.setCreationClientId("thisRegistrar")
.setLastEppUpdateTime(fakeClock.nowUtc())
.setLastEppUpdateClientId("another registrar")
.setLastEppUpdateClientId("thatRegistrar")
.setLastTransferTime(fakeClock.nowUtc())
.setInetAddresses(ImmutableSet.of(InetAddresses.forString("127.0.0.1")))
.setStatusValues(ImmutableSet.of(StatusValue.OK))
@@ -81,13 +89,22 @@ class HostResourceTest extends EntityTestCase {
.build()));
}
@Test
@TestOfyAndSql
void testPersistence() {
HostResource newHost = host.asBuilder().setRepoId("NEWHOST").build();
tm().transact(() -> tm().insert(newHost));
assertThat(ImmutableList.of(tm().transact(() -> tm().load(newHost.createVKey()))))
.comparingElementsUsing(immutableObjectCorrespondence("revisions"))
.containsExactly(newHost);
}
@TestOfyOnly
void testLoadingByForeignKey() {
assertThat(loadByForeignKey(HostResource.class, host.getForeignKey(), fakeClock.nowUtc()))
.hasValue(host);
}
@Test
@TestOfyOnly
void testIndexing() throws Exception {
// Clone it and save it before running the indexing test so that its transferData fields are
// populated from the superordinate domain.
@@ -100,7 +117,7 @@ class HostResourceTest extends EntityTestCase {
"currentSponsorClientId");
}
@Test
@TestOfyAndSql
void testEmptyStringsBecomeNull() {
assertThat(
new HostResource.Builder()
@@ -122,7 +139,7 @@ class HostResourceTest extends EntityTestCase {
.isNotNull();
}
@Test
@TestOfyAndSql
void testEmptySetsBecomeNull() {
assertThat(new HostResource.Builder().setInetAddresses(null).build().inetAddresses).isNull();
assertThat(new HostResource.Builder().setInetAddresses(ImmutableSet.of()).build().inetAddresses)
@@ -135,7 +152,7 @@ class HostResourceTest extends EntityTestCase {
.isNotNull();
}
@Test
@TestOfyAndSql
void testImplicitStatusValues() {
// OK is implicit if there's no other statuses.
assertAboutHosts()
@@ -157,13 +174,13 @@ class HostResourceTest extends EntityTestCase {
.hasExactlyStatusValues(StatusValue.CLIENT_HOLD);
}
@Test
@TestOfyAndSql
void testToHydratedString_notCircular() {
// If there are circular references, this will overflow the stack.
host.toHydratedString();
}
@Test
@TestOfyAndSql
void testFailure_uppercaseHostName() {
IllegalArgumentException thrown =
assertThrows(
@@ -173,7 +190,7 @@ class HostResourceTest extends EntityTestCase {
.contains("Host name must be in puny-coded, lower-case form");
}
@Test
@TestOfyAndSql
void testFailure_utf8HostName() {
IllegalArgumentException thrown =
assertThrows(
@@ -183,14 +200,14 @@ class HostResourceTest extends EntityTestCase {
.contains("Host name must be in puny-coded, lower-case form");
}
@Test
@TestOfyAndSql
void testComputeLastTransferTime_hostNeverSwitchedDomains_domainWasNeverTransferred() {
domain = domain.asBuilder().setLastTransferTime(null).build();
host = host.asBuilder().setLastTransferTime(null).setLastSuperordinateChange(null).build();
assertThat(host.computeLastTransferTime(domain)).isNull();
}
@Test
@TestOfyAndSql
void testComputeLastTransferTime_hostNeverSwitchedDomains_domainWasTransferred() {
// Host was created on Day 1.
// Domain was transferred on Day 2.
@@ -205,7 +222,7 @@ class HostResourceTest extends EntityTestCase {
assertThat(host.computeLastTransferTime(domain)).isEqualTo(day2);
}
@Test
@TestOfyAndSql
void testComputeLastTransferTime_hostCreatedAfterDomainWasTransferred() {
// Domain was transferred on Day 1.
// Host was created subordinate to domain on Day 2.
@@ -217,9 +234,9 @@ class HostResourceTest extends EntityTestCase {
.setCreationTime(day2)
.setRepoId("DEADBEEF-COM")
.setHostName("ns1.example.com")
.setCreationClientId("a registrar")
.setCreationClientId("thisRegistrar")
.setLastEppUpdateTime(fakeClock.nowUtc())
.setLastEppUpdateClientId("another registrar")
.setLastEppUpdateClientId("thatRegistrar")
.setInetAddresses(ImmutableSet.of(InetAddresses.forString("127.0.0.1")))
.setStatusValues(ImmutableSet.of(StatusValue.OK))
.setSuperordinateDomain(domain.createVKey())
@@ -227,7 +244,7 @@ class HostResourceTest extends EntityTestCase {
assertThat(host.computeLastTransferTime(domain)).isNull();
}
@Test
@TestOfyAndSql
void testComputeLastTransferTime_hostWasTransferred_domainWasNeverTransferred() {
// Host was transferred on Day 1.
// Host was made subordinate to domain on Day 2.
@@ -237,7 +254,7 @@ class HostResourceTest extends EntityTestCase {
assertThat(host.computeLastTransferTime(domain)).isEqualTo(day1);
}
@Test
@TestOfyAndSql
void testComputeLastTransferTime_domainWasTransferredBeforeHostBecameSubordinate() {
// Host was transferred on Day 1.
// Domain was transferred on Day 2.
@@ -247,7 +264,7 @@ class HostResourceTest extends EntityTestCase {
assertThat(host.computeLastTransferTime(domain)).isEqualTo(day1);
}
@Test
@TestOfyAndSql
void testComputeLastTransferTime_domainWasTransferredAfterHostBecameSubordinate() {
// Host was transferred on Day 1.
// Host was made subordinate to domain on Day 2.
@@ -20,6 +20,7 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistDeletedHost;
@@ -29,16 +30,21 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableList;
import google.registry.model.EntityTestCase;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyHostIndex;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestCacheExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link ForeignKeyIndex}. */
public class ForeignKeyIndexTest extends EntityTestCase {
@DualDatabaseTest
class ForeignKeyIndexTest extends EntityTestCase {
@RegisterExtension
public final TestCacheExtension testCacheExtension =
@@ -49,7 +55,17 @@ public class ForeignKeyIndexTest extends EntityTestCase {
createTld("com");
}
@Test
@TestSqlOnly
void testModifyForeignKeyIndex_notThrowExceptionInSql() {
DomainBase domainBase = newDomainBase("test.com");
ForeignKeyIndex<DomainBase> fki = ForeignKeyIndex.create(domainBase, fakeClock.nowUtc());
tm().transact(() -> tm().insert(fki));
tm().transact(() -> tm().put(fki));
tm().transact(() -> tm().delete(fki));
tm().transact(() -> tm().update(fki));
}
@TestOfyOnly
void testPersistence() {
// Persist a host and implicitly persist a ForeignKeyIndex for it.
HostResource host = persistActiveHost("ns1.example.com");
@@ -59,7 +75,7 @@ public class ForeignKeyIndexTest extends EntityTestCase {
assertThat(fki.getDeletionTime()).isEqualTo(END_OF_TIME);
}
@Test
@TestOfyOnly
void testIndexing() throws Exception {
// Persist a host and implicitly persist a ForeignKeyIndex for it.
persistActiveHost("ns1.example.com");
@@ -68,38 +84,50 @@ public class ForeignKeyIndexTest extends EntityTestCase {
"deletionTime");
}
@Test
@TestOfyAndSql
void testLoadForNonexistentForeignKey_returnsNull() {
assertThat(ForeignKeyIndex.load(HostResource.class, "ns1.example.com", fakeClock.nowUtc()))
.isNull();
}
@Test
@TestOfyAndSql
void testLoadForDeletedForeignKey_returnsNull() {
HostResource host = persistActiveHost("ns1.example.com");
persistResource(ForeignKeyIndex.create(host, fakeClock.nowUtc().minusDays(1)));
if (tm().isOfy()) {
persistResource(ForeignKeyIndex.create(host, fakeClock.nowUtc().minusDays(1)));
} else {
persistResource(host.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build());
}
assertThat(ForeignKeyIndex.load(HostResource.class, "ns1.example.com", fakeClock.nowUtc()))
.isNull();
}
@Test
@TestOfyAndSql
void testLoad_newerKeyHasBeenSoftDeleted() {
HostResource host1 = persistActiveHost("ns1.example.com");
fakeClock.advanceOneMilli();
ForeignKeyHostIndex fki = new ForeignKeyHostIndex();
fki.foreignKey = "ns1.example.com";
fki.topReference = host1.createVKey();
fki.deletionTime = fakeClock.nowUtc();
persistResource(fki);
if (tm().isOfy()) {
ForeignKeyHostIndex fki = new ForeignKeyHostIndex();
fki.foreignKey = "ns1.example.com";
fki.topReference = host1.createVKey();
fki.deletionTime = fakeClock.nowUtc();
persistResource(fki);
} else {
persistResource(host1.asBuilder().setDeletionTime(fakeClock.nowUtc()).build());
}
assertThat(ForeignKeyIndex.load(HostResource.class, "ns1.example.com", fakeClock.nowUtc()))
.isNull();
}
@Test
@TestOfyAndSql
void testBatchLoad_skipsDeletedAndNonexistent() {
persistActiveHost("ns1.example.com");
HostResource host = persistActiveHost("ns2.example.com");
persistResource(ForeignKeyIndex.create(host, fakeClock.nowUtc().minusDays(1)));
if (tm().isOfy()) {
persistResource(ForeignKeyIndex.create(host, fakeClock.nowUtc().minusDays(1)));
} else {
persistResource(host.asBuilder().setDeletionTime(fakeClock.nowUtc().minusDays(1)).build());
}
assertThat(
ForeignKeyIndex.load(
HostResource.class,
@@ -109,7 +137,7 @@ public class ForeignKeyIndexTest extends EntityTestCase {
.containsExactly("ns1.example.com");
}
@Test
@TestOfyAndSql
void testDeadCodeThatDeletedScrapCommandsReference() {
persistActiveHost("omg");
assertThat(ForeignKeyIndex.load(HostResource.class, "omg", fakeClock.nowUtc()).getForeignKey())
@@ -124,7 +152,7 @@ public class ForeignKeyIndexTest extends EntityTestCase {
return ForeignKeyIndex.load(ContactResource.class, contactId, fakeClock.nowUtc());
}
@Test
@TestOfyOnly
void test_loadCached_cachesNonexistenceOfHosts() {
assertThat(
ForeignKeyIndex.loadCached(
@@ -144,7 +172,7 @@ public class ForeignKeyIndexTest extends EntityTestCase {
.containsExactly("ns4.example.com", loadHostFki("ns4.example.com"));
}
@Test
@TestOfyOnly
void test_loadCached_cachesExistenceOfHosts() {
HostResource host1 = persistActiveHost("ns1.example.com");
HostResource host2 = persistActiveHost("ns2.example.com");
@@ -172,7 +200,7 @@ public class ForeignKeyIndexTest extends EntityTestCase {
"ns3.example.com", loadHostFki("ns3.example.com"));
}
@Test
@TestOfyOnly
void test_loadCached_doesntSeeHostChangesWhileCacheIsValid() {
HostResource originalHost = persistActiveHost("ns1.example.com");
ForeignKeyIndex<HostResource> originalFki = loadHostFki("ns1.example.com");
@@ -195,7 +223,7 @@ public class ForeignKeyIndexTest extends EntityTestCase {
.containsExactly("ns1.example.com", originalFki);
}
@Test
@TestOfyOnly
void test_loadCached_filtersOutSoftDeletedHosts() {
persistActiveHost("ns1.example.com");
persistDeletedHost("ns2.example.com", fakeClock.nowUtc().minusDays(1));
@@ -207,7 +235,7 @@ public class ForeignKeyIndexTest extends EntityTestCase {
.containsExactly("ns1.example.com", loadHostFki("ns1.example.com"));
}
@Test
@TestOfyOnly
void test_loadCached_cachesContactFkis() {
persistActiveContact("contactid1");
ForeignKeyIndex<ContactResource> fki1 = loadContactFki("contactid1");
@@ -50,21 +50,6 @@ public class SignedMarkRevocationListDaoTest {
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
}
@Test
void trySave_failureIsSwallowed() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.getLatestRevision().get();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
// This should throw an exception, which is swallowed and nothing changed
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList secondFromDb = SignedMarkRevocationListDao.getLatestRevision().get();
assertAboutImmutableObjects().that(secondFromDb).isEqualExceptFields(fromDb);
}
@Test
void testRetrieval_notPresent() {
assertThat(SignedMarkRevocationListDao.getLatestRevision().isPresent()).isFalse();
@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.smd.SignedMarkRevocationList.SHARD_SIZE;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.Duration.standardDays;
@@ -138,6 +139,33 @@ public class SignedMarkRevocationListTest {
.isEqualTo(DateTime.parse("2000-01-01T00:00:00Z"));
}
@Test
void test_getCreationTime_missingInCloudSQL() {
clock.setTo(DateTime.parse("2000-01-01T00:00:00Z"));
createSaveGetHelper(1);
jpaTm().transact(() -> jpaTm().delete(SignedMarkRevocationListDao.getLatestRevision().get()));
RuntimeException thrown =
assertThrows(RuntimeException.class, () -> SignedMarkRevocationList.get());
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Signed mark revocation list in Cloud SQL is empty.");
}
@Test
void test_getCreationTime_unequalListsInDatabases() {
clock.setTo(DateTime.parse("2000-01-01T00:00:00Z"));
createSaveGetHelper(1);
ImmutableMap.Builder<String, DateTime> revokes = new ImmutableMap.Builder<>();
for (int i = 0; i < 3; i++) {
revokes.put(Integer.toString(i), clock.nowUtc());
}
SignedMarkRevocationListDao.trySave(
SignedMarkRevocationList.create(clock.nowUtc(), revokes.build()));
RuntimeException thrown =
assertThrows(RuntimeException.class, () -> SignedMarkRevocationList.get());
assertThat(thrown).hasMessageThat().contains("Unequal SM revocation lists detected:");
}
@Test
void test_isSmdRevoked_present() {
final int rows = SHARD_SIZE + 1;
@@ -0,0 +1,36 @@
// 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.translators;
import static google.registry.model.translators.EppHistoryVKeyTranslatorFactory.kindPathToVKeyClass;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test;
/** Unit test for {@link EppHistoryVKeyTranslatorFactory}. */
class EppHistoryVKeyTranslatorFactoryTest {
@Test
void assertAllVKeyClassesHavingCreateFromOfyKeyMethod() {
kindPathToVKeyClass.forEach(
(kindPath, vKeyClass) -> {
try {
vKeyClass.getDeclaredMethod("create", com.googlecode.objectify.Key.class);
} catch (NoSuchMethodException e) {
fail("Missing static method create(com.googlecode.objectify.Key) on " + vKeyClass, e);
}
});
}
}
@@ -0,0 +1,116 @@
// 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.persistence;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.domain.DomainBase;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.BillingVKey.BillingEventVKey;
import google.registry.persistence.BillingVKey.BillingRecurrenceVKey;
import google.registry.schema.replay.EntityTest.EntityForTesting;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import javax.persistence.Transient;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit test for {@link BillingVKey}. */
@DualDatabaseTest
class BillingVKeyTest {
@RegisterExtension
final AppEngineExtension appEngine =
AppEngineExtension.builder()
.withDatastoreAndCloudSql()
.withOfyTestEntities(BillingVKeyTestEntity.class)
.withJpaUnitTestEntities(BillingVKeyTestEntity.class)
.build();
@TestOfyAndSql
void testRestoreSymmetricVKey() {
Key<HistoryEntry> domainHistoryKey =
Key.create(Key.create(DomainBase.class, "domainRepoId"), HistoryEntry.class, 10L);
Key<BillingEvent.OneTime> oneTimeOfyKey =
Key.create(domainHistoryKey, BillingEvent.OneTime.class, 100L);
VKey<BillingEvent.OneTime> oneTimeVKey =
VKey.create(BillingEvent.OneTime.class, 100L, oneTimeOfyKey);
Key<BillingEvent.Recurring> recurringOfyKey =
Key.create(domainHistoryKey, BillingEvent.Recurring.class, 200L);
VKey<BillingEvent.Recurring> recurringVKey =
VKey.create(BillingEvent.Recurring.class, 200L, recurringOfyKey);
BillingVKeyTestEntity original = new BillingVKeyTestEntity(oneTimeVKey, recurringVKey);
tm().transact(() -> tm().insert(original));
BillingVKeyTestEntity persisted = tm().transact(() -> tm().load(original.createVKey()));
assertThat(persisted).isEqualTo(original);
assertThat(persisted.getBillingEventVKey()).isEqualTo(oneTimeVKey);
assertThat(persisted.getBillingRecurrenceVKey()).isEqualTo(recurringVKey);
}
@TestOfyAndSql
void testHandleNullVKeyCorrectly() {
BillingVKeyTestEntity original = new BillingVKeyTestEntity(null, null);
tm().transact(() -> tm().insert(original));
BillingVKeyTestEntity persisted = tm().transact(() -> tm().load(original.createVKey()));
assertThat(persisted).isEqualTo(original);
}
@EntityForTesting
@Entity
@javax.persistence.Entity
private static class BillingVKeyTestEntity extends ImmutableObject {
@Transient @Parent Key<EntityGroupRoot> parent = getCrossTldKey();
@Id @javax.persistence.Id String id = "id";
BillingEventVKey billingEventVKey;
BillingRecurrenceVKey billingRecurrenceVKey;
BillingVKeyTestEntity() {}
BillingVKeyTestEntity(
VKey<BillingEvent.OneTime> onetime, VKey<BillingEvent.Recurring> recurring) {
this.billingEventVKey = BillingEventVKey.create(onetime);
this.billingRecurrenceVKey = BillingRecurrenceVKey.create(recurring);
}
VKey<BillingEvent.OneTime> getBillingEventVKey() {
return billingEventVKey.createVKey();
}
VKey<BillingEvent.Recurring> getBillingRecurrenceVKey() {
return billingRecurrenceVKey.createVKey();
}
VKey<BillingVKeyTestEntity> createVKey() {
return VKey.create(
BillingVKeyTestEntity.class, id, Key.create(parent, BillingVKeyTestEntity.class, id));
}
}
}
@@ -56,12 +56,17 @@ class DomainHistoryVKeyTest {
TestEntity persisted = tm().transact(() -> tm().load(original.createVKey()));
assertThat(persisted).isEqualTo(original);
// Double check that the persisted.domainHistoryVKey is a symmetric VKey
assertThat(persisted.domainHistoryVKey.getKind()).isEqualTo(HistoryEntry.class);
assertThat(persisted.domainHistoryVKey.getOfyKey())
assertThat(persisted.domainHistoryVKey.createOfyKey())
.isEqualTo(
Key.create(Key.create(DomainBase.class, "domainRepoId"), HistoryEntry.class, 10L));
assertThat(persisted.domainHistoryVKey.getSqlKey())
assertThat(persisted.domainHistoryVKey.createSqlKey())
.isEqualTo(new DomainHistoryId("domainRepoId", 10L));
assertThat(persisted.domainHistoryVKey.createVKey())
.isEqualTo(
VKey.create(
HistoryEntry.class,
new DomainHistoryId("domainRepoId", 10L),
Key.create(Key.create(DomainBase.class, "domainRepoId"), HistoryEntry.class, 10L)));
}
@TestOfyAndSql
@@ -69,9 +74,12 @@ class DomainHistoryVKeyTest {
Key<HistoryEntry> ofyKey =
Key.create(Key.create(DomainBase.class, "domainRepoId"), HistoryEntry.class, 10L);
DomainHistoryVKey domainHistoryVKey = DomainHistoryVKey.create(ofyKey);
assertThat(domainHistoryVKey.getKind()).isEqualTo(HistoryEntry.class);
assertThat(domainHistoryVKey.getOfyKey()).isEqualTo(ofyKey);
assertThat(domainHistoryVKey.getSqlKey()).isEqualTo(new DomainHistoryId("domainRepoId", 10L));
assertThat(domainHistoryVKey.createOfyKey()).isEqualTo(ofyKey);
assertThat(domainHistoryVKey.createSqlKey())
.isEqualTo(new DomainHistoryId("domainRepoId", 10L));
assertThat(domainHistoryVKey.createVKey())
.isEqualTo(
VKey.create(HistoryEntry.class, new DomainHistoryId("domainRepoId", 10L), ofyKey));
}
@EntityForTesting
@@ -19,8 +19,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import google.registry.persistence.VKey;
import google.registry.persistence.WithLongVKey;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestExtension;
import google.registry.testing.AppEngineExtension;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.junit.jupiter.api.Test;
@@ -30,33 +29,56 @@ import org.junit.jupiter.api.extension.RegisterExtension;
public class LongVKeyConverterTest {
@RegisterExtension
public final JpaUnitTestExtension jpaExtension =
new JpaTestRules.Builder()
.withEntityClass(TestEntity.class, VKeyConverter_LongType.class)
.buildUnitTestRule();
public final AppEngineExtension appEngineExtension =
new AppEngineExtension.Builder()
.withDatastoreAndCloudSql()
.withoutCannedData()
.withJpaUnitTestEntities(
TestLongEntity.class,
VKeyConverter_LongType.class,
VKeyConverter_CompositeLongType.class)
.withOfyTestEntities(TestLongEntity.class, CompositeKeyTestLongEntity.class)
.build();
@Test
void testRoundTrip() {
TestEntity original = new TestEntity(VKey.createSql(TestEntity.class, 10L));
TestLongEntity original =
new TestLongEntity(
VKey.createSql(TestLongEntity.class, 10L),
VKey.createSql(CompositeKeyTestLongEntity.class, 20L));
jpaTm().transact(() -> jpaTm().getEntityManager().persist(original));
TestEntity retrieved =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
TestLongEntity retrieved =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestLongEntity.class, "id"));
assertThat(retrieved.number.getSqlKey()).isEqualTo(10L);
assertThat(retrieved.number.getOfyKey().getId()).isEqualTo(10L);
assertThat(retrieved.composite.getSqlKey()).isEqualTo(20L);
assertThat(retrieved.composite.maybeGetOfyKey().isPresent()).isFalse();
}
@Entity(name = "TestEntity")
@Entity(name = "TestLongEntity")
@com.googlecode.objectify.annotation.Entity
@WithLongVKey(classNameSuffix = "LongType")
static class TestEntity {
@Id String id = "id";
static class TestLongEntity {
@com.googlecode.objectify.annotation.Id @Id String id = "id";
VKey<TestEntity> number;
VKey<TestLongEntity> number;
VKey<CompositeKeyTestLongEntity> composite;
TestEntity(VKey<TestEntity> number) {
TestLongEntity(VKey<TestLongEntity> number, VKey<CompositeKeyTestLongEntity> composite) {
this.number = number;
this.composite = composite;
}
/** Default constructor, needed for hibernate. */
public TestEntity() {}
public TestLongEntity() {}
}
@Entity(name = "CompositeKeyTestLongEntity")
@com.googlecode.objectify.annotation.Entity
@WithLongVKey(classNameSuffix = "CompositeLongType", compositeKey = true)
static class CompositeKeyTestLongEntity {
@com.googlecode.objectify.annotation.Id @Id String id = "id";
}
}
@@ -19,8 +19,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestExtension;
import google.registry.testing.AppEngineExtension;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.junit.jupiter.api.Test;
@@ -30,36 +29,61 @@ import org.junit.jupiter.api.extension.RegisterExtension;
public class StringVKeyConverterTest {
@RegisterExtension
public final JpaUnitTestExtension jpaExtension =
new JpaTestRules.Builder()
.withEntityClass(TestEntity.class, VKeyConverter_StringType.class)
.buildUnitTestRule();
public final AppEngineExtension appEngineExtension =
new AppEngineExtension.Builder()
.withDatastoreAndCloudSql()
.withoutCannedData()
.withJpaUnitTestEntities(
TestStringEntity.class,
VKeyConverter_StringType.class,
VKeyConverter_CompositeStringType.class)
.withOfyTestEntities(TestStringEntity.class, CompositeKeyTestStringEntity.class)
.build();
@Test
void testRoundTrip() {
TestEntity original =
new TestEntity("TheRealSpartacus", VKey.createSql(TestEntity.class, "ImSpartacus!"));
TestStringEntity original =
new TestStringEntity(
"TheRealSpartacus",
VKey.createSql(TestStringEntity.class, "ImSpartacus!"),
VKey.createSql(CompositeKeyTestStringEntity.class, "NoImSpartacus!"));
jpaTm().transact(() -> jpaTm().getEntityManager().persist(original));
TestEntity retrieved =
TestStringEntity retrieved =
jpaTm()
.transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "TheRealSpartacus"));
.transact(
() -> jpaTm().getEntityManager().find(TestStringEntity.class, "TheRealSpartacus"));
assertThat(retrieved.other.getSqlKey()).isEqualTo("ImSpartacus!");
assertThat(retrieved.other.getOfyKey().getName()).isEqualTo("ImSpartacus!");
assertThat(retrieved.composite.getSqlKey()).isEqualTo("NoImSpartacus!");
assertThat(retrieved.composite.maybeGetOfyKey().isPresent()).isFalse();
}
@Entity(name = "TestEntity")
@Entity(name = "TestStringEntity")
@com.googlecode.objectify.annotation.Entity
@WithStringVKey(classNameSuffix = "StringType")
static class TestEntity {
@Id String id;
static class TestStringEntity {
@com.googlecode.objectify.annotation.Id @Id String id;
VKey<TestEntity> other;
VKey<TestStringEntity> other;
VKey<CompositeKeyTestStringEntity> composite;
TestEntity(String id, VKey<TestEntity> other) {
TestStringEntity(
String id, VKey<TestStringEntity> other, VKey<CompositeKeyTestStringEntity> composite) {
this.id = id;
this.other = other;
this.composite = composite;
}
/** Default constructor, needed for hibernate. */
public TestEntity() {}
public TestStringEntity() {}
}
@Entity(name = "CompositeKeyTestStringEntity")
@com.googlecode.objectify.annotation.Entity
@WithStringVKey(classNameSuffix = "CompositeStringType", compositeKey = true)
static class CompositeKeyTestStringEntity {
@com.googlecode.objectify.annotation.Id @Id String id = "id";
}
}
@@ -32,6 +32,11 @@ public class FakeSecretManagerClient implements SecretManagerClient {
@Inject
FakeSecretManagerClient() {}
@Override
public String getProject() {
return "fake_project";
}
@Override
public void createSecret(String secretId) {
checkNotNull(secretId, "secretId");
@@ -41,6 +46,12 @@ public class FakeSecretManagerClient implements SecretManagerClient {
secrets.put(secretId, new SecretEntry(secretId));
}
@Override
public boolean secretExists(String secretId) {
checkNotNull(secretId, "secretId");
return secrets.containsKey(secretId);
}
@Override
public Iterable<String> listSecrets() {
return ImmutableSet.copyOf(secrets.keySet());
@@ -78,6 +89,28 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return secretEntry.getVersion(version).getData();
}
@Override
public void enableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
SecretEntry secretEntry = secrets.get(secretId);
if (secretEntry == null) {
throw new NoSuchSecretResourceException(null);
}
secretEntry.enableVersion(version);
}
@Override
public void disableSecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
checkNotNull(version, "version");
SecretEntry secretEntry = secrets.get(secretId);
if (secretEntry == null) {
throw new NoSuchSecretResourceException(null);
}
secretEntry.disableVersion(version);
}
@Override
public void destroySecretVersion(String secretId, String version) {
checkNotNull(secretId, "secretId");
@@ -118,6 +151,20 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return state;
}
void enable() {
if (state.equals(State.DESTROYED)) {
throw new SecretManagerException(null);
}
state = State.ENABLED;
}
void disable() {
if (state.equals(State.DESTROYED)) {
throw new SecretManagerException(null);
}
state = State.DISABLED;
}
void destroy() {
data = null;
state = State.DESTROYED;
@@ -145,6 +192,8 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return versions.get(index);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version " + version.get());
} catch (ArrayIndexOutOfBoundsException e) {
throw new NoSuchSecretResourceException(null);
}
}
@@ -156,15 +205,16 @@ public class FakeSecretManagerClient implements SecretManagerClient {
return builder.build();
}
void enableVersion(String version) {
getVersion(Optional.of(version)).enable();
}
void disableVersion(String version) {
getVersion(Optional.of(version)).disable();
}
void destroyVersion(String version) {
try {
int index = Integer.valueOf(version);
versions.get(index).destroy();
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version " + version);
} catch (ArrayIndexOutOfBoundsException e) {
throw new NoSuchSecretResourceException(null);
}
getVersion(Optional.of(version)).destroy();
}
}
}
@@ -17,13 +17,20 @@ package google.registry.privileges.secretmanager;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
import com.google.cloud.secretmanager.v1.SecretVersion.State;
import google.registry.privileges.secretmanager.SecretManagerClient.NoSuchSecretResourceException;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretAlreadyExistsException;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretManagerException;
import google.registry.util.Retrier;
import google.registry.util.SystemSleeper;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
@@ -43,52 +50,53 @@ public class SecretManagerClientTest {
private static final String SECRET_ID_PREFIX = "TEST_" + UUID.randomUUID() + "_";
// Used for unique secret id generation.
private static int seqno = 0;
private static SecretManagerClient secretManagerClient;
private static boolean isUnitTest = true;
private static String nextSecretId() {
return SECRET_ID_PREFIX + seqno++;
}
private String secretId;
@BeforeAll
static void beforeAll() {
static void beforeAll() throws IOException {
String environmentName = System.getProperty("test.gcp_integration.env");
if (environmentName != null) {
secretManagerClient =
DaggerSecretManagerModule_SecretManagerComponent.builder()
.secretManagerModule(
new SecretManagerModule(String.format("domain-registry-%s", environmentName)))
.build()
.secretManagerClient();
SecretManagerModule.provideSecretManagerClient(
SecretManagerServiceSettings.newBuilder()
.setCredentialsProvider(() -> GoogleCredentials.getApplicationDefault())
.build(),
String.format("domain-registry-%s", environmentName),
new Retrier(new SystemSleeper(), 1));
isUnitTest = false;
} else {
secretManagerClient = new FakeSecretManagerClient();
}
}
@AfterAll
static void afterAll() {
@BeforeEach
void beforeEach() {
secretId = SECRET_ID_PREFIX + seqno++;
}
@AfterEach
void afterEach() throws IOException {
if (isUnitTest) {
return;
}
for (String secretId : secretManagerClient.listSecrets()) {
if (secretId.startsWith(SECRET_ID_PREFIX)) {
secretManagerClient.deleteSecret(secretId);
}
try {
secretManagerClient.deleteSecret(secretId);
} catch (NoSuchSecretResourceException e) {
// deleteSecret() deleted it already.
}
}
@Test
void createSecret_success() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
assertThat(secretManagerClient.listSecrets()).contains(secretId);
}
@Test
void createSecret_duplicate() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
assertThrows(
SecretAlreadyExistsException.class, () -> secretManagerClient.createSecret(secretId));
@@ -96,16 +104,25 @@ public class SecretManagerClientTest {
@Test
void addSecretVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED))
.containsExactly(version);
}
@Test
void secretExists_true() {
secretManagerClient.createSecret(secretId);
assertThat(secretManagerClient.secretExists(secretId)).isTrue();
}
@Test
void secretExists_False() {
assertThat(secretManagerClient.secretExists(secretId)).isFalse();
}
@Test
void getSecretData_byVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.getSecretData(secretId, Optional.of(version)))
@@ -114,15 +131,70 @@ public class SecretManagerClientTest {
@Test
void getSecretData_latestVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.getSecretData(secretId, Optional.empty())).isEqualTo("mydata");
}
@Test
void disableSecretVersion() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.disableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DISABLED)).contains(version);
}
@Test
void disableSecretVersion_ignoreAlreadyDisabled() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.disableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DISABLED)).contains(version);
secretManagerClient.disableSecretVersion(secretId, version);
}
@Test
void disableSecretVersion_destroyed() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.destroySecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DESTROYED)).contains(version);
assertThrows(
SecretManagerException.class,
() -> secretManagerClient.disableSecretVersion(secretId, version));
}
@Test
void enableSecretVersion_ignoreAlreadyEnabled() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED)).contains(version);
secretManagerClient.enableSecretVersion(secretId, version);
}
@Test
void enableSecretVersion() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.disableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DISABLED)).contains(version);
secretManagerClient.enableSecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.ENABLED)).contains(version);
}
@Test
void enableSecretVersion_destroyed() {
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.destroySecretVersion(secretId, version);
assertThat(secretManagerClient.listSecretVersions(secretId, State.DESTROYED)).contains(version);
assertThrows(
SecretManagerException.class,
() -> secretManagerClient.enableSecretVersion(secretId, version));
}
@Test
void destroySecretVersion() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
String version = secretManagerClient.addSecretVersion(secretId, "mydata");
secretManagerClient.destroySecretVersion(secretId, version);
@@ -134,7 +206,6 @@ public class SecretManagerClientTest {
@Test
void deleteSecret() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
assertThat(secretManagerClient.listSecrets()).contains(secretId);
secretManagerClient.deleteSecret(secretId);
@@ -0,0 +1,63 @@
// 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.privileges.secretmanager;
import static com.google.common.truth.Truth.assertThat;
import com.google.cloud.secretmanager.v1.SecretVersionName;
import google.registry.privileges.secretmanager.SqlUser.RobotId;
import google.registry.privileges.secretmanager.SqlUser.RobotUser;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link SqlCredentialStore}. */
public class SqlCredentialStoreTest {
private final SecretManagerClient client = new FakeSecretManagerClient();
private final SqlCredentialStore credentialStore = new SqlCredentialStore(client, "db");
private SqlUser user = new RobotUser(RobotId.NOMULUS);
@Test
void createSecret() {
credentialStore.createOrUpdateCredential(user, "password");
assertThat(client.secretExists("sql-cred-live-label-nomulus-db")).isTrue();
assertThat(
SecretVersionName.parse(
client.getSecretData("sql-cred-live-label-nomulus-db", Optional.empty()))
.getSecret())
.isEqualTo("sql-cred-data-nomulus-db");
assertThat(client.secretExists("sql-cred-data-nomulus-db")).isTrue();
assertThat(client.getSecretData("sql-cred-data-nomulus-db", Optional.empty()))
.isEqualTo("nomulus password");
}
@Test
void getCredential() {
credentialStore.createOrUpdateCredential(user, "password");
SqlCredential credential = credentialStore.getCredential(user);
assertThat(credential.login()).isEqualTo("nomulus");
assertThat(credential.password()).isEqualTo("password");
}
@Test
void deleteCredential() {
credentialStore.createOrUpdateCredential(user, "password");
assertThat(client.secretExists("sql-cred-live-label-nomulus-db")).isTrue();
assertThat(client.secretExists("sql-cred-data-nomulus-db")).isTrue();
credentialStore.deleteCredential(user);
assertThat(client.secretExists("sql-cred-live-label-nomulus-db")).isFalse();
assertThat(client.secretExists("sql-cred-data-nomulus-db")).isFalse();
}
}
@@ -34,7 +34,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link Spec11RegistrarThreatMatchesParser}. */
class Spec11RegistrarThreatMatchesParserTest {
public class Spec11RegistrarThreatMatchesParserTest {
private static final String TODAY = "2018-07-21";
private static final String YESTERDAY = "2018-07-20";
@@ -66,6 +66,12 @@ class Spec11RegistrarThreatMatchesParserTest {
assertThat(parser.getPreviousDateWithMatches(LocalDate.parse(TODAY))).isEmpty();
}
@Test
void testNonexistent_returnsEmpty() throws Exception {
assertThat(parser.getRegistrarThreatMatches(LocalDate.parse(YESTERDAY).minusYears(1)))
.isEmpty();
}
@Test
void testFindPrevious_olderThanYesterdayFound() throws Exception {
setupFile("spec11_fake_report_previous_day", "2018-07-14");
@@ -95,7 +101,7 @@ class Spec11RegistrarThreatMatchesParserTest {
}
/** The expected contents of the sample spec11 report file */
static ImmutableSet<RegistrarThreatMatches> sampleThreatMatches() throws Exception {
public static ImmutableSet<RegistrarThreatMatches> sampleThreatMatches() throws Exception {
return ImmutableSet.of(getMatchA(), getMatchB());
}
@@ -454,10 +454,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
.setEnvIsAdmin(userInfo.isAdmin());
}
if (clock != null) {
helper.setClock(() -> clock.nowUtc().getMillis());
}
if (withLocalModules) {
helper.setEnvInstance("0");
}
@@ -750,6 +750,20 @@ public class DatabaseHelper {
.build());
}
/** Persists and returns a {@link Registrar} with the specified registrarId. */
public static Registrar persistNewRegistrar(String registrarId) {
return persistNewRegistrar(registrarId, registrarId + " name", Registrar.Type.REAL, 100L);
}
/** Persists and returns a list of {@link Registrar}s with the specified registrarIds. */
public static ImmutableList<Registrar> persistNewRegistrars(String... registrarIds) {
ImmutableList.Builder<Registrar> newRegistrars = new ImmutableList.Builder<>();
for (String registrarId : registrarIds) {
newRegistrars.add(persistNewRegistrar(registrarId));
}
return newRegistrars.build();
}
private static Iterable<BillingEvent> getBillingEvents() {
return transactIfJpaTm(
() ->
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import java.lang.reflect.Field;
@@ -160,6 +161,14 @@ class DualDatabaseTestInvocationContextProvider implements TestTemplateInvocatio
private static boolean isDualDatabaseTest(ExtensionContext context) {
Object testInstance = context.getTestInstance().orElseThrow(RuntimeException::new);
return testInstance.getClass().isAnnotationPresent(DualDatabaseTest.class);
// If the test method is declared in its parent class,
// e.g. google.registry.flows.ResourceFlowTestCase.testRequiresLogin,
// we don't consider it is a DualDatabaseTest. This is because there may exist some subclasses
// that have not been migrated to DualDatabaseTest.
boolean isDeclaredTestMethod =
ImmutableSet.copyOf(testInstance.getClass().getDeclaredMethods())
.contains(context.getTestMethod().orElseThrow(RuntimeException::new));
return testInstance.getClass().isAnnotationPresent(DualDatabaseTest.class)
&& isDeclaredTestMethod;
}
}
@@ -17,7 +17,8 @@ package google.registry.tools;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.toArray;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC;
@@ -116,7 +117,7 @@ public abstract class CommandTestCase<C extends Command> {
} finally {
// Clear the session cache so that subsequent reads for verification purposes hit Datastore.
// This primarily matters for AutoTimestamp fields, which otherwise won't have updated values.
ofy().clearSessionCache();
tm().clearSessionCache();
// Reset back to UNITTEST environment.
RegistryToolEnvironment.UNITTEST.setup(systemPropertyExtension);
}
@@ -185,12 +186,12 @@ public abstract class CommandTestCase<C extends Command> {
/** Reloads the given resource from Datastore. */
<T> T reloadResource(T resource) {
return ofy().load().entity(resource).now();
return transactIfJpaTm(() -> tm().load(resource));
}
/** Returns count of all poll messages in Datastore. */
int getPollMessageCount() {
return ofy().load().type(PollMessage.class).count();
return transactIfJpaTm(() -> tm().loadAll(PollMessage.class).size());
}
/**

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